@holdpoint/cli 0.1.0-alpha.15 → 0.1.0-alpha.17

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 existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync, copyFileSync } from "fs";
9
- import { join, dirname } from "path";
10
- import { fileURLToPath } from "url";
11
- import chalk from "chalk";
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
+ import { fileURLToPath as fileURLToPath2 } from "url";
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,49 +54,272 @@ 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
- // src/commands/init.ts
58
+ // src/templates.ts
59
+ import { copyFileSync, existsSync as existsSync2, writeFileSync } from "fs";
60
+ import { join, dirname } from "path";
61
+ import { fileURLToPath } from "url";
62
62
  var __dirname = dirname(fileURLToPath(import.meta.url));
63
- function getTemplatePath(stack) {
64
- const name = stack === "unknown" ? "_base" : stack;
63
+ function getBundledTemplatePath(filename) {
65
64
  const candidates = [
66
- join(__dirname, "templates", `${name}.yaml`),
65
+ join(__dirname, "templates", filename),
67
66
  // dist/templates/ (published package)
68
- join(__dirname, "../../../templates", `${name}.yaml`),
67
+ join(__dirname, "../../../templates", filename),
69
68
  // monorepo dev fallback
70
- join(process.cwd(), "templates", `${name}.yaml`)
69
+ join(process.cwd(), "templates", filename)
71
70
  // cwd fallback
72
71
  ];
73
- for (const p of candidates) {
74
- if (existsSync2(p)) return p;
72
+ for (const candidate of candidates) {
73
+ if (existsSync2(candidate)) return candidate;
75
74
  }
76
75
  return "";
77
76
  }
78
- function getMasterPromptPath() {
77
+ function ensureBundledFile(outputPath, templateFilename, fallbackContent) {
78
+ if (existsSync2(outputPath)) {
79
+ return false;
80
+ }
81
+ const templatePath = getBundledTemplatePath(templateFilename);
82
+ if (templatePath) {
83
+ copyFileSync(templatePath, outputPath);
84
+ } else {
85
+ writeFileSync(outputPath, fallbackContent, "utf8");
86
+ }
87
+ return true;
88
+ }
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
+
310
+ // src/commands/init.ts
311
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
312
+ function getDefaultTemplatePath() {
79
313
  const candidates = [
80
- join(__dirname, "templates/MASTER_PROMPT.md"),
314
+ join2(__dirname2, "templates", "default.yaml"),
81
315
  // dist/templates/ (published package)
82
- join(__dirname, "../../../templates/MASTER_PROMPT.md"),
316
+ join2(__dirname2, "../../../templates", "default.yaml"),
83
317
  // monorepo dev fallback
84
- join(process.cwd(), "templates/MASTER_PROMPT.md")
318
+ join2(process.cwd(), "templates", "default.yaml")
85
319
  // cwd fallback
86
320
  ];
87
321
  for (const p of candidates) {
88
- if (existsSync2(p)) return p;
322
+ if (existsSync4(p)) return p;
89
323
  }
90
324
  return "";
91
325
  }
@@ -102,85 +336,122 @@ checks:
102
336
  label: "JSDoc on changed public functions"
103
337
  prompt: "Ensure all changed public functions and exports have JSDoc comments."
104
338
  `;
339
+ var MINIMAL_MASTER_PROMPT = `# Holdpoint
340
+
341
+ Run \`holdpoint check\` before marking any task complete.
342
+ See \`checks.yaml\` for the full list of checks.
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
+ `;
348
+ var MINIMAL_PREREQUISITES = `# Holdpoint prerequisites
349
+
350
+ Holdpoint installed repo-local engine integrations for one or more AI coding agents. Before relying on them locally, review these setup notes:
351
+
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.
354
+ - **OpenAI Codex** \u2014 project-level hooks require trust approval. Run \`codex trust\` in the Codex TUI or review the hook with \`/hooks\`.
355
+ - **General** \u2014 Holdpoint expects Node.js 18+ and a git repository so \`holdpoint init\`, \`holdpoint update\`, and \`holdpoint check\` can run normally.
356
+
357
+ Docs: https://holdpoint.dev/docs
358
+ `;
105
359
  async function initCommand(options) {
106
360
  const spinner = ora("Initialising Holdpoint\u2026").start();
107
- const stack = options.stack ?? detectStack();
108
361
  const agentOpt = options.agent;
109
362
  const agents = !agentOpt || agentOpt === "all" ? ["copilot", "claude", "cursor", "codex"] : [agentOpt];
110
- spinner.text = `Stack: ${chalk.cyan(stack)} \u2014 installing for: ${chalk.cyan(agents.join(", "))}`;
363
+ spinner.text = `Installing for: ${chalk2.cyan(agents.join(", "))}`;
111
364
  const pm = detectPackageManager();
112
365
  let yamlContent = MINIMAL_CHECKS_YAML;
113
- if (!existsSync2("checks.yaml")) {
114
- const templatePath = getTemplatePath(stack);
366
+ if (!existsSync4("checks.yaml")) {
367
+ const templatePath = getDefaultTemplatePath();
115
368
  if (templatePath) {
116
- yamlContent = readFileSync2(templatePath, "utf8");
369
+ yamlContent = readFileSync3(templatePath, "utf8");
117
370
  }
118
371
  if (pm !== "pnpm") {
119
372
  yamlContent = yamlContent.replace(/\bpnpm\b/g, pm);
120
373
  }
121
- writeFileSync("checks.yaml", yamlContent, "utf8");
374
+ writeFileSync3("checks.yaml", yamlContent, "utf8");
122
375
  } else {
123
- yamlContent = readFileSync2("checks.yaml", "utf8");
376
+ yamlContent = readFileSync3("checks.yaml", "utf8");
124
377
  }
125
378
  const config = parseHoldpointYaml(yamlContent);
126
379
  const generatedDir = ".github/holdpoint/generated";
127
- mkdirSync(generatedDir, { recursive: true });
128
- writeFileSync(`${generatedDir}/checks.immutable.json`, buildConfigJson(config), "utf8");
380
+ mkdirSync2(generatedDir, { recursive: true });
381
+ writeFileSync3(`${generatedDir}/checks.immutable.json`, buildConfigJson(config), "utf8");
129
382
  if (agents.includes("copilot")) {
130
383
  const extDir = ".github/extensions/holdpoint";
131
- mkdirSync(extDir, { recursive: true });
132
- writeFileSync(join(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");
133
387
  }
134
388
  if (agents.includes("claude")) {
135
- mkdirSync(".claude", { recursive: true });
389
+ mkdirSync2(".claude", { recursive: true });
136
390
  const settingsPath = ".claude/settings.json";
137
391
  let existing = {};
138
- if (existsSync2(settingsPath)) {
392
+ if (existsSync4(settingsPath)) {
139
393
  try {
140
- existing = JSON.parse(readFileSync2(settingsPath, "utf8"));
394
+ existing = JSON.parse(readFileSync3(settingsPath, "utf8"));
141
395
  } catch {
142
396
  }
143
397
  }
144
398
  const holdpointHooks = JSON.parse(buildClaudeEngineJson(config));
145
- writeFileSync(
399
+ writeFileSync3(
146
400
  settingsPath,
147
- JSON.stringify({ ...existing, hooks: holdpointHooks.hooks }, null, 2),
401
+ JSON.stringify(mergeClaudeSettings(existing, holdpointHooks), null, 2),
148
402
  "utf8"
149
403
  );
404
+ spliceBreadcrumb("CLAUDE.md");
150
405
  }
151
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");
152
423
  const cursorRules = buildCursorEngine(config);
153
424
  const cursorPath = ".cursorrules";
154
- if (existsSync2(cursorPath)) {
155
- const existing = readFileSync2(cursorPath, "utf8");
425
+ if (existsSync4(cursorPath)) {
426
+ const existing = readFileSync3(cursorPath, "utf8");
156
427
  if (!existing.includes("Holdpoint Rules")) {
157
- writeFileSync(cursorPath, existing + "\n" + cursorRules, "utf8");
428
+ writeFileSync3(cursorPath, `${existing.trimEnd()}
429
+
430
+ ${cursorRules}`, "utf8");
158
431
  }
159
432
  } else {
160
- writeFileSync(cursorPath, cursorRules, "utf8");
433
+ writeFileSync3(cursorPath, cursorRules, "utf8");
161
434
  }
435
+ spliceBreadcrumb(".cursor/rules/holdpoint.md");
162
436
  }
163
437
  if (agents.includes("codex")) {
164
- mkdirSync(".codex", { recursive: true });
165
- writeFileSync(".codex/hooks.json", buildCodexHooksJson(config), "utf8");
166
- writeFileSync(".codex/holdpoint-check.mjs", buildCodexCheckScript(config), "utf8");
167
- writeFileSync(".codex/config.toml", buildCodexConfigToml(), "utf8");
168
- const agentsMdPath = "AGENTS.md";
169
- const existing = existsSync2(agentsMdPath) ? readFileSync2(agentsMdPath, "utf8") : "";
170
- writeFileSync(agentsMdPath, spliceAgentsMd(existing, config), "utf8");
171
- }
172
- if (!existsSync2("MASTER_PROMPT.md")) {
173
- const guidePath = getMasterPromptPath();
174
- if (guidePath) {
175
- copyFileSync(guidePath, "MASTER_PROMPT.md");
176
- } else {
177
- writeFileSync(
178
- "MASTER_PROMPT.md",
179
- "# Holdpoint\n\nRun `holdpoint check` before marking any task complete.\nSee `checks.yaml` for the full list of checks.\n",
180
- "utf8"
181
- );
182
- }
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");
183
443
  }
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
+ );
450
+ ensureBundledFile(
451
+ "HOLDPOINT_PREREQUISITES.md",
452
+ "HOLDPOINT_PREREQUISITES.md",
453
+ MINIMAL_PREREQUISITES
454
+ );
184
455
  spinner.text = "Installing holdpoint as a devDependency\u2026";
185
456
  const installCmds = {
186
457
  pnpm: "pnpm add -D holdpoint@alpha",
@@ -189,40 +460,46 @@ async function initCommand(options) {
189
460
  };
190
461
  const installCmd = installCmds[pm];
191
462
  try {
192
- execSync(installCmd, { stdio: "pipe" });
193
- spinner.succeed(chalk.bold.green("Holdpoint initialised!"));
463
+ execSync2(installCmd, { stdio: "pipe" });
464
+ spinner.succeed(chalk2.bold.green("Holdpoint initialised!"));
194
465
  } catch {
195
466
  spinner.warn(
196
- chalk.yellow(`Holdpoint initialised, but could not install the package automatically.`) + `
197
- Run manually: ${chalk.yellow(installCmd)}`
467
+ chalk2.yellow(`Holdpoint initialised, but could not install the package automatically.`) + `
468
+ Run manually: ${chalk2.yellow(installCmd)}`
198
469
  );
199
470
  }
471
+ const preflight = runPreflight(agents);
472
+ printPreflight(preflight);
200
473
  console.log(`
201
- ${chalk.cyan("Next steps:")}
202
- 1. Edit ${chalk.yellow("checks.yaml")} to customise your eval checkpoints
203
- 2. Commit ${chalk.yellow("checks.yaml")} and the generated engine files
204
- 3. 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
205
479
 
206
- Visual builder: ${chalk.yellow("holdpoint builder")} (opens localhost:4321)
207
- 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(", "))}
208
482
  `);
209
483
  }
210
484
 
211
485
  // src/commands/check.ts
212
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
213
- import { join as join2 } from "path";
214
- import chalk2 from "chalk";
486
+ import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
487
+ import { join as join3 } from "path";
488
+ import chalk3 from "chalk";
215
489
  import ora2 from "ora";
216
490
  import { parseHoldpointYaml as parseHoldpointYaml2, matchesWhen } from "@holdpoint/yaml-core";
217
491
  import { runDeterministicChecks } from "@holdpoint/yaml-core/runner";
218
- import { execSync as execSync2 } from "child_process";
492
+ import { execSync as execSync3 } from "child_process";
493
+ import { randomUUID } from "crypto";
494
+ import { identifyProject } from "@holdpoint/live-daemon";
495
+ import { BridgeClient } from "@holdpoint/sdk";
219
496
  var COMMIT_CACHE_PATH = ".holdpoint/checked-commits.json";
220
497
  var COMMIT_CACHE_MAX = 100;
221
498
  var CHECK_REPORTS_PATH = ".holdpoint/check-reports.json";
222
499
  var CHECK_REPORTS_MAX = 50;
223
500
  function getStagedFiles() {
224
501
  try {
225
- const out = execSync2("git diff --cached --name-only", {
502
+ const out = execSync3("git diff --cached --name-only", {
226
503
  encoding: "utf8",
227
504
  stdio: ["pipe", "pipe", "ignore"]
228
505
  });
@@ -233,7 +510,7 @@ function getStagedFiles() {
233
510
  }
234
511
  function getAllChangedFiles() {
235
512
  try {
236
- const out = execSync2("git diff --name-only HEAD", {
513
+ const out = execSync3("git diff --name-only HEAD", {
237
514
  encoding: "utf8",
238
515
  stdio: ["pipe", "pipe", "ignore"]
239
516
  });
@@ -244,7 +521,7 @@ function getAllChangedFiles() {
244
521
  }
245
522
  function getLastCommitFiles() {
246
523
  try {
247
- const out = execSync2("git diff --name-only HEAD~1 HEAD", {
524
+ const out = execSync3("git diff --name-only HEAD~1 HEAD", {
248
525
  encoding: "utf8",
249
526
  stdio: ["pipe", "pipe", "ignore"]
250
527
  });
@@ -255,7 +532,7 @@ function getLastCommitFiles() {
255
532
  }
256
533
  function getHeadSha() {
257
534
  try {
258
- return execSync2("git rev-parse HEAD", {
535
+ return execSync3("git rev-parse HEAD", {
259
536
  encoding: "utf8",
260
537
  stdio: ["pipe", "pipe", "ignore"]
261
538
  }).trim();
@@ -265,7 +542,7 @@ function getHeadSha() {
265
542
  }
266
543
  function readCommitCache() {
267
544
  try {
268
- const raw = readFileSync3(COMMIT_CACHE_PATH, "utf8");
545
+ const raw = readFileSync4(COMMIT_CACHE_PATH, "utf8");
269
546
  const parsed = JSON.parse(raw);
270
547
  return new Set(Array.isArray(parsed.verified) ? parsed.verified : []);
271
548
  } catch {
@@ -276,18 +553,18 @@ function recordCommitCache(sha) {
276
553
  try {
277
554
  const existing = readCommitCache();
278
555
  const updated = [sha, ...[...existing].filter((s) => s !== sha)].slice(0, COMMIT_CACHE_MAX);
279
- mkdirSync2(join2(COMMIT_CACHE_PATH, ".."), { recursive: true });
280
- writeFileSync2(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");
281
558
  } catch {
282
559
  }
283
560
  }
284
561
  function recordCheckReport(run) {
285
562
  try {
286
- mkdirSync2(join2(CHECK_REPORTS_PATH, ".."), { recursive: true });
563
+ mkdirSync3(join3(CHECK_REPORTS_PATH, ".."), { recursive: true });
287
564
  let existing = { runs: [] };
288
- if (existsSync3(CHECK_REPORTS_PATH)) {
565
+ if (existsSync5(CHECK_REPORTS_PATH)) {
289
566
  try {
290
- existing = JSON.parse(readFileSync3(CHECK_REPORTS_PATH, "utf8"));
567
+ existing = JSON.parse(readFileSync4(CHECK_REPORTS_PATH, "utf8"));
291
568
  if (!Array.isArray(existing.runs)) existing.runs = [];
292
569
  } catch {
293
570
  existing = { runs: [] };
@@ -296,21 +573,21 @@ function recordCheckReport(run) {
296
573
  const updated = {
297
574
  runs: [run, ...existing.runs].slice(0, CHECK_REPORTS_MAX)
298
575
  };
299
- writeFileSync2(CHECK_REPORTS_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
576
+ writeFileSync4(CHECK_REPORTS_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
300
577
  } catch {
301
578
  }
302
579
  }
303
580
  async function checkCommand(options) {
304
- if (!existsSync3("checks.yaml")) {
305
- 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."));
306
583
  process.exit(1);
307
584
  }
308
- const yamlContent = readFileSync3("checks.yaml", "utf8");
585
+ const yamlContent = readFileSync4("checks.yaml", "utf8");
309
586
  let config;
310
587
  try {
311
588
  config = parseHoldpointYaml2(yamlContent);
312
589
  } catch (err) {
313
- console.error(chalk2.red("Invalid checks.yaml:"), err.message);
590
+ console.error(chalk3.red("Invalid checks.yaml:"), err.message);
314
591
  process.exit(1);
315
592
  }
316
593
  const headSha = getHeadSha();
@@ -323,7 +600,7 @@ async function checkCommand(options) {
323
600
  } else {
324
601
  if (headSha && readCommitCache().has(headSha)) {
325
602
  console.log(
326
- 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.`)
327
604
  );
328
605
  process.exit(0);
329
606
  }
@@ -332,10 +609,10 @@ async function checkCommand(options) {
332
609
  changedFiles = lastCommit;
333
610
  usedHeadShaForCache = true;
334
611
  console.log(
335
- 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.")
336
613
  );
337
614
  } else {
338
- 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."));
339
616
  process.exit(0);
340
617
  }
341
618
  }
@@ -343,10 +620,10 @@ async function checkCommand(options) {
343
620
  changedFiles = getAllChangedFiles();
344
621
  if (changedFiles.length === 0) {
345
622
  console.log(
346
- 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.")
347
624
  );
348
625
  console.log(
349
- chalk2.dim(
626
+ chalk3.dim(
350
627
  " Tip: if you just ran `holdpoint init`, commit the generated files to clear the git-commit check."
351
628
  )
352
629
  );
@@ -354,9 +631,9 @@ async function checkCommand(options) {
354
631
  }
355
632
  const guides = Object.entries(config.context?.guides ?? {});
356
633
  if (guides.length > 0) {
357
- console.log(chalk2.cyan("\nProject guides:"));
634
+ console.log(chalk3.cyan("\nProject guides:"));
358
635
  for (const [key, text] of guides) {
359
- console.log(chalk2.bold(` ${key}:`), chalk2.dim(String(text).trim()));
636
+ console.log(chalk3.bold(` ${key}:`), chalk3.dim(String(text).trim()));
360
637
  }
361
638
  console.log("");
362
639
  }
@@ -374,9 +651,9 @@ async function checkCommand(options) {
374
651
  console.log("");
375
652
  console.log(
376
653
  [
377
- chalk2.green(`\u2713 ${passed.length} passed`),
378
- failed.length > 0 ? chalk2.red(`\u2717 ${failed.length} failed`) : "",
379
- 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`) : ""
380
657
  ].filter(Boolean).join(" ")
381
658
  );
382
659
  const promptChecks = config.checks.filter(
@@ -384,9 +661,9 @@ async function checkCommand(options) {
384
661
  );
385
662
  if (promptChecks.length > 0) {
386
663
  console.log(`
387
- ${chalk2.cyan("Agent prompts to act on:")}`);
664
+ ${chalk3.cyan("Agent prompts to act on:")}`);
388
665
  for (const c of promptChecks) {
389
- console.log(` ${chalk2.yellow("\u25A1")} [${c.label}] ${c.prompt ?? ""}`);
666
+ console.log(` ${chalk3.yellow("\u25A1")} [${c.label}] ${c.prompt ?? ""}`);
390
667
  }
391
668
  }
392
669
  const reportResults = [
@@ -420,6 +697,30 @@ ${chalk2.cyan("Agent prompts to act on:")}`);
420
697
  }
421
698
  };
422
699
  recordCheckReport(run);
700
+ const project = identifyProject(process.cwd());
701
+ const bridge = new BridgeClient();
702
+ const liveEvents = reportResults.filter(
703
+ (result) => result.kind === "cmd"
704
+ ).map((result, index) => ({
705
+ v: 1,
706
+ id: randomUUID(),
707
+ ts: Date.now() + index,
708
+ engine: "holdpoint",
709
+ session_id: "check-runner",
710
+ project_hash: project.hash,
711
+ cwd: process.cwd(),
712
+ type: "check_run",
713
+ payload: {
714
+ check_id: result.id,
715
+ label: result.label,
716
+ status: result.status,
717
+ duration_ms: 0,
718
+ ...result.output ? { output: result.output } : {}
719
+ }
720
+ }));
721
+ if (liveEvents.length > 0) {
722
+ await bridge.sendEvents(liveEvents);
723
+ }
423
724
  if (failed.length > 0) {
424
725
  process.exit(1);
425
726
  }
@@ -428,258 +729,337 @@ ${chalk2.cyan("Agent prompts to act on:")}`);
428
729
  }
429
730
  }
430
731
  function printResult(result) {
431
- 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");
432
733
  const label = result.check.label;
433
734
  console.log(`${icon} ${label}`);
434
735
  if (result.status === "fail" && result.output) {
435
736
  const trimmed = result.output.trim().split("\n").slice(0, 10).join("\n");
436
- console.log(chalk2.dim(trimmed.replace(/^/gm, " ")));
737
+ console.log(chalk3.dim(trimmed.replace(/^/gm, " ")));
437
738
  }
438
739
  if (result.status === "skip" && result.skipReason) {
439
- console.log(chalk2.dim(` ${result.skipReason}`));
740
+ console.log(chalk3.dim(` ${result.skipReason}`));
440
741
  }
441
742
  }
442
743
 
443
744
  // src/commands/validate.ts
444
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
445
- import chalk3 from "chalk";
745
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
746
+ import chalk4 from "chalk";
446
747
  import { parseHoldpointYaml as parseHoldpointYaml3, validateConfig } from "@holdpoint/yaml-core";
447
748
  async function validateCommand() {
448
- if (!existsSync4("checks.yaml")) {
449
- 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."));
450
751
  process.exit(1);
451
752
  }
452
- const text = readFileSync4("checks.yaml", "utf8");
753
+ const text = readFileSync5("checks.yaml", "utf8");
453
754
  let config;
454
755
  try {
455
756
  config = parseHoldpointYaml3(text);
456
757
  } catch (err) {
457
- console.error(chalk3.red("Parse error:"), err.message);
758
+ console.error(chalk4.red("Parse error:"), err.message);
458
759
  process.exit(1);
459
760
  }
460
761
  const result = validateConfig(config);
461
762
  if (result.valid) {
462
- console.log(chalk3.green("\u2713 checks.yaml is valid"));
763
+ console.log(chalk4.green("\u2713 checks.yaml is valid"));
463
764
  console.log(
464
- chalk3.dim(
765
+ chalk4.dim(
465
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`
466
767
  )
467
768
  );
468
769
  } else {
469
- console.error(chalk3.red("\u2717 checks.yaml has errors:"));
770
+ console.error(chalk4.red("\u2717 checks.yaml has errors:"));
470
771
  for (const err of result.errors) {
471
- console.error(` ${chalk3.yellow(err.path)}: ${err.message}`);
772
+ console.error(` ${chalk4.yellow(err.path)}: ${err.message}`);
472
773
  }
473
774
  process.exit(1);
474
775
  }
475
776
  }
476
777
 
477
778
  // src/commands/update.ts
478
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
479
- 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";
480
781
  import ora3 from "ora";
481
782
  import { parseHoldpointYaml as parseHoldpointYaml4 } from "@holdpoint/yaml-core";
482
783
  import { buildConfigJson as buildConfigJson2, buildEngine as buildEngine2 } from "@holdpoint/engine-copilot";
483
784
  import { buildEngineJson as buildClaudeEngineJson2 } from "@holdpoint/engine-claude";
484
- 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";
485
790
  import {
486
791
  buildConfigToml as buildCodexConfigToml2,
487
792
  buildHooksJson as buildCodexHooksJson2,
488
- buildCheckScript as buildCodexCheckScript2,
489
- spliceAgentsMd as spliceAgentsMd2
793
+ buildCheckScript as buildCodexCheckScript2
490
794
  } from "@holdpoint/engine-codex";
795
+ var MINIMAL_PREREQUISITES2 = `# Holdpoint prerequisites
796
+
797
+ Holdpoint installed repo-local engine integrations for one or more AI coding agents. Before relying on them locally, review these setup notes:
798
+
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.
801
+ - **OpenAI Codex** \u2014 project-level hooks require trust approval. Run \`codex trust\` in the Codex TUI or review the hook with \`/hooks\`.
802
+ - **General** \u2014 Holdpoint expects Node.js 18+ and a git repository so \`holdpoint init\`, \`holdpoint update\`, and \`holdpoint check\` can run normally.
803
+
804
+ Docs: https://holdpoint.dev/docs
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
+ `;
491
810
  async function updateCommand() {
492
- if (!existsSync5("checks.yaml")) {
493
- 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."));
494
813
  process.exit(1);
495
814
  }
496
815
  const spinner = ora3("Updating Holdpoint engine files\u2026").start();
497
- const config = parseHoldpointYaml4(readFileSync5("checks.yaml", "utf8"));
816
+ const config = parseHoldpointYaml4(readFileSync6("checks.yaml", "utf8"));
498
817
  const detected = detectInstalledAgents();
499
818
  const agents = detected.length > 0 ? detected : ["copilot", "claude", "cursor", "codex"];
500
819
  const generatedDir = ".github/holdpoint/generated";
501
- mkdirSync3(generatedDir, { recursive: true });
502
- writeFileSync3(`${generatedDir}/checks.immutable.json`, buildConfigJson2(config), "utf8");
820
+ mkdirSync4(generatedDir, { recursive: true });
821
+ writeFileSync5(`${generatedDir}/checks.immutable.json`, buildConfigJson2(config), "utf8");
503
822
  if (agents.includes("copilot")) {
504
823
  const extDir = ".github/extensions/holdpoint";
505
- mkdirSync3(extDir, { recursive: true });
506
- writeFileSync3(`${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");
507
827
  }
508
828
  if (agents.includes("claude")) {
509
- mkdirSync3(".claude", { recursive: true });
829
+ mkdirSync4(".claude", { recursive: true });
510
830
  const settingsPath = ".claude/settings.json";
511
831
  let existing = {};
512
- if (existsSync5(settingsPath)) {
832
+ if (existsSync7(settingsPath)) {
513
833
  try {
514
- existing = JSON.parse(readFileSync5(settingsPath, "utf8"));
834
+ existing = JSON.parse(readFileSync6(settingsPath, "utf8"));
515
835
  } catch {
516
836
  }
517
837
  }
518
838
  const hooks = JSON.parse(buildClaudeEngineJson2(config));
519
- writeFileSync3(
839
+ writeFileSync5(
520
840
  settingsPath,
521
- JSON.stringify({ ...existing, hooks: hooks.hooks }, null, 2) + "\n"
841
+ JSON.stringify(mergeClaudeSettings(existing, hooks), null, 2) + "\n"
522
842
  );
843
+ spliceBreadcrumb("CLAUDE.md");
523
844
  }
524
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");
525
862
  const cursorRules = buildCursorEngine2(config);
526
863
  const cursorPath = ".cursorrules";
527
- if (existsSync5(cursorPath)) {
528
- const content = readFileSync5(cursorPath, "utf8");
864
+ if (existsSync7(cursorPath)) {
865
+ const content = readFileSync6(cursorPath, "utf8");
529
866
  const start = content.indexOf("# \u2500\u2500\u2500 Holdpoint Rules");
530
867
  const end = content.indexOf("# \u2500\u2500\u2500 End Holdpoint Rules \u2500\u2500\u2500");
531
868
  if (start !== -1 && end !== -1) {
532
869
  const afterEnd = content.indexOf("\n", end);
533
- const updated = content.slice(0, start) + cursorRules + content.slice(afterEnd === -1 ? end : afterEnd + 1);
534
- writeFileSync3(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);
535
877
  } else {
536
- writeFileSync3(cursorPath, content + "\n" + cursorRules);
878
+ writeFileSync5(cursorPath, `${content.trimEnd()}
879
+
880
+ ${cursorRules}`);
537
881
  }
882
+ } else {
883
+ writeFileSync5(cursorPath, cursorRules);
538
884
  }
885
+ spliceBreadcrumb(".cursor/rules/holdpoint.md");
539
886
  }
540
887
  if (agents.includes("codex")) {
541
- mkdirSync3(".codex", { recursive: true });
542
- writeFileSync3(".codex/hooks.json", buildCodexHooksJson2(config), "utf8");
543
- writeFileSync3(".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");
544
891
  const configTomlPath = ".codex/config.toml";
545
- if (!existsSync5(configTomlPath)) {
546
- writeFileSync3(configTomlPath, buildCodexConfigToml2(), "utf8");
892
+ if (!existsSync7(configTomlPath)) {
893
+ writeFileSync5(configTomlPath, buildCodexConfigToml2(), "utf8");
547
894
  } else {
548
- const existing2 = readFileSync5(configTomlPath, "utf8");
549
- if (!existing2.includes("[features]")) {
550
- writeFileSync3(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");
551
898
  }
552
899
  }
553
- const agentsMdPath = "AGENTS.md";
554
- const existing = existsSync5(agentsMdPath) ? readFileSync5(agentsMdPath, "utf8") : "";
555
- writeFileSync3(agentsMdPath, spliceAgentsMd2(existing, config), "utf8");
900
+ spliceBreadcrumb("AGENTS.md");
901
+ }
902
+ const wroteReference = ensureBundledFile(
903
+ "HOLDPOINT_REFERENCE.md",
904
+ "HOLDPOINT_REFERENCE.md",
905
+ MINIMAL_HOLDPOINT_REFERENCE2
906
+ );
907
+ const wrotePrerequisites = ensureBundledFile(
908
+ "HOLDPOINT_PREREQUISITES.md",
909
+ "HOLDPOINT_PREREQUISITES.md",
910
+ MINIMAL_PREREQUISITES2
911
+ );
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
+ }
918
+ if (wrotePrerequisites) {
919
+ console.log(
920
+ chalk5.cyan(
921
+ "Created HOLDPOINT_PREREQUISITES.md with Copilot experimental-mode and other agent setup notes."
922
+ )
923
+ );
556
924
  }
557
- spinner.succeed(chalk4.green("Engine files updated from current checks.yaml"));
558
925
  }
559
926
 
560
927
  // src/commands/build.ts
561
- import { createServer } from "http";
562
- import { createReadStream, existsSync as existsSync6 } from "fs";
563
- import { join as join3, extname, dirname as dirname2 } from "path";
564
- import { fileURLToPath as fileURLToPath2 } from "url";
565
- import { execSync as execSync3 } from "child_process";
566
- import chalk5 from "chalk";
567
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
568
- var MIME = {
569
- ".html": "text/html; charset=utf-8",
570
- ".js": "text/javascript",
571
- ".mjs": "text/javascript",
572
- ".css": "text/css",
573
- ".svg": "image/svg+xml",
574
- ".png": "image/png",
575
- ".ico": "image/x-icon",
576
- ".woff": "font/woff",
577
- ".woff2": "font/woff2",
578
- ".ttf": "font/ttf",
579
- ".json": "application/json"
580
- };
581
- function serveFile(res, filePath) {
582
- const mime = MIME[extname(filePath)] ?? "application/octet-stream";
583
- res.writeHead(200, { "Content-Type": mime });
584
- createReadStream(filePath).pipe(res);
585
- }
586
- function handleRequest(req, res, uiDir) {
587
- const url = (req.url ?? "/").split("?")[0] ?? "/";
588
- if (url === "/__holdpoint/initial-yaml") {
589
- const checksPath = join3(process.cwd(), "checks.yaml");
590
- if (existsSync6(checksPath)) {
591
- res.writeHead(200, { "Content-Type": "text/yaml; charset=utf-8" });
592
- createReadStream(checksPath).pipe(res);
593
- } else {
594
- res.writeHead(404, { "Content-Type": "text/plain" });
595
- 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 };
596
956
  }
597
- return;
957
+ await sleep(100);
598
958
  }
599
- if (url === "/__holdpoint/initial-reports") {
600
- const reportsPath = join3(process.cwd(), ".holdpoint", "check-reports.json");
601
- if (existsSync6(reportsPath)) {
602
- res.writeHead(200, { "Content-Type": "application/json" });
603
- createReadStream(reportsPath).pipe(res);
604
- } else {
605
- res.writeHead(404, { "Content-Type": "text/plain" });
606
- 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;
607
982
  }
983
+ const parent = dirname4(current);
984
+ if (parent === current) {
985
+ return null;
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) {
608
1003
  return;
609
1004
  }
610
- const candidate = join3(uiDir, url === "/" ? "index.html" : url);
611
- const filePath = existsSync6(candidate) ? candidate : join3(uiDir, "index.html");
612
- serveFile(res, filePath);
1005
+ url.searchParams.set("project", project.hash);
1006
+ url.searchParams.set("name", project.name);
1007
+ url.searchParams.set("root", project.root);
613
1008
  }
1009
+
1010
+ // src/commands/build.ts
614
1011
  async function buildCommand() {
615
- const port = 4321;
616
- const uiDir = join3(__dirname2, "builder-ui");
617
- if (!existsSync6(uiDir)) {
618
- console.error(chalk5.red("\u2717 Builder UI not found.\n"));
619
- console.log(chalk5.dim(" This is unexpected for a published build of @holdpoint/cli."));
620
- console.log(chalk5.dim(" If you installed from source, rebuild with: pnpm turbo build\n"));
621
- process.exit(1);
622
- }
623
- const server = createServer((req, res) => handleRequest(req, res, uiDir));
624
- await new Promise((resolve, reject) => {
625
- server.listen(port, () => {
626
- console.log(
627
- `
628
- ${chalk5.green("\u2713")} Holdpoint builder running at ${chalk5.cyan(`http://localhost:${port}`)}`
629
- );
630
- console.log(chalk5.dim(" Edit checks.yaml, then reload the page to see updates"));
631
- console.log(chalk5.dim(" Press Ctrl+C to stop\n"));
632
- const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
633
- try {
634
- execSync3(`${openCmd} http://localhost:${port}`, { stdio: "ignore" });
635
- } catch {
636
- }
637
- });
638
- server.on("error", reject);
639
- process.on("SIGINT", () => {
640
- console.log(chalk5.dim("\n Stopping builder\u2026"));
641
- server.close(() => resolve());
642
- });
643
- });
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())}`);
644
1024
  }
645
1025
 
646
1026
  // src/commands/evolve.ts
647
- import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
648
- import { execSync as execSync6 } from "child_process";
649
- 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";
650
1030
  import ora4 from "ora";
651
1031
  import { parseHoldpointYaml as parseHoldpointYaml5, generateYaml } from "@holdpoint/yaml-core";
652
1032
 
653
1033
  // src/evolve/scanner.ts
654
- import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync } from "fs";
655
- import { join as join4 } from "path";
656
- import { execSync as execSync4 } from "child_process";
1034
+ import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync } from "fs";
1035
+ import { join as join5 } from "path";
1036
+ import { execSync as execSync5 } from "child_process";
657
1037
  function tryReadJson(path) {
658
1038
  try {
659
- return JSON.parse(readFileSync6(path, "utf8"));
1039
+ return JSON.parse(readFileSync7(path, "utf8"));
660
1040
  } catch {
661
1041
  return null;
662
1042
  }
663
1043
  }
664
1044
  function tryReadText(path) {
665
1045
  try {
666
- return readFileSync6(path, "utf8");
1046
+ return readFileSync7(path, "utf8");
667
1047
  } catch {
668
1048
  return "";
669
1049
  }
670
1050
  }
671
1051
  function scanProject(cwd = process.cwd()) {
672
- const exists = (p) => existsSync7(join4(cwd, p));
1052
+ const exists = (p) => existsSync9(join5(cwd, p));
673
1053
  const packageManager = exists("pnpm-lock.yaml") ? "pnpm" : exists("yarn.lock") ? "yarn" : exists("bun.lockb") ? "bun" : "npm";
674
- const pkg = tryReadJson(join4(cwd, "package.json"));
1054
+ const pkg = tryReadJson(join5(cwd, "package.json"));
675
1055
  const scripts = pkg?.scripts ?? {};
676
1056
  const deps = /* @__PURE__ */ new Set([
677
1057
  ...Object.keys(pkg?.dependencies ?? {}),
678
1058
  ...Object.keys(pkg?.devDependencies ?? {})
679
1059
  ]);
680
- const pyprojectText = tryReadText(join4(cwd, "pyproject.toml"));
681
- const requirementsText = tryReadText(join4(cwd, "requirements.txt"));
682
- const pipfileText = tryReadText(join4(cwd, "Pipfile"));
1060
+ const pyprojectText = tryReadText(join5(cwd, "pyproject.toml"));
1061
+ const requirementsText = tryReadText(join5(cwd, "requirements.txt"));
1062
+ const pipfileText = tryReadText(join5(cwd, "Pipfile"));
683
1063
  const allPyText = pyprojectText + requirementsText + pipfileText;
684
1064
  const hasPytest = exists("pytest.ini") || exists("setup.cfg") || allPyText.includes("pytest") || allPyText.includes("[tool.pytest");
685
1065
  const hasRuff = allPyText.includes("ruff") || deps.has("ruff");
@@ -731,9 +1111,9 @@ function scanProject(cwd = process.cwd()) {
731
1111
  }
732
1112
 
733
1113
  // src/evolve/dead-checker.ts
734
- import { execSync as execSync5 } from "child_process";
735
- import { readdirSync as readdirSync2, existsSync as existsSync8 } from "fs";
736
- import { join as join5 } from "path";
1114
+ import { execSync as execSync6 } from "child_process";
1115
+ import { readdirSync as readdirSync2, existsSync as existsSync10 } from "fs";
1116
+ import { join as join6 } from "path";
737
1117
  var NAMED_SCOPES = /* @__PURE__ */ new Set([
738
1118
  "frontend",
739
1119
  "backend",
@@ -778,7 +1158,7 @@ function walkDir(dir, root, depth, maxDepth) {
778
1158
  const results = [];
779
1159
  for (const entry of entries) {
780
1160
  if (WALK_IGNORED.has(entry) || entry.startsWith(".")) continue;
781
- const full = join5(dir, entry);
1161
+ const full = join6(dir, entry);
782
1162
  const rel = full.slice(root.length + 1);
783
1163
  results.push(rel);
784
1164
  const children = walkDir(full, root, depth + 1, maxDepth);
@@ -788,7 +1168,7 @@ function walkDir(dir, root, depth, maxDepth) {
788
1168
  }
789
1169
  function getRepoFiles(cwd) {
790
1170
  try {
791
- const out = execSync5("git ls-files", {
1171
+ const out = execSync6("git ls-files", {
792
1172
  cwd,
793
1173
  encoding: "utf8",
794
1174
  stdio: ["pipe", "pipe", "ignore"]
@@ -825,7 +1205,7 @@ function detectStaleChecks(config, repoFiles) {
825
1205
  if (matches.length === 0) {
826
1206
  const label = patternAlias ? `Pattern '${patternAlias}' (= '${regexStr}')` : `Regex '${regexStr}'`;
827
1207
  const suggestedConditionPath = extractPathFromRegex(regexStr);
828
- const pathGone = !suggestedConditionPath || !existsSync8(join5(process.cwd(), suggestedConditionPath));
1208
+ const pathGone = !suggestedConditionPath || !existsSync10(join6(process.cwd(), suggestedConditionPath));
829
1209
  if (pathGone) {
830
1210
  stale.push({
831
1211
  check,
@@ -844,6 +1224,9 @@ function pmScript(profile, script, fallback) {
844
1224
  if (profile.packageManager === "npm") return `npm run ${script}`;
845
1225
  return `${profile.packageManager} ${script}`;
846
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.`;
847
1230
  function getTemplates(profile) {
848
1231
  return [
849
1232
  // ── Universal checks (always proposed for any project) ──────────────────
@@ -867,8 +1250,8 @@ function getTemplates(profile) {
867
1250
  },
868
1251
  {
869
1252
  id: "no-todos",
870
- label: "No TODO/FIXME left in changed code",
871
- 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,
872
1255
  trigger: () => true
873
1256
  },
874
1257
  // ── TypeScript / JavaScript ──────────────────────────────────────────────
@@ -1026,20 +1409,20 @@ function withHeader(header, newYaml) {
1026
1409
  return header + "\n\n" + newYaml;
1027
1410
  }
1028
1411
  async function evolveCommand(options) {
1029
- if (!existsSync9("checks.yaml")) {
1030
- 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."));
1031
1414
  process.exit(1);
1032
1415
  }
1033
1416
  const spinner = ora4("Scanning project profile\u2026").start();
1034
1417
  const cwd = process.cwd();
1035
1418
  const profile = scanProject(cwd);
1036
1419
  const repoFiles = getRepoFiles(cwd);
1037
- const yamlContent = readFileSync7("checks.yaml", "utf8");
1420
+ const yamlContent = readFileSync8("checks.yaml", "utf8");
1038
1421
  let config;
1039
1422
  try {
1040
1423
  config = parseHoldpointYaml5(yamlContent);
1041
1424
  } catch (err) {
1042
- spinner.fail(chalk6.red("Invalid checks.yaml:") + " " + err.message);
1425
+ spinner.fail(chalk7.red("Invalid checks.yaml:") + " " + err.message);
1043
1426
  process.exit(1);
1044
1427
  }
1045
1428
  spinner.stop();
@@ -1048,7 +1431,7 @@ async function evolveCommand(options) {
1048
1431
  const allTemplates = getTemplates(profile);
1049
1432
  const proposals = allTemplates.filter((t) => t.trigger(profile) && !existingIds.has(t.id));
1050
1433
  const staleChecks = detectStaleChecks(config, repoFiles);
1051
- console.log(chalk6.bold("\n\u{1F4CB} Project profile:"));
1434
+ console.log(chalk7.bold("\n\u{1F4CB} Project profile:"));
1052
1435
  const traits = [
1053
1436
  ["TypeScript", profile.hasTypeScript, "tsconfig.json"],
1054
1437
  ["ESLint", profile.hasEslint, "eslint.config.*"],
@@ -1074,40 +1457,40 @@ async function evolveCommand(options) {
1074
1457
  ];
1075
1458
  const detected = traits.filter(([, yes]) => yes);
1076
1459
  if (detected.length === 0) {
1077
- console.log(chalk6.dim(" (empty project \u2014 only universal checks apply)"));
1460
+ console.log(chalk7.dim(" (empty project \u2014 only universal checks apply)"));
1078
1461
  } else {
1079
1462
  for (const [name, , hint] of detected) {
1080
- console.log(` ${chalk6.green("\u2713")} ${name.padEnd(18)} ${chalk6.dim(hint)}`);
1463
+ console.log(` ${chalk7.green("\u2713")} ${name.padEnd(18)} ${chalk7.dim(hint)}`);
1081
1464
  }
1082
1465
  }
1083
1466
  if (staleChecks.length > 0) {
1084
- console.log(chalk6.bold(`
1467
+ console.log(chalk7.bold(`
1085
1468
  \u26A0\uFE0F Stale checks (${staleChecks.length}):`));
1086
1469
  for (const { check, reason, suggestedConditionPath } of staleChecks) {
1087
- const fix = suggestedConditionPath ? chalk6.dim(` \u2192 will wrap with conditionId: file_exists: ${suggestedConditionPath}`) : chalk6.dim(" \u2192 no path inferred; review manually");
1088
- 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}`);
1089
1472
  }
1090
1473
  }
1091
1474
  if (proposals.length === 0 && staleChecks.length === 0) {
1092
- 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."));
1093
1476
  return;
1094
1477
  }
1095
1478
  if (proposals.length > 0) {
1096
- console.log(chalk6.bold(`
1479
+ console.log(chalk7.bold(`
1097
1480
  \u{1F4A1} Proposed additions (${proposals.length}):`));
1098
1481
  for (const t of proposals) {
1099
- const scope = t.when ? chalk6.cyan(` when: ${t.when}`) : "";
1100
- const type = t.cmd ? chalk6.dim("cmd") : chalk6.dim("prompt");
1101
- 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" : ""}`);
1102
- 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}`);
1103
1486
  console.log(` ${preview}`);
1104
1487
  }
1105
1488
  }
1106
1489
  if (!options.apply) {
1107
1490
  console.log(
1108
- chalk6.red(`
1491
+ chalk7.red(`
1109
1492
  \u2717 checks.yaml is out of sync with the project profile.`) + `
1110
- Run ${chalk6.bold("npx @holdpoint/cli@alpha evolve --apply")} to apply these changes.`
1493
+ Run ${chalk7.bold("npx @holdpoint/cli@alpha suggest --apply")} to apply these changes.`
1111
1494
  );
1112
1495
  process.exit(1);
1113
1496
  }
@@ -1148,42 +1531,881 @@ async function evolveCommand(options) {
1148
1531
  };
1149
1532
  const header = extractHeader(yamlContent);
1150
1533
  const newYaml = withHeader(header, generateYaml(updatedConfig));
1151
- writeFileSync4("checks.yaml", newYaml, "utf8");
1534
+ writeFileSync6("checks.yaml", newYaml, "utf8");
1152
1535
  applySpinner.text = "Running holdpoint update\u2026";
1153
1536
  try {
1154
- execSync6("npx @holdpoint/cli@alpha update", { stdio: "pipe" });
1537
+ execSync7("npx @holdpoint/cli@alpha update", { stdio: "pipe" });
1155
1538
  } catch {
1156
1539
  applySpinner.warn(
1157
- 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.")
1158
1541
  );
1159
1542
  printAppliedSummary(proposals.length, staleChecks.length);
1160
1543
  return;
1161
1544
  }
1162
- applySpinner.succeed(chalk6.green("checks.yaml updated and engine files regenerated."));
1545
+ applySpinner.succeed(chalk7.green("checks.yaml updated and engine files regenerated."));
1163
1546
  printAppliedSummary(proposals.length, staleChecks.length);
1164
1547
  }
1165
1548
  function printAppliedSummary(added, wrapped) {
1166
1549
  const parts = [];
1167
- 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`));
1168
1551
  if (wrapped > 0)
1169
- parts.push(chalk6.yellow(`${wrapped} stale check${wrapped === 1 ? "" : "s"} wrapped`));
1552
+ parts.push(chalk7.yellow(`${wrapped} stale check${wrapped === 1 ? "" : "s"} wrapped`));
1170
1553
  if (parts.length > 0) console.log(" " + parts.join(" \xB7 "));
1171
1554
  console.log(
1172
- 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'")
1556
+ );
1557
+ }
1558
+
1559
+ // src/commands/live.ts
1560
+ import chalk8 from "chalk";
1561
+ async function liveCommand(options = {}) {
1562
+ const { info, started } = await ensureDaemon();
1563
+ const baseUrl = new URL(`/__holdpoint/live-auth`, `http://127.0.0.1:${info.port}`);
1564
+ baseUrl.searchParams.set("token", info.token);
1565
+ baseUrl.searchParams.set("path", "/live/");
1566
+ const currentProject = options.project ? null : tryResolveCurrentProject();
1567
+ if (options.project) {
1568
+ baseUrl.searchParams.set("project", options.project);
1569
+ } else if (currentProject) {
1570
+ appendProjectAuthParams(baseUrl, currentProject);
1571
+ }
1572
+ openBrowser(baseUrl.toString());
1573
+ console.log(
1574
+ chalk8.green(
1575
+ started ? "\u2713 Started Holdpoint Live and opened the browser" : "\u2713 Opened Holdpoint Live"
1576
+ )
1577
+ );
1578
+ console.log(` url: ${chalk8.cyan(baseUrl.toString())}`);
1579
+ }
1580
+
1581
+ // src/commands/daemon.ts
1582
+ import chalk9 from "chalk";
1583
+ import {
1584
+ DaemonAlreadyRunningError,
1585
+ isProcessAlive,
1586
+ readDaemonLock,
1587
+ readHealthyDaemonLock as readHealthyDaemonLock2,
1588
+ removeDaemonLock,
1589
+ startDaemonProcess
1590
+ } from "@holdpoint/live-daemon";
1591
+
1592
+ // src/version.ts
1593
+ var CLI_VERSION = "0.1.0-alpha.15";
1594
+
1595
+ // src/commands/daemon.ts
1596
+ function formatUptime(lock) {
1597
+ const seconds = Math.max(0, Math.floor((Date.now() - lock.started_at) / 1e3));
1598
+ const minutes = Math.floor(seconds / 60);
1599
+ const remainingSeconds = seconds % 60;
1600
+ return minutes > 0 ? `${minutes}m ${remainingSeconds}s` : `${remainingSeconds}s`;
1601
+ }
1602
+ async function fetchSessionCount(lock) {
1603
+ try {
1604
+ const response = await fetch(`http://127.0.0.1:${lock.port}/v1/sessions`, {
1605
+ headers: {
1606
+ authorization: `Bearer ${lock.token}`
1607
+ }
1608
+ });
1609
+ if (!response.ok) return null;
1610
+ const parsed = await response.json();
1611
+ return Array.isArray(parsed.sessions) ? parsed.sessions.length : null;
1612
+ } catch {
1613
+ return null;
1614
+ }
1615
+ }
1616
+ function sleep2(ms) {
1617
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
1618
+ }
1619
+ async function daemonStartCommand() {
1620
+ const { info, started } = await ensureDaemon();
1621
+ const sessionCount = await fetchSessionCount(info);
1622
+ const headline = started ? "Started Holdpoint Live daemon" : "Reused existing Holdpoint Live daemon";
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))}`);
1627
+ if (sessionCount !== null) {
1628
+ console.log(` sessions: ${chalk9.cyan(String(sessionCount))}`);
1629
+ }
1630
+ }
1631
+ async function daemonStatusCommand() {
1632
+ const lock = await readHealthyDaemonLock2();
1633
+ if (!lock) {
1634
+ console.log(chalk9.yellow("Holdpoint Live daemon is not running."));
1635
+ return;
1636
+ }
1637
+ const sessionCount = await fetchSessionCount(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))}`);
1642
+ if (sessionCount !== null) {
1643
+ console.log(` sessions: ${chalk9.cyan(String(sessionCount))}`);
1644
+ }
1645
+ }
1646
+ async function daemonStopCommand() {
1647
+ const lock = readDaemonLock();
1648
+ if (!lock) {
1649
+ console.log(chalk9.yellow("Holdpoint Live daemon is not running."));
1650
+ return;
1651
+ }
1652
+ if (!isProcessAlive(lock.pid)) {
1653
+ removeDaemonLock(void 0, lock.token);
1654
+ console.log(chalk9.yellow("Removed stale Holdpoint Live lockfile."));
1655
+ return;
1656
+ }
1657
+ process.kill(lock.pid, "SIGTERM");
1658
+ const deadline = Date.now() + 5e3;
1659
+ while (Date.now() < deadline) {
1660
+ if (!isProcessAlive(lock.pid)) {
1661
+ removeDaemonLock(void 0, lock.token);
1662
+ console.log(chalk9.green(`\u2713 Stopped Holdpoint Live daemon (${lock.pid})`));
1663
+ return;
1664
+ }
1665
+ await sleep2(100);
1666
+ }
1667
+ process.kill(lock.pid, "SIGKILL");
1668
+ await sleep2(100);
1669
+ removeDaemonLock(void 0, lock.token);
1670
+ console.log(chalk9.green(`\u2713 Force-stopped Holdpoint Live daemon (${lock.pid})`));
1671
+ }
1672
+ async function daemonServeCommand(options) {
1673
+ try {
1674
+ const daemon2 = await startDaemonProcess({
1675
+ version: CLI_VERSION,
1676
+ ...options.port ? { port: Number(options.port) } : {}
1677
+ });
1678
+ await daemon2.closed;
1679
+ } catch (error) {
1680
+ if (error instanceof DaemonAlreadyRunningError) {
1681
+ process.exit(0);
1682
+ }
1683
+ throw error;
1684
+ }
1685
+ }
1686
+
1687
+ // src/commands/engines.ts
1688
+ import chalk10 from "chalk";
1689
+
1690
+ // src/engines.ts
1691
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
1692
+ import { dirname as dirname5, join as join7, resolve, sep } from "path";
1693
+ import { createRequire } from "module";
1694
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
1695
+ import { parseEventV1 } from "@holdpoint/live-protocol";
1696
+ var require2 = createRequire(import.meta.url);
1697
+ var CLI_SRC_DIR = dirname5(fileURLToPath3(import.meta.url));
1698
+ var MONOREPO_ROOT = resolve(CLI_SRC_DIR, "../../..");
1699
+ var BUILTIN_LIVE_ENGINE_PACKAGES = [
1700
+ "@holdpoint/engine-claude",
1701
+ "@holdpoint/engine-codex",
1702
+ "@holdpoint/engine-cursor"
1703
+ ];
1704
+ var HOLDPOINT_ENGINE_KEYWORD = "holdpoint-engine";
1705
+ function isObject3(value) {
1706
+ return value != null && typeof value === "object" && !Array.isArray(value);
1707
+ }
1708
+ function isExternalLiveEnginePackageName(packageName) {
1709
+ return /^holdpoint-engine-[a-z0-9-]+$/.test(packageName) || /^@[a-z0-9_.-]+\/holdpoint-engine-[a-z0-9-]+$/.test(packageName);
1710
+ }
1711
+ function readJsonFile(path) {
1712
+ if (!existsSync12(path)) {
1713
+ return null;
1714
+ }
1715
+ try {
1716
+ const parsed = JSON.parse(readFileSync9(path, "utf8"));
1717
+ return isObject3(parsed) ? parsed : null;
1718
+ } catch {
1719
+ return null;
1720
+ }
1721
+ }
1722
+ function findNearestPackageRoot(startDir) {
1723
+ let current = resolve(startDir);
1724
+ while (true) {
1725
+ if (existsSync12(join7(current, "package.json"))) {
1726
+ return current;
1727
+ }
1728
+ const parent = dirname5(current);
1729
+ if (parent === current) {
1730
+ return resolve(startDir);
1731
+ }
1732
+ current = parent;
1733
+ }
1734
+ }
1735
+ function findPackageRootFromFile(path) {
1736
+ let current = dirname5(path);
1737
+ while (true) {
1738
+ if (existsSync12(join7(current, "package.json"))) {
1739
+ return current;
1740
+ }
1741
+ const parent = dirname5(current);
1742
+ if (parent === current) {
1743
+ return null;
1744
+ }
1745
+ current = parent;
1746
+ }
1747
+ }
1748
+ function getDependencyEnginePackageNames(projectRoot) {
1749
+ const packageJson = readJsonFile(join7(projectRoot, "package.json"));
1750
+ if (!packageJson) {
1751
+ return [];
1752
+ }
1753
+ const packageNames = /* @__PURE__ */ new Set();
1754
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
1755
+ const deps = packageJson[field];
1756
+ if (!isObject3(deps)) {
1757
+ continue;
1758
+ }
1759
+ for (const packageName of Object.keys(deps)) {
1760
+ if (isExternalLiveEnginePackageName(packageName)) {
1761
+ packageNames.add(packageName);
1762
+ }
1763
+ }
1764
+ }
1765
+ return [...packageNames];
1766
+ }
1767
+ function resolvePackageRoot(packageName, projectRoot) {
1768
+ try {
1769
+ const entryPath = require2.resolve(packageName);
1770
+ return findPackageRootFromFile(entryPath);
1771
+ } catch {
1772
+ }
1773
+ try {
1774
+ const entryPath = require2.resolve(packageName, {
1775
+ paths: [projectRoot, process.cwd()]
1776
+ });
1777
+ return findPackageRootFromFile(entryPath);
1778
+ } catch {
1779
+ }
1780
+ try {
1781
+ const packageJsonPath = require2.resolve(`${packageName}/package.json`, {
1782
+ paths: [projectRoot, process.cwd()]
1783
+ });
1784
+ return dirname5(packageJsonPath);
1785
+ } catch {
1786
+ if (packageName.startsWith("@holdpoint/")) {
1787
+ const scopedName = packageName.split("/")[1];
1788
+ if (scopedName) {
1789
+ const packageDir = resolve(MONOREPO_ROOT, "packages", scopedName);
1790
+ if (existsSync12(join7(packageDir, "package.json"))) {
1791
+ return packageDir;
1792
+ }
1793
+ }
1794
+ }
1795
+ return null;
1796
+ }
1797
+ }
1798
+ function formatImportError(error) {
1799
+ return error instanceof Error && error.message ? error.message : String(error);
1800
+ }
1801
+ function parseManifest(value) {
1802
+ if (!isObject3(value)) {
1803
+ return null;
1804
+ }
1805
+ if (value.manifestVersion !== 1) {
1806
+ return null;
1807
+ }
1808
+ if (typeof value.id !== "string" || !/^[a-z0-9-]+$/.test(value.id)) {
1809
+ return null;
1810
+ }
1811
+ if (typeof value.displayName !== "string" || !value.displayName.trim()) {
1812
+ return null;
1813
+ }
1814
+ return {
1815
+ manifestVersion: 1,
1816
+ id: value.id,
1817
+ displayName: value.displayName
1818
+ };
1819
+ }
1820
+ function parseLiveCapabilities(value) {
1821
+ if (!isObject3(value)) {
1822
+ return null;
1823
+ }
1824
+ const capabilities = {};
1825
+ for (const key of [
1826
+ "can_stream",
1827
+ "can_control",
1828
+ "can_modify_context",
1829
+ "can_register_tools",
1830
+ "control_online"
1831
+ ]) {
1832
+ const entry = value[key];
1833
+ if (entry === void 0) {
1834
+ continue;
1835
+ }
1836
+ if (typeof entry !== "boolean") {
1837
+ return null;
1838
+ }
1839
+ capabilities[key] = entry;
1840
+ }
1841
+ return capabilities;
1842
+ }
1843
+ function parseLiveAdapter(value, manifest) {
1844
+ if (!isObject3(value)) {
1845
+ return null;
1846
+ }
1847
+ if (typeof value.id !== "string" || typeof value.displayName !== "string") {
1848
+ return null;
1849
+ }
1850
+ if (value.id !== manifest.id || value.displayName !== manifest.displayName) {
1851
+ return null;
1852
+ }
1853
+ const capabilities = parseLiveCapabilities(value.capabilities);
1854
+ if (!capabilities) {
1855
+ return null;
1856
+ }
1857
+ const generateBridgeCommand = value.generateBridgeCommand;
1858
+ if (typeof generateBridgeCommand !== "function") {
1859
+ return null;
1860
+ }
1861
+ const translateHookInput = value.translateHookInput;
1862
+ if (typeof translateHookInput !== "function") {
1863
+ return null;
1864
+ }
1865
+ return {
1866
+ id: value.id,
1867
+ displayName: value.displayName,
1868
+ capabilities,
1869
+ generateBridgeCommand: () => {
1870
+ const command = generateBridgeCommand();
1871
+ if (typeof command !== "string") {
1872
+ throw new Error("adapter.generateBridgeCommand() must return a string");
1873
+ }
1874
+ return command;
1875
+ },
1876
+ translateHookInput: (raw, options) => {
1877
+ const event = translateHookInput(raw, options);
1878
+ return event == null ? null : parseEventV1(event);
1879
+ }
1880
+ };
1881
+ }
1882
+ async function importModule(modulePath) {
1883
+ const moduleUrl = pathToFileURL(modulePath).href;
1884
+ return await import(moduleUrl);
1885
+ }
1886
+ function resolvePackageAssetPath(packageRoot, relativePath) {
1887
+ const declaredPath = resolve(packageRoot, relativePath);
1888
+ const sourceFallback = resolve(
1889
+ packageRoot,
1890
+ relativePath.replace(/^\.\/dist\//, "./src/").replace(/\.js$/, ".ts")
1891
+ );
1892
+ if (packageRoot.startsWith(resolve(MONOREPO_ROOT, "packages") + sep) && existsSync12(sourceFallback)) {
1893
+ return sourceFallback;
1894
+ }
1895
+ if (existsSync12(declaredPath)) {
1896
+ return declaredPath;
1897
+ }
1898
+ return sourceFallback;
1899
+ }
1900
+ async function resolveCandidate(packageName, source, projectRoot) {
1901
+ const packageRoot = resolvePackageRoot(packageName, projectRoot);
1902
+ if (!packageRoot) {
1903
+ return {
1904
+ packageName,
1905
+ source,
1906
+ status: "ignored",
1907
+ reason: "package could not be resolved from this project"
1908
+ };
1909
+ }
1910
+ const packageJson = readJsonFile(join7(packageRoot, "package.json"));
1911
+ if (!packageJson) {
1912
+ return {
1913
+ packageName,
1914
+ source,
1915
+ status: "ignored",
1916
+ reason: "package.json could not be read"
1917
+ };
1918
+ }
1919
+ const keywords = Array.isArray(packageJson.keywords) ? packageJson.keywords : [];
1920
+ if (!keywords.includes(HOLDPOINT_ENGINE_KEYWORD)) {
1921
+ return {
1922
+ packageName,
1923
+ source,
1924
+ status: "ignored",
1925
+ reason: `missing \`${HOLDPOINT_ENGINE_KEYWORD}\` keyword`
1926
+ };
1927
+ }
1928
+ const metadata = isObject3(packageJson.holdpoint) ? packageJson.holdpoint : void 0;
1929
+ if (!metadata?.manifest) {
1930
+ return {
1931
+ packageName,
1932
+ source,
1933
+ status: "ignored",
1934
+ reason: "missing `holdpoint.manifest` package.json field"
1935
+ };
1936
+ }
1937
+ if (!metadata.adapter) {
1938
+ return {
1939
+ packageName,
1940
+ source,
1941
+ status: "ignored",
1942
+ reason: "missing `holdpoint.adapter` package.json field"
1943
+ };
1944
+ }
1945
+ const manifestPath = resolvePackageAssetPath(packageRoot, metadata.manifest);
1946
+ const adapterPath = resolvePackageAssetPath(packageRoot, metadata.adapter);
1947
+ if (!existsSync12(manifestPath)) {
1948
+ return {
1949
+ packageName,
1950
+ source,
1951
+ status: "ignored",
1952
+ reason: "manifest file does not exist"
1953
+ };
1954
+ }
1955
+ if (!existsSync12(adapterPath)) {
1956
+ return {
1957
+ packageName,
1958
+ source,
1959
+ status: "ignored",
1960
+ reason: "adapter file does not exist"
1961
+ };
1962
+ }
1963
+ try {
1964
+ const manifestModule = await importModule(manifestPath);
1965
+ const manifest = parseManifest(manifestModule.manifest);
1966
+ if (!manifest) {
1967
+ return {
1968
+ packageName,
1969
+ source,
1970
+ status: "ignored",
1971
+ reason: "manifest export is invalid"
1972
+ };
1973
+ }
1974
+ return {
1975
+ packageName,
1976
+ source,
1977
+ status: "loaded",
1978
+ manifest,
1979
+ packageRoot,
1980
+ adapterPath
1981
+ };
1982
+ } catch (error) {
1983
+ return {
1984
+ packageName,
1985
+ source,
1986
+ status: "ignored",
1987
+ reason: `manifest import failed: ${formatImportError(error)}`
1988
+ };
1989
+ }
1990
+ }
1991
+ async function discoverLiveEnginesDetailed(options) {
1992
+ const projectRoot = findNearestPackageRoot(options?.cwd ?? process.cwd());
1993
+ const dependencyPackages = getDependencyEnginePackageNames(projectRoot);
1994
+ const seenPackages = /* @__PURE__ */ new Set();
1995
+ const results = [];
1996
+ const loadedIds = /* @__PURE__ */ new Set();
1997
+ const candidates = [
1998
+ ...BUILTIN_LIVE_ENGINE_PACKAGES.map((packageName) => ({
1999
+ packageName,
2000
+ source: "built-in"
2001
+ })),
2002
+ ...dependencyPackages.map((packageName) => ({ packageName, source: "dependency" }))
2003
+ ];
2004
+ for (const candidate of candidates) {
2005
+ if (seenPackages.has(candidate.packageName)) {
2006
+ continue;
2007
+ }
2008
+ seenPackages.add(candidate.packageName);
2009
+ const result = await resolveCandidate(candidate.packageName, candidate.source, projectRoot);
2010
+ if (result.status === "loaded" && result.manifest) {
2011
+ if (loadedIds.has(result.manifest.id)) {
2012
+ results.push({
2013
+ packageName: result.packageName,
2014
+ source: result.source,
2015
+ status: "ignored",
2016
+ reason: `engine id \`${result.manifest.id}\` collides with an already loaded adapter`,
2017
+ manifest: result.manifest
2018
+ });
2019
+ continue;
2020
+ }
2021
+ loadedIds.add(result.manifest.id);
2022
+ }
2023
+ results.push(result);
2024
+ }
2025
+ return results;
2026
+ }
2027
+ async function discoverLiveEngines(options) {
2028
+ const results = await discoverLiveEnginesDetailed(options);
2029
+ return results.map(({ packageName, source, status, reason, manifest }) => ({
2030
+ packageName,
2031
+ source,
2032
+ status,
2033
+ ...reason ? { reason } : {},
2034
+ ...manifest ? { manifest } : {}
2035
+ }));
2036
+ }
2037
+ async function loadLiveAdapter(engineId, options) {
2038
+ const results = await discoverLiveEnginesDetailed(options);
2039
+ const match = results.find(
2040
+ (result) => result.status === "loaded" && result.manifest?.id === engineId
2041
+ );
2042
+ if (!match?.adapterPath || !match.manifest) {
2043
+ return null;
2044
+ }
2045
+ try {
2046
+ const adapterModule = await importModule(match.adapterPath);
2047
+ return parseLiveAdapter(adapterModule.adapter, match.manifest);
2048
+ } catch {
2049
+ return null;
2050
+ }
2051
+ }
2052
+
2053
+ // src/commands/engines.ts
2054
+ async function enginesCommand(options = {}) {
2055
+ const engines = await discoverLiveEngines();
2056
+ if (options.json) {
2057
+ console.log(JSON.stringify(engines, null, 2));
2058
+ return;
2059
+ }
2060
+ if (engines.length === 0) {
2061
+ console.log("No Holdpoint Live engines were discovered.");
2062
+ return;
2063
+ }
2064
+ for (const engine of engines) {
2065
+ if (engine.status === "loaded" && engine.manifest) {
2066
+ console.log(
2067
+ `${chalk10.green("loaded")} ${chalk10.cyan(engine.manifest.id)} (${engine.manifest.displayName}) from ${chalk10.yellow(engine.packageName)} [${engine.source}]`
2068
+ );
2069
+ continue;
2070
+ }
2071
+ console.log(
2072
+ `${chalk10.yellow("ignored")} ${chalk10.yellow(engine.packageName)} [${engine.source}] \u2014 ${engine.reason ?? "unknown reason"}`
2073
+ );
2074
+ }
2075
+ }
2076
+
2077
+ // src/commands/event.ts
2078
+ import { readFileSync as readFileSync10 } from "fs";
2079
+ import { parseEventV1 as parseEventV12, parseEventsBatch } from "@holdpoint/live-protocol";
2080
+ import { BridgeClient as BridgeClient2 } from "@holdpoint/sdk";
2081
+ function readStdin() {
2082
+ return readFileSync10(0, "utf8");
2083
+ }
2084
+ async function eventCommand(options) {
2085
+ const stdin = readStdin().trim();
2086
+ if (!stdin) {
2087
+ if (options.fromHook) {
2088
+ process.exit(0);
2089
+ }
2090
+ console.error("No JSON input received on stdin.");
2091
+ process.exit(3);
2092
+ }
2093
+ let raw;
2094
+ try {
2095
+ raw = JSON.parse(stdin);
2096
+ } catch {
2097
+ if (options.fromHook) {
2098
+ process.exit(0);
2099
+ }
2100
+ console.error("Invalid JSON input.");
2101
+ process.exit(3);
2102
+ }
2103
+ const client = new BridgeClient2();
2104
+ try {
2105
+ if (options.fromHook) {
2106
+ if (!options.engine) {
2107
+ process.exit(0);
2108
+ }
2109
+ const adapter = await loadLiveAdapter(options.engine);
2110
+ if (!adapter) {
2111
+ process.exit(0);
2112
+ }
2113
+ const event = adapter.translateHookInput(raw, { cwd: process.cwd() });
2114
+ if (!event) {
2115
+ process.exit(0);
2116
+ }
2117
+ await client.sendEvent(event);
2118
+ process.exit(0);
2119
+ }
2120
+ if (Array.isArray(raw)) {
2121
+ await client.sendEvents(parseEventsBatch(raw));
2122
+ } else {
2123
+ await client.sendEvent(parseEventV12(raw));
2124
+ }
2125
+ } catch (error) {
2126
+ console.error(error.message);
2127
+ process.exit(3);
2128
+ }
2129
+ }
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)
1173
2275
  );
1174
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
+ }
1175
2376
 
1176
2377
  // src/index.ts
1177
2378
  var program = new Command();
1178
- program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version("0.1.0-alpha.2");
1179
- program.command("init").description("Initialise Holdpoint in the current project").option("--stack <stack>", "Stack type: typescript | python | nextjs | fullstack").option(
2379
+ program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version(CLI_VERSION);
2380
+ program.action(() => {
2381
+ program.outputHelp();
2382
+ });
2383
+ program.command("init").description("Initialise Holdpoint in the current project").option(
1180
2384
  "--agent <agent>",
1181
2385
  "Agent to install for: copilot | claude | cursor | codex (default: all four)"
1182
2386
  ).action(initCommand);
1183
2387
  program.command("check").description("Run task checks from checks.yaml").option("--staged", "Only check against git-staged files").action(checkCommand);
1184
2388
  program.command("validate").description("Validate checks.yaml schema and print any errors").action(validateCommand);
1185
2389
  program.command("update").description("Regenerate engine files from current checks.yaml (preserves checks.yaml)").action(updateCommand);
1186
- program.command("builder").description("Open the visual builder UI on localhost:4321").action(buildCommand);
1187
- 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);
2390
+ program.command("builder").description("Open the visual builder UI in the Holdpoint daemon").action(buildCommand);
2391
+ program.command("live").description("Open the Holdpoint Live UI").option("--project <project>", "Open the UI focused on a specific project hash").action(liveCommand);
2392
+ var daemon = program.command("daemon").description("Manage the Holdpoint Live daemon");
2393
+ daemon.command("start").description("Start or connect to the singleton Holdpoint Live daemon").action(daemonStartCommand);
2394
+ daemon.command("status").description("Show Holdpoint Live daemon status").action(daemonStatusCommand);
2395
+ daemon.command("stop").description("Stop the running Holdpoint Live daemon").action(daemonStopCommand);
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);
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);
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);
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
+ });
1188
2410
  program.parse();
1189
2411
  //# sourceMappingURL=index.js.map