@primitive.ai/prim 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
@@ -1,20 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ color,
4
+ colorForArea,
5
+ stripAnsi
6
+ } from "./chunk-BEEGFDGU.js";
7
+ import {
8
+ checkAffectedDecisions,
9
+ daemonOrDirectGet,
10
+ formatDecisionsWarning,
11
+ getGitContext
12
+ } from "./chunk-TPQ3X244.js";
13
+ import {
14
+ HttpError,
3
15
  REFRESH_TOKEN_PATH,
4
16
  TOKEN_EXPIRES_PATH,
5
17
  TOKEN_FILE_PATH,
6
18
  getAuthToken,
7
19
  getClient,
8
- getGitContext,
9
20
  getSiteUrl,
10
21
  getTokenExpiresAt,
11
22
  saveTokenExpiry
12
- } from "./chunk-SHLF6OL2.js";
23
+ } from "./chunk-6SIEWWUL.js";
24
+ import {
25
+ JOURNAL_DIR,
26
+ SESSIONS_DIR,
27
+ bucketStats,
28
+ listBuckets,
29
+ readMovesFromPath
30
+ } from "./chunk-JZGWQDM5.js";
31
+ import {
32
+ daemonIsLive,
33
+ daemonRequest
34
+ } from "./chunk-UTKQTZHL.js";
13
35
 
14
36
  // src/index.ts
15
- import { readFileSync as readFileSync6 } from "fs";
16
- import { dirname as dirname3, resolve as resolve3 } from "path";
17
- import { fileURLToPath as fileURLToPath2 } from "url";
37
+ import { readFileSync as readFileSync10 } from "fs";
38
+ import { dirname as dirname5, resolve as resolve4 } from "path";
39
+ import { fileURLToPath as fileURLToPath3 } from "url";
18
40
  import { Command } from "commander";
19
41
  import updateNotifier from "update-notifier";
20
42
 
@@ -112,10 +134,10 @@ function registerAuthCommands(program2) {
112
134
  process.exit(1);
113
135
  });
114
136
  });
115
- const port = await new Promise((resolve4) => {
137
+ const port = await new Promise((resolve5) => {
116
138
  server.listen(CALLBACK_PORT, LOCALHOST, () => {
117
139
  const addr = server.address();
118
- resolve4(typeof addr === "object" && addr ? addr.port : 0);
140
+ resolve5(typeof addr === "object" && addr ? addr.port : 0);
119
141
  });
120
142
  });
121
143
  const redirectUri = `http://${LOCALHOST}:${port}/callback`;
@@ -256,8 +278,411 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
256
278
  return data.access_token;
257
279
  }
258
280
 
281
+ // src/commands/claude-install.ts
282
+ import {
283
+ closeSync,
284
+ existsSync as existsSync2,
285
+ fsyncSync,
286
+ mkdirSync as mkdirSync2,
287
+ openSync,
288
+ readFileSync as readFileSync2,
289
+ renameSync,
290
+ writeFileSync as writeFileSync2
291
+ } from "fs";
292
+ import { homedir } from "os";
293
+ import { dirname as dirname2, join } from "path";
294
+ var CAPTURE_COMMAND = "prim-hook";
295
+ var GATE_COMMAND = "prim-pre-tool-use";
296
+ var POST_TOOL_USE_COMMAND = "prim-post-tool-use";
297
+ var SESSION_START_COMMAND = "prim-session-start";
298
+ var SESSION_END_COMMAND = "prim-session-end";
299
+ var STATUSLINE_COMMAND = "prim statusline";
300
+ var STATUSLINE_REFRESH_SECONDS = 5;
301
+ var PRIM_COMMANDS = /* @__PURE__ */ new Set([
302
+ CAPTURE_COMMAND,
303
+ GATE_COMMAND,
304
+ POST_TOOL_USE_COMMAND,
305
+ SESSION_START_COMMAND,
306
+ SESSION_END_COMMAND
307
+ ]);
308
+ var JSON_INDENT = 2;
309
+ var USER_SCOPE_PATH = join(homedir(), ".claude", "settings.json");
310
+ var PROJECT_SCOPE_PATH = join(process.cwd(), ".claude", "settings.json");
311
+ var CAPTURE_EVENTS = [
312
+ "SessionStart",
313
+ "UserPromptSubmit",
314
+ "PreToolUse",
315
+ "PostToolUse",
316
+ "Stop",
317
+ "SessionEnd",
318
+ "SubagentStop"
319
+ ];
320
+ var REGISTRATIONS = [
321
+ ...CAPTURE_EVENTS.map((event) => ({ event, matcher: "*", command: CAPTURE_COMMAND })),
322
+ { event: "PreToolUse", matcher: "Edit|Write|MultiEdit", command: GATE_COMMAND },
323
+ { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", command: POST_TOOL_USE_COMMAND },
324
+ { event: "SessionStart", matcher: "*", command: SESSION_START_COMMAND },
325
+ { event: "SessionEnd", matcher: "*", command: SESSION_END_COMMAND }
326
+ ];
327
+ function settingsPathFor(scope) {
328
+ return scope === "user" ? USER_SCOPE_PATH : PROJECT_SCOPE_PATH;
329
+ }
330
+ function readSettings(path) {
331
+ if (!existsSync2(path)) {
332
+ return {};
333
+ }
334
+ const raw = readFileSync2(path, "utf-8");
335
+ try {
336
+ return JSON.parse(raw);
337
+ } catch (err) {
338
+ const detail = err instanceof Error ? err.message : String(err);
339
+ throw new Error(`${path} is not valid JSON: ${detail}`);
340
+ }
341
+ }
342
+ function entryHasCommand(entry, command) {
343
+ return entry.hooks?.some((h) => h.command === command) ?? false;
344
+ }
345
+ function canonicalEntry(reg) {
346
+ return { matcher: reg.matcher, hooks: [{ type: "command", command: reg.command }] };
347
+ }
348
+ function stripCommand(list, command) {
349
+ const out = [];
350
+ for (const e of list) {
351
+ const hooks = (e.hooks ?? []).filter((h) => h.command !== command);
352
+ if (hooks.length > 0) {
353
+ out.push({ ...e, hooks });
354
+ }
355
+ }
356
+ return out;
357
+ }
358
+ function ensureRegistration(list, reg, force) {
359
+ const hasCanonical = list.some(
360
+ (e) => e.matcher === reg.matcher && e.hooks?.length === 1 && e.hooks[0].command === reg.command
361
+ );
362
+ if (hasCanonical && !force) {
363
+ return list;
364
+ }
365
+ return [...stripCommand(list, reg.command), canonicalEntry(reg)];
366
+ }
367
+ function isPrimStatusLine(settings) {
368
+ const s = settings.statusLine;
369
+ return s?.type === "command" && s?.command === STATUSLINE_COMMAND;
370
+ }
371
+ function applyStatusLine(settings) {
372
+ if (settings.statusLine && !isPrimStatusLine(settings)) {
373
+ return settings;
374
+ }
375
+ return {
376
+ ...settings,
377
+ statusLine: {
378
+ type: "command",
379
+ command: STATUSLINE_COMMAND,
380
+ refreshInterval: STATUSLINE_REFRESH_SECONDS
381
+ }
382
+ };
383
+ }
384
+ function applyInstall(settings, options = {}) {
385
+ const hooks = { ...settings.hooks ?? {} };
386
+ for (const reg of REGISTRATIONS) {
387
+ hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
388
+ }
389
+ return applyStatusLine({ ...settings, hooks });
390
+ }
391
+ function applyUninstall(settings) {
392
+ const source = settings.hooks ?? {};
393
+ const hooks = {};
394
+ for (const event of Object.keys(source)) {
395
+ let list = source[event] ?? [];
396
+ for (const command of PRIM_COMMANDS) {
397
+ list = stripCommand(list, command);
398
+ }
399
+ if (list.length > 0) {
400
+ hooks[event] = list;
401
+ }
402
+ }
403
+ const next = { ...settings, hooks };
404
+ if (isPrimStatusLine(next)) {
405
+ next.statusLine = void 0;
406
+ }
407
+ return next;
408
+ }
409
+ function captureInstalled(settings) {
410
+ return CAPTURE_EVENTS.some(
411
+ (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_COMMAND))
412
+ );
413
+ }
414
+ function statuslineInstalled(settings) {
415
+ return isPrimStatusLine(settings);
416
+ }
417
+ function isGateInstalled(settings) {
418
+ return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_COMMAND));
419
+ }
420
+ function atomicWrite(path, content) {
421
+ const dir = dirname2(path);
422
+ if (!existsSync2(dir)) {
423
+ mkdirSync2(dir, { recursive: true });
424
+ }
425
+ const tmp = `${path}.tmp.${String(Date.now())}`;
426
+ writeFileSync2(tmp, `${JSON.stringify(content, null, JSON_INDENT)}
427
+ `, "utf-8");
428
+ const fd = openSync(tmp, "r+");
429
+ try {
430
+ fsyncSync(fd);
431
+ } finally {
432
+ closeSync(fd);
433
+ }
434
+ renameSync(tmp, path);
435
+ }
436
+ function performInstall(scope, force) {
437
+ const path = settingsPathFor(scope);
438
+ const before = readSettings(path);
439
+ const after = applyInstall(before, { force });
440
+ const changed = JSON.stringify(before) !== JSON.stringify(after);
441
+ if (changed) {
442
+ atomicWrite(path, after);
443
+ }
444
+ return {
445
+ scope,
446
+ path,
447
+ gate: isGateInstalled(after),
448
+ capture: captureInstalled(after),
449
+ statusline: statuslineInstalled(after),
450
+ changed
451
+ };
452
+ }
453
+ function performUninstall(scope) {
454
+ const path = settingsPathFor(scope);
455
+ const before = readSettings(path);
456
+ const after = applyUninstall(before);
457
+ const changed = JSON.stringify(before) !== JSON.stringify(after);
458
+ if (changed) {
459
+ atomicWrite(path, after);
460
+ }
461
+ return {
462
+ scope,
463
+ path,
464
+ gate: isGateInstalled(after),
465
+ capture: captureInstalled(after),
466
+ statusline: statuslineInstalled(after),
467
+ changed
468
+ };
469
+ }
470
+ function performStatus() {
471
+ const statusFor = (path) => {
472
+ const settings = readSettings(path);
473
+ return {
474
+ path,
475
+ gate: isGateInstalled(settings),
476
+ capture: captureInstalled(settings),
477
+ statusline: statuslineInstalled(settings)
478
+ };
479
+ };
480
+ return { user: statusFor(USER_SCOPE_PATH), project: statusFor(PROJECT_SCOPE_PATH) };
481
+ }
482
+ function resolveScope(input) {
483
+ if (input === void 0 || input === "user") {
484
+ return "user";
485
+ }
486
+ if (input === "project") {
487
+ return "project";
488
+ }
489
+ console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
490
+ process.exit(1);
491
+ }
492
+ function registerClaudeCommands(program2) {
493
+ const claude = program2.command("claude").description("Manage the prim Claude Code integration (capture, gate, ingest, presence)");
494
+ claude.command("install").description("Register the prim hooks + statusline in Claude Code's settings.json").option(
495
+ "--scope <scope>",
496
+ "user (default, ~/.claude/settings.json) or project (./.claude/settings.json)"
497
+ ).option("--force", "Replace any drifted prim hook entries").action((opts) => {
498
+ const scope = resolveScope(opts.scope);
499
+ const result = performInstall(scope, opts.force ?? false);
500
+ if (result.changed) {
501
+ console.error(
502
+ `[prim] Claude Code integration installed (${scope} scope) at ${result.path}`
503
+ );
504
+ } else {
505
+ console.error(
506
+ `[prim] Claude Code integration already present at ${result.path} (no changes)`
507
+ );
508
+ }
509
+ console.log(JSON.stringify(result, null, JSON_INDENT));
510
+ });
511
+ claude.command("uninstall").description("Remove all prim hooks + the prim statusline from settings.json").option(
512
+ "--scope <scope>",
513
+ "user (default, ~/.claude/settings.json) or project (./.claude/settings.json)"
514
+ ).action((opts) => {
515
+ const scope = resolveScope(opts.scope);
516
+ const result = performUninstall(scope);
517
+ if (result.changed) {
518
+ console.error(`[prim] prim hooks removed from ${result.path}`);
519
+ } else {
520
+ console.error(`[prim] no prim hooks to remove at ${result.path} (nothing changed)`);
521
+ }
522
+ console.log(JSON.stringify(result, null, JSON_INDENT));
523
+ });
524
+ claude.command("status").description(
525
+ "Report whether each prim surface (gate, capture, statusline) is installed per scope"
526
+ ).action(() => {
527
+ const result = performStatus();
528
+ const mark = (b) => b ? "\u2713" : "\u2717";
529
+ const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} \xB7 statusline ${mark(s.statusline)} (${s.path})`;
530
+ console.error(`${line("user", result.user)}
531
+ ${line("project", result.project)}`);
532
+ console.log(JSON.stringify(result, null, JSON_INDENT));
533
+ });
534
+ }
535
+
536
+ // src/commands/codex-install.ts
537
+ import { homedir as homedir2 } from "os";
538
+ import { join as join2 } from "path";
539
+ var CAPTURE_COMMAND2 = "prim-hook --agent codex";
540
+ var GATE_COMMAND2 = "prim-pre-tool-use --agent codex";
541
+ var POST_TOOL_USE_COMMAND2 = "prim-post-tool-use --agent codex";
542
+ var SESSION_START_COMMAND2 = "prim-session-start --agent codex";
543
+ var JSON_INDENT2 = 2;
544
+ var CODEX_CAPTURE_EVENTS = [
545
+ "SessionStart",
546
+ "UserPromptSubmit",
547
+ "PreToolUse",
548
+ "PostToolUse",
549
+ "Stop",
550
+ "SubagentStop"
551
+ ];
552
+ var PRIM_COMMANDS2 = /* @__PURE__ */ new Set([
553
+ CAPTURE_COMMAND2,
554
+ GATE_COMMAND2,
555
+ POST_TOOL_USE_COMMAND2,
556
+ SESSION_START_COMMAND2
557
+ ]);
558
+ var CODEX_REGISTRATIONS = [
559
+ ...CODEX_CAPTURE_EVENTS.map((event) => ({ event, matcher: "*", command: CAPTURE_COMMAND2 })),
560
+ { event: "PreToolUse", matcher: "apply_patch", command: GATE_COMMAND2 },
561
+ { event: "PostToolUse", matcher: "apply_patch", command: POST_TOOL_USE_COMMAND2 },
562
+ { event: "SessionStart", matcher: "*", command: SESSION_START_COMMAND2 }
563
+ ];
564
+ var USER_SCOPE_PATH2 = join2(homedir2(), ".codex", "hooks.json");
565
+ var PROJECT_SCOPE_PATH2 = join2(process.cwd(), ".codex", "hooks.json");
566
+ function settingsPathFor2(scope) {
567
+ return scope === "user" ? USER_SCOPE_PATH2 : PROJECT_SCOPE_PATH2;
568
+ }
569
+ function applyInstall2(settings, options = {}) {
570
+ const hooks = { ...settings.hooks ?? {} };
571
+ for (const reg of CODEX_REGISTRATIONS) {
572
+ hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
573
+ }
574
+ return { ...settings, hooks };
575
+ }
576
+ function applyUninstall2(settings) {
577
+ const source = settings.hooks ?? {};
578
+ const hooks = {};
579
+ for (const event of Object.keys(source)) {
580
+ let list = source[event] ?? [];
581
+ for (const command of PRIM_COMMANDS2) {
582
+ list = stripCommand(list, command);
583
+ }
584
+ if (list.length > 0) {
585
+ hooks[event] = list;
586
+ }
587
+ }
588
+ return { ...settings, hooks };
589
+ }
590
+ function captureInstalled2(settings) {
591
+ return CODEX_CAPTURE_EVENTS.some(
592
+ (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_COMMAND2))
593
+ );
594
+ }
595
+ function isGateInstalled2(settings) {
596
+ return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_COMMAND2));
597
+ }
598
+ function resultFor(scope, path, after, changed) {
599
+ return {
600
+ scope,
601
+ path,
602
+ gate: isGateInstalled2(after),
603
+ capture: captureInstalled2(after),
604
+ changed
605
+ };
606
+ }
607
+ function performInstall2(scope, force) {
608
+ const path = settingsPathFor2(scope);
609
+ const before = readSettings(path);
610
+ const after = applyInstall2(before, { force });
611
+ const changed = JSON.stringify(before) !== JSON.stringify(after);
612
+ if (changed) {
613
+ atomicWrite(path, after);
614
+ }
615
+ return resultFor(scope, path, after, changed);
616
+ }
617
+ function performUninstall2(scope) {
618
+ const path = settingsPathFor2(scope);
619
+ const before = readSettings(path);
620
+ const after = applyUninstall2(before);
621
+ const changed = JSON.stringify(before) !== JSON.stringify(after);
622
+ if (changed) {
623
+ atomicWrite(path, after);
624
+ }
625
+ return resultFor(scope, path, after, changed);
626
+ }
627
+ function performStatus2() {
628
+ const statusFor = (path) => {
629
+ const settings = readSettings(path);
630
+ return { path, gate: isGateInstalled2(settings), capture: captureInstalled2(settings) };
631
+ };
632
+ return { user: statusFor(USER_SCOPE_PATH2), project: statusFor(PROJECT_SCOPE_PATH2) };
633
+ }
634
+ function resolveScope2(input) {
635
+ if (input === void 0 || input === "user") {
636
+ return "user";
637
+ }
638
+ if (input === "project") {
639
+ return "project";
640
+ }
641
+ console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
642
+ process.exit(1);
643
+ }
644
+ var TRUST_NOTICE = "[prim] Codex requires hook trust: run `/hooks` in Codex to review and trust these hooks (or start Codex with --dangerously-bypass-hook-trust). Until trusted, the hooks will not fire.";
645
+ function registerCodexCommands(program2) {
646
+ const codex = program2.command("codex").description("Manage the prim Codex integration (capture, gate, ingest, presence)");
647
+ codex.command("install").description("Register the prim hooks in Codex's ~/.codex/hooks.json").option(
648
+ "--scope <scope>",
649
+ "user (default, ~/.codex/hooks.json) or project (./.codex/hooks.json)"
650
+ ).option("--force", "Replace any drifted prim hook entries").action((opts) => {
651
+ const scope = resolveScope2(opts.scope);
652
+ const result = performInstall2(scope, opts.force ?? false);
653
+ if (result.changed) {
654
+ console.error(`[prim] Codex integration installed (${scope} scope) at ${result.path}`);
655
+ } else {
656
+ console.error(`[prim] Codex integration already present at ${result.path} (no changes)`);
657
+ }
658
+ console.error(TRUST_NOTICE);
659
+ console.log(JSON.stringify(result, null, JSON_INDENT2));
660
+ });
661
+ codex.command("uninstall").description("Remove all prim hooks from ~/.codex/hooks.json").option(
662
+ "--scope <scope>",
663
+ "user (default, ~/.codex/hooks.json) or project (./.codex/hooks.json)"
664
+ ).action((opts) => {
665
+ const scope = resolveScope2(opts.scope);
666
+ const result = performUninstall2(scope);
667
+ if (result.changed) {
668
+ console.error(`[prim] prim hooks removed from ${result.path}`);
669
+ } else {
670
+ console.error(`[prim] no prim hooks to remove at ${result.path} (nothing changed)`);
671
+ }
672
+ console.log(JSON.stringify(result, null, JSON_INDENT2));
673
+ });
674
+ codex.command("status").description("Report whether each prim surface (gate, capture) is installed per scope").action(() => {
675
+ const result = performStatus2();
676
+ const mark = (b) => b ? "\u2713" : "\u2717";
677
+ const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} (${s.path})`;
678
+ console.error(`${line("user", result.user)}
679
+ ${line("project", result.project)}`);
680
+ console.log(JSON.stringify(result, null, JSON_INDENT2));
681
+ });
682
+ }
683
+
259
684
  // src/commands/context.ts
260
- import { readFileSync as readFileSync2 } from "fs";
685
+ import { readFileSync as readFileSync3 } from "fs";
261
686
  function registerContextCommands(program2) {
262
687
  const context = program2.command("context").description("Manage contexts");
263
688
  context.command("list").description("List contexts").option("-s, --scope <scope>", "Filter by scope: project, global, external").option("-t, --project-id <projectId>", "List contexts linked to a specific project").option("--json", "Output as JSON").action(async (opts) => {
@@ -286,7 +711,7 @@ function registerContextCommands(program2) {
286
711
  const client = getClient();
287
712
  let text = opts.text;
288
713
  if (opts.file) {
289
- text = readFileSync2(opts.file, "utf-8");
714
+ text = readFileSync3(opts.file, "utf-8");
290
715
  }
291
716
  const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
292
717
  const result = await client.post("/api/cli/contexts", {
@@ -309,7 +734,7 @@ function registerContextCommands(program2) {
309
734
  const client = getClient();
310
735
  let text = opts.text;
311
736
  if (opts.file) {
312
- text = readFileSync2(opts.file, "utf-8");
737
+ text = readFileSync3(opts.file, "utf-8");
313
738
  }
314
739
  await client.patch(`/api/cli/contexts/${contextId}`, {
315
740
  name: opts.name,
@@ -373,9 +798,783 @@ function printContextList(contexts) {
373
798
  ${contexts.length} context(s)`);
374
799
  }
375
800
 
801
+ // src/commands/daemon.ts
802
+ import { spawn } from "child_process";
803
+ import { existsSync as existsSync3, readFileSync as readFileSync4, unlinkSync } from "fs";
804
+ import { homedir as homedir3 } from "os";
805
+ import { join as join3 } from "path";
806
+ var DAEMON_BIN = "prim-daemon-server";
807
+ var PID_PATH = join3(homedir3(), ".config", "prim", "daemon.pid");
808
+ var SOCK_PATH = join3(homedir3(), ".config", "prim", "sock");
809
+ var STOP_TIMEOUT_MS = 5e3;
810
+ var STOP_POLL_MS = 100;
811
+ var STATUS_PROBE_TIMEOUT_MS = 500;
812
+ var POST_START_WAIT_MS = 400;
813
+ var EXIT_NOT_RUNNING = 2;
814
+ function readPidfile() {
815
+ if (!existsSync3(PID_PATH)) {
816
+ return null;
817
+ }
818
+ const raw = readFileSync4(PID_PATH, "utf-8").trim();
819
+ const pid = Number(raw);
820
+ if (!Number.isInteger(pid) || pid <= 0) {
821
+ return null;
822
+ }
823
+ return { pid, alive: processIsAlive(pid) };
824
+ }
825
+ function processIsAlive(pid) {
826
+ try {
827
+ process.kill(pid, 0);
828
+ return true;
829
+ } catch {
830
+ return false;
831
+ }
832
+ }
833
+ function clearStaleArtifacts() {
834
+ try {
835
+ unlinkSync(PID_PATH);
836
+ } catch {
837
+ }
838
+ try {
839
+ unlinkSync(SOCK_PATH);
840
+ } catch {
841
+ }
842
+ }
843
+ function sleep(ms) {
844
+ return new Promise((resolve5) => {
845
+ const timer = setTimeout(resolve5, ms);
846
+ timer.unref();
847
+ });
848
+ }
849
+ async function daemonStart(opts) {
850
+ const existing = readPidfile();
851
+ if (existing?.alive) {
852
+ process.stderr.write(`[prim] daemon already running (pid=${existing.pid})
853
+ `);
854
+ console.log(JSON.stringify({ started: false, pid: existing.pid }, null, 2));
855
+ return;
856
+ }
857
+ if (existing && !existing.alive) {
858
+ clearStaleArtifacts();
859
+ }
860
+ if (opts.foreground) {
861
+ const child2 = spawn(DAEMON_BIN, [], { stdio: "inherit" });
862
+ child2.on("exit", (code) => {
863
+ process.exit(code ?? 0);
864
+ });
865
+ return;
866
+ }
867
+ const child = spawn(DAEMON_BIN, [], {
868
+ detached: true,
869
+ stdio: ["ignore", "ignore", "ignore"]
870
+ });
871
+ child.unref();
872
+ await sleep(POST_START_WAIT_MS);
873
+ const after = readPidfile();
874
+ if (after?.alive) {
875
+ process.stderr.write(`[prim] daemon started (pid=${after.pid}, socket=${SOCK_PATH})
876
+ `);
877
+ console.log(JSON.stringify({ started: true, pid: after.pid }, null, 2));
878
+ return;
879
+ }
880
+ process.stderr.write(
881
+ "[prim] daemon start: bin spawned but no pidfile observed (check that `prim-daemon-server` is on PATH)\n"
882
+ );
883
+ console.log(JSON.stringify({ started: false }, null, 2));
884
+ }
885
+ async function daemonStop() {
886
+ const existing = readPidfile();
887
+ if (!existing) {
888
+ process.stderr.write("[prim] daemon not running (no pidfile)\n");
889
+ console.log(JSON.stringify({ stopped: false, wasRunning: false }, null, 2));
890
+ return;
891
+ }
892
+ if (!existing.alive) {
893
+ clearStaleArtifacts();
894
+ process.stderr.write("[prim] daemon not running (cleared stale pidfile)\n");
895
+ console.log(JSON.stringify({ stopped: false, wasRunning: false }, null, 2));
896
+ return;
897
+ }
898
+ try {
899
+ process.kill(existing.pid, "SIGTERM");
900
+ } catch (err) {
901
+ process.stderr.write(
902
+ `[prim] could not signal pid=${existing.pid}: ${err instanceof Error ? err.message : String(err)}
903
+ `
904
+ );
905
+ console.log(JSON.stringify({ stopped: false, pid: existing.pid }, null, 2));
906
+ return;
907
+ }
908
+ const deadline = Date.now() + STOP_TIMEOUT_MS;
909
+ while (Date.now() < deadline) {
910
+ if (!processIsAlive(existing.pid)) {
911
+ clearStaleArtifacts();
912
+ process.stderr.write(`[prim] daemon stopped (pid=${existing.pid})
913
+ `);
914
+ console.log(JSON.stringify({ stopped: true, pid: existing.pid }, null, 2));
915
+ return;
916
+ }
917
+ await sleep(STOP_POLL_MS);
918
+ }
919
+ process.stderr.write(
920
+ `[prim] daemon did not exit within ${STOP_TIMEOUT_MS}ms (pid=${existing.pid} still alive)
921
+ `
922
+ );
923
+ console.log(JSON.stringify({ stopped: false, pid: existing.pid }, null, 2));
924
+ }
925
+ async function daemonStatus() {
926
+ const pid = readPidfile();
927
+ if (!pid?.alive) {
928
+ process.stderr.write("[prim] \u2717 daemon down\n");
929
+ console.log(JSON.stringify({ running: false }, null, 2));
930
+ if (!process.exitCode) {
931
+ process.exitCode = EXIT_NOT_RUNNING;
932
+ }
933
+ return;
934
+ }
935
+ const live = await daemonIsLive(STATUS_PROBE_TIMEOUT_MS);
936
+ if (!live) {
937
+ process.stderr.write(`[prim] \u2717 daemon pid=${pid.pid} alive but socket not responding
938
+ `);
939
+ console.log(JSON.stringify({ running: true, responding: false, pid: pid.pid }, null, 2));
940
+ if (!process.exitCode) {
941
+ process.exitCode = EXIT_NOT_RUNNING;
942
+ }
943
+ return;
944
+ }
945
+ const snapshot = await daemonRequest(
946
+ "status_snapshot",
947
+ {},
948
+ { timeoutMs: STATUS_PROBE_TIMEOUT_MS }
949
+ );
950
+ if (!snapshot) {
951
+ process.stderr.write("[prim] \u2713 daemon live (no snapshot)\n");
952
+ console.log(JSON.stringify({ running: true, responding: true }, null, 2));
953
+ return;
954
+ }
955
+ process.stderr.write(
956
+ `[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}
957
+ `
958
+ );
959
+ console.log(JSON.stringify({ running: true, responding: true, ...snapshot }, null, 2));
960
+ }
961
+ async function daemonRestart(opts) {
962
+ await daemonStop();
963
+ await daemonStart(opts);
964
+ }
965
+ function registerDaemonCommands(program2) {
966
+ const daemon = program2.command("daemon").description("Manage the prim companion daemon (latency unlock + presence + broadcast)");
967
+ daemon.command("start").description("Spawn the prim-daemon-server in the background").option("--foreground", "Run in the foreground (inherit stdio); use under launchd / systemd").action(async (opts) => {
968
+ await daemonStart(opts);
969
+ });
970
+ daemon.command("stop").description("Send SIGTERM to the running daemon and clean up the socket").action(async () => {
971
+ await daemonStop();
972
+ });
973
+ daemon.command("status").description("Report daemon liveness + a snapshot if responding").action(async () => {
974
+ await daemonStatus();
975
+ });
976
+ daemon.command("restart").description("Stop, then start (preserves no state today)").option("--foreground", "Restart in the foreground").action(async (opts) => {
977
+ await daemonRestart(opts);
978
+ });
979
+ }
980
+
981
+ // src/decisions/cascade-renderer.ts
982
+ var DEPENDENTS_INLINE_LIMIT = 5;
983
+ var KNOWLEDGE_INLINE_LIMIT = 4;
984
+ var ISO_DATE_LENGTH = 10;
985
+ var INTENT_TRUNC = 60;
986
+ var DEFAULT_WIDTH = 80;
987
+ var SOFT_WRAP_INDENT = " ";
988
+ function terminalWidth() {
989
+ return process.stdout.columns ?? DEFAULT_WIDTH;
990
+ }
991
+ function softWrap(line, opts) {
992
+ const width = opts?.width ?? terminalWidth();
993
+ const indent = opts?.indent ?? "";
994
+ if (stripAnsi(line).length <= width) {
995
+ return [line];
996
+ }
997
+ const words = line.split(" ");
998
+ const out = [];
999
+ let current = "";
1000
+ for (const w of words) {
1001
+ if (current === "") {
1002
+ current = w;
1003
+ continue;
1004
+ }
1005
+ const tentative = `${current} ${w}`;
1006
+ if (stripAnsi(tentative).length > width) {
1007
+ out.push(current);
1008
+ current = `${indent}${w}`;
1009
+ continue;
1010
+ }
1011
+ current = tentative;
1012
+ }
1013
+ if (current.length > 0) {
1014
+ out.push(current);
1015
+ }
1016
+ return out;
1017
+ }
1018
+ function formatDate(ms) {
1019
+ return new Date(ms).toISOString().slice(0, ISO_DATE_LENGTH);
1020
+ }
1021
+ function truncate(s, max) {
1022
+ if (s.length <= max) {
1023
+ return s;
1024
+ }
1025
+ return `${s.slice(0, max - 1)}\u2026`;
1026
+ }
1027
+ function bracketed(label) {
1028
+ return `[${label}]`;
1029
+ }
1030
+ function knowledgeRow(files, contexts, triggerFile, triggerContextName) {
1031
+ const tokens = [];
1032
+ for (const ctx of contexts.slice(0, KNOWLEDGE_INLINE_LIMIT)) {
1033
+ const star = ctx.name === triggerContextName ? " *" : "";
1034
+ tokens.push(bracketed(`${ctx.name}${star}`));
1035
+ }
1036
+ for (const f of files.slice(0, Math.max(0, KNOWLEDGE_INLINE_LIMIT - contexts.length))) {
1037
+ const star = f === triggerFile ? " *" : "";
1038
+ tokens.push(bracketed(`${f}${star}`));
1039
+ }
1040
+ const overflow = files.length + contexts.length - tokens.length > 0 ? ` (+${String(files.length + contexts.length - tokens.length)} more)` : "";
1041
+ if (tokens.length === 0) {
1042
+ return [" (no upstream knowledge refs)"];
1043
+ }
1044
+ return [` ${tokens.join(" ")}${overflow}`];
1045
+ }
1046
+ function areaChip(area) {
1047
+ if (!area) {
1048
+ return color("[--]", "gray");
1049
+ }
1050
+ return color(`[${area}]`, colorForArea(area));
1051
+ }
1052
+ function countCrossAreaDependents(parentArea, dependents) {
1053
+ if (!parentArea) {
1054
+ const areaCounts = /* @__PURE__ */ new Map();
1055
+ for (const d of dependents) {
1056
+ if (d.area) {
1057
+ areaCounts.set(d.area, (areaCounts.get(d.area) ?? 0) + 1);
1058
+ }
1059
+ }
1060
+ if (areaCounts.size <= 1) {
1061
+ return 0;
1062
+ }
1063
+ let dominantCount = 0;
1064
+ for (const c of areaCounts.values()) {
1065
+ if (c > dominantCount) {
1066
+ dominantCount = c;
1067
+ }
1068
+ }
1069
+ return dependents.filter((d) => d.area).length - dominantCount;
1070
+ }
1071
+ let count = 0;
1072
+ for (const d of dependents) {
1073
+ if (d.area && d.area !== parentArea) {
1074
+ count++;
1075
+ }
1076
+ }
1077
+ return count;
1078
+ }
1079
+ function dependentsBox(dependents) {
1080
+ if (dependents.length === 0) {
1081
+ return [" (no downstream dependents)"];
1082
+ }
1083
+ const inlineCount = Math.min(dependents.length, DEPENDENTS_INLINE_LIMIT);
1084
+ const header = `${String(dependents.length)} affected:`;
1085
+ const lines = [` ${header}`];
1086
+ for (const d of dependents.slice(0, inlineCount)) {
1087
+ lines.push(` \u2022 ${areaChip(d.area)} ${truncate(d.intent, INTENT_TRUNC)}`);
1088
+ }
1089
+ if (dependents.length > inlineCount) {
1090
+ lines.push(` + ${String(dependents.length - inlineCount)} more`);
1091
+ }
1092
+ return lines;
1093
+ }
1094
+ function triggerHeadline(t) {
1095
+ const at = formatDate(t.flaggedAt);
1096
+ if (t.type === "file_edit" && t.file) {
1097
+ return `trigger: file '${t.file}' was edited; cascade fired at ${at}.`;
1098
+ }
1099
+ if (t.type === "context_edit" && t.contextName) {
1100
+ return `trigger: context '${t.contextName}' was edited; cascade fired at ${at}.`;
1101
+ }
1102
+ if (t.type === "supersession") {
1103
+ return `trigger: an upstream decision was superseded; cascade fired at ${at}.`;
1104
+ }
1105
+ if (t.type === "invalidation") {
1106
+ return `trigger: an upstream decision was invalidated; cascade fired at ${at}.`;
1107
+ }
1108
+ if (t.type === "confirmation_request") {
1109
+ return `trigger: asking-policy confirmation request opened at ${at}.`;
1110
+ }
1111
+ return `trigger: ${t.type} at ${at}.`;
1112
+ }
1113
+ function triggerLine(result) {
1114
+ const t = result.trigger;
1115
+ if (!t) {
1116
+ return [];
1117
+ }
1118
+ const lines = [triggerHeadline(t)];
1119
+ if (t.authorName) {
1120
+ lines.push(` by ${t.authorName}`);
1121
+ }
1122
+ if (t.reason) {
1123
+ lines.push(` reason: ${t.reason}`);
1124
+ }
1125
+ return lines;
1126
+ }
1127
+ function renderCascade(result) {
1128
+ const d = result.decision;
1129
+ const id = d.shortId ? `dec_${d.shortId}` : d.id;
1130
+ const idColored = color(id, "orange");
1131
+ const header = `what this would break \xB7 ${String(result.fanOut)} decision(s) \xB7 enforcing`;
1132
+ const lines = [header, "", "knowledge"];
1133
+ lines.push(
1134
+ ...knowledgeRow(
1135
+ result.upstream.files,
1136
+ result.upstream.contexts,
1137
+ result.trigger?.file,
1138
+ result.trigger?.contextName
1139
+ )
1140
+ );
1141
+ if (result.trigger && (result.trigger.file || result.trigger.contextName)) {
1142
+ lines.push(" |");
1143
+ lines.push(" | refs (just edited)");
1144
+ lines.push(" \u25BC");
1145
+ }
1146
+ const decisionLine = `\u2022 ${idColored} ${truncate(d.intent, INTENT_TRUNC)}`;
1147
+ const fanOutFragment = result.fanOut > 0 ? ` \xB7 ${String(result.fanOut)} decision(s) depend on this` : "";
1148
+ const meta = ` ${d.authorName} \xB7 ${formatDate(d.classifiedAt)}${fanOutFragment} \xB7 ${result.reversibility ?? "(unset)"} reversibility`;
1149
+ lines.push("", decisionLine, meta);
1150
+ lines.push("");
1151
+ lines.push("dependents");
1152
+ lines.push(...dependentsBox(result.downstream));
1153
+ const triggered = triggerLine(result);
1154
+ if (triggered.length > 0) {
1155
+ lines.push("");
1156
+ for (const t of triggered) {
1157
+ lines.push(...softWrap(t, { indent: SOFT_WRAP_INDENT }));
1158
+ }
1159
+ }
1160
+ const crossArea = countCrossAreaDependents(d.area, result.downstream);
1161
+ const crossAreaFragment = crossArea > 0 ? ` \xB7 ${String(crossArea)} cross-area dependency` : "";
1162
+ const noEdgesFragment = result.downstream.length === 0 ? " (no edges yet)" : "";
1163
+ lines.push(
1164
+ `impact: ${String(result.fanOut)} decision(s) need review${noEdgesFragment}${crossAreaFragment}.`
1165
+ );
1166
+ if (result.truncated) {
1167
+ lines.push(
1168
+ " \u26A0 blast radius truncated \u2014 more refs/dependents than the server returns per request; not all shown."
1169
+ );
1170
+ }
1171
+ return lines.join("\n");
1172
+ }
1173
+
1174
+ // src/decisions/cascade.ts
1175
+ var NOT_FOUND_RE = /not found/i;
1176
+ var CASCADE_TIMEOUT_MS = 1e4;
1177
+ var defaultDeps = { getClient };
1178
+ var CascadeNotFoundError = class extends Error {
1179
+ constructor(idOrShortId) {
1180
+ super(`Decision not found: ${idOrShortId}`);
1181
+ this.name = "CascadeNotFoundError";
1182
+ }
1183
+ };
1184
+ async function fetchCascade(idOrShortId, deps = defaultDeps) {
1185
+ const params = new URLSearchParams({ id: idOrShortId });
1186
+ const client = deps.getClient();
1187
+ try {
1188
+ return await daemonOrDirectGet(
1189
+ "decisions_cascade",
1190
+ `/api/cli/decisions/cascade?${params.toString()}`,
1191
+ client,
1192
+ CASCADE_TIMEOUT_MS
1193
+ );
1194
+ } catch (err) {
1195
+ if (err instanceof Error && NOT_FOUND_RE.test(err.message)) {
1196
+ throw new CascadeNotFoundError(idOrShortId);
1197
+ }
1198
+ throw err;
1199
+ }
1200
+ }
1201
+ function formatCascadeJson(result) {
1202
+ return JSON.stringify(result, null, 2);
1203
+ }
1204
+
1205
+ // src/decisions/recent.ts
1206
+ var RECENT_TIMEOUT_MS = 1e4;
1207
+ var defaultDeps2 = { getClient };
1208
+ async function fetchRecent(args, deps = defaultDeps2) {
1209
+ const params = new URLSearchParams();
1210
+ if (args.limit !== void 0) {
1211
+ params.set("limit", String(args.limit));
1212
+ }
1213
+ if (args.since !== void 0) {
1214
+ params.set("since", args.since);
1215
+ }
1216
+ const client = deps.getClient();
1217
+ try {
1218
+ const res = await daemonOrDirectGet(
1219
+ "decisions_recent",
1220
+ `/api/cli/decisions/recent?${params.toString()}`,
1221
+ client,
1222
+ RECENT_TIMEOUT_MS
1223
+ );
1224
+ const result = { decisions: res.decisions };
1225
+ if (res.unavailable !== void 0) {
1226
+ result.unavailable = res.unavailable;
1227
+ }
1228
+ return result;
1229
+ } catch (err) {
1230
+ const detail = err instanceof Error ? err.message : String(err);
1231
+ return { decisions: [], unavailable: `recent feed failed: ${detail}` };
1232
+ }
1233
+ }
1234
+ var SHORT_ID_PREFIX = "dec_";
1235
+ var ZERO_PAD_TWO = 2;
1236
+ function pad2(n) {
1237
+ return n.toString().padStart(ZERO_PAD_TWO, "0");
1238
+ }
1239
+ function formatClock(ms) {
1240
+ const d = new Date(ms);
1241
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
1242
+ }
1243
+ function authorLabel(row) {
1244
+ if (!row.authorIsSelf) {
1245
+ return row.authorName;
1246
+ }
1247
+ switch (row.producerKind) {
1248
+ case "claude_code":
1249
+ return "Your Claude Code";
1250
+ case "codex":
1251
+ return "Your Codex";
1252
+ case "chat":
1253
+ return "Your chat";
1254
+ case "spec_edit":
1255
+ return "Your spec edit";
1256
+ case "cli":
1257
+ return "Your CLI";
1258
+ default:
1259
+ return `Your ${row.authorName}`;
1260
+ }
1261
+ }
1262
+ var AUTHOR_WIDTH = 18;
1263
+ function padRight(s, width) {
1264
+ return s.length >= width ? `${s.slice(0, width - 1)} ` : s.padEnd(width, " ");
1265
+ }
1266
+ function formatRecentHuman(result) {
1267
+ if (result.unavailable !== void 0) {
1268
+ return `[prim] recent \xB7 feed not verified \u2014 ${result.unavailable}`;
1269
+ }
1270
+ if (result.decisions.length === 0) {
1271
+ return "[prim] recent \xB7 0 decisions";
1272
+ }
1273
+ const lines = [`[prim] recent \xB7 ${String(result.decisions.length)} decision(s)`];
1274
+ for (const row of result.decisions) {
1275
+ const clock = formatClock(row.classifiedAt);
1276
+ const author = padRight(authorLabel(row), AUTHOR_WIDTH);
1277
+ const areaText = row.area ? `\u2022 ${row.area}` : "\u2022";
1278
+ const areaPlain = padRight(areaText, 12);
1279
+ const areaCol = row.area ? areaPlain.replace("\u2022", color("\u2022", colorForArea(row.area))) : areaPlain;
1280
+ lines.push(` ${clock} ${author}${areaCol}${row.intent}`);
1281
+ }
1282
+ return lines.join("\n");
1283
+ }
1284
+ function formatRecentJson(result) {
1285
+ return JSON.stringify(result, null, 2);
1286
+ }
1287
+ function renderIdentifier(row) {
1288
+ if (row.shortId) {
1289
+ return `${SHORT_ID_PREFIX}${row.shortId}`;
1290
+ }
1291
+ return row.id;
1292
+ }
1293
+
1294
+ // src/decisions/confirm.ts
1295
+ var CONFIRM_TIMEOUT_MS = 1e4;
1296
+ var defaultDeps3 = { getClient };
1297
+ var NOT_FOUND_RE2 = /not found/i;
1298
+ var AMBIGUOUS_RE = /ambiguous/i;
1299
+ var NOT_AUTHOR_RE = /author/i;
1300
+ var ConfirmNotFoundError = class extends Error {
1301
+ constructor(idOrShortId) {
1302
+ super(`Decision not found: ${idOrShortId}`);
1303
+ this.name = "ConfirmNotFoundError";
1304
+ }
1305
+ };
1306
+ async function fetchConfirm(idOrShortId, confirmed, deps = defaultDeps3) {
1307
+ const request = { idOrShortId, confirmed };
1308
+ const client = deps.getClient();
1309
+ try {
1310
+ const outcome = await client.post(
1311
+ "/api/cli/decisions/confirm",
1312
+ { id: idOrShortId, confirmed },
1313
+ { signal: AbortSignal.timeout(CONFIRM_TIMEOUT_MS) }
1314
+ );
1315
+ return { request, outcome };
1316
+ } catch (err) {
1317
+ if (err instanceof Error) {
1318
+ if (NOT_FOUND_RE2.test(err.message)) {
1319
+ throw new ConfirmNotFoundError(idOrShortId);
1320
+ }
1321
+ if (AMBIGUOUS_RE.test(err.message)) {
1322
+ return { request, outcome: { outcome: "ambiguous" } };
1323
+ }
1324
+ if (NOT_AUTHOR_RE.test(err.message)) {
1325
+ return { request, outcome: { outcome: "not_author" } };
1326
+ }
1327
+ }
1328
+ throw err;
1329
+ }
1330
+ }
1331
+ function intentWord(confirmed) {
1332
+ return confirmed ? "confirmed" : "rejected";
1333
+ }
1334
+ function formatConfirmHuman(result) {
1335
+ const { request, outcome } = result;
1336
+ switch (outcome.outcome) {
1337
+ case "confirmed":
1338
+ case "corrected":
1339
+ case "stale": {
1340
+ const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
1341
+ if (outcome.outcome === "stale") {
1342
+ return `[prim] ${id} ${intentWord(request.confirmed)} \u2014 the prompt had gone stale; recorded against the current decision.`;
1343
+ }
1344
+ if (outcome.outcome === "corrected") {
1345
+ return `[prim] ${id} ${intentWord(request.confirmed)} with a correction.`;
1346
+ }
1347
+ return `[prim] ${id} ${intentWord(request.confirmed)}.`;
1348
+ }
1349
+ case "already_responded": {
1350
+ const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
1351
+ const priorWord = outcome.confirmed === void 0 ? "already answered" : `already ${intentWord(outcome.confirmed)}`;
1352
+ const when = new Date(outcome.respondedAt).toISOString();
1353
+ return `[prim] ${id} was ${priorWord} (responded ${when}); nothing to change.`;
1354
+ }
1355
+ case "no_pending_prompt": {
1356
+ const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
1357
+ return `[prim] ${id} has no pending confirmation request \u2014 nothing to acknowledge.`;
1358
+ }
1359
+ case "ambiguous":
1360
+ return `[prim] shortId "${request.idOrShortId}" is ambiguous in this organization \u2014 retry with the full decision id.`;
1361
+ default:
1362
+ return "[prim] only the decision's author can respond to its confirmation prompt.";
1363
+ }
1364
+ }
1365
+ function formatConfirmJson(result) {
1366
+ return JSON.stringify(result.outcome, null, 2);
1367
+ }
1368
+
1369
+ // src/decisions/show.ts
1370
+ var NOT_FOUND_RE3 = /not found/i;
1371
+ function colorStatus(status) {
1372
+ if (status === "under_review") {
1373
+ return color(status, "orange");
1374
+ }
1375
+ if (status === "active") {
1376
+ return color(status, "green");
1377
+ }
1378
+ return color(status, "gray");
1379
+ }
1380
+ var SHOW_TIMEOUT_MS = 1e4;
1381
+ var defaultDeps4 = { getClient };
1382
+ var DecisionNotFoundError = class extends Error {
1383
+ constructor(idOrShortId) {
1384
+ super(`Decision not found: ${idOrShortId}`);
1385
+ this.name = "DecisionNotFoundError";
1386
+ }
1387
+ };
1388
+ async function fetchShow(idOrShortId, deps = defaultDeps4) {
1389
+ const params = new URLSearchParams({ id: idOrShortId });
1390
+ const client = deps.getClient();
1391
+ try {
1392
+ return await daemonOrDirectGet(
1393
+ "decisions_show",
1394
+ `/api/cli/decisions/show?${params.toString()}`,
1395
+ client,
1396
+ SHOW_TIMEOUT_MS
1397
+ );
1398
+ } catch (err) {
1399
+ if (err instanceof Error && NOT_FOUND_RE3.test(err.message)) {
1400
+ throw new DecisionNotFoundError(idOrShortId);
1401
+ }
1402
+ throw err;
1403
+ }
1404
+ }
1405
+ var GATED_FLAG_KINDS = /* @__PURE__ */ new Set(["file_edit", "supersession", "context_edit"]);
1406
+ function describeFlag(flag) {
1407
+ const detail = flag.reason ? ` \u2014 ${flag.reason}` : "";
1408
+ if (flag.acknowledgedAt !== void 0) {
1409
+ return `acknowledged ${flag.type}${detail}`;
1410
+ }
1411
+ if (flag.type === "confirmation_request") {
1412
+ return `pending confirmation request${detail}`;
1413
+ }
1414
+ if (GATED_FLAG_KINDS.has(flag.type)) {
1415
+ const verdict = flag.gateVerdict ?? "unknown";
1416
+ return `pending ${flag.type} (verdict: ${verdict})${detail}`;
1417
+ }
1418
+ return `pending ${flag.type}${detail}`;
1419
+ }
1420
+ function describeNode(node) {
1421
+ const id = renderIdentifier({ shortId: node.shortId, id: node.id });
1422
+ const area = node.area ? ` \u2022 ${node.area}` : "";
1423
+ return `${id}${area} ${node.intent} (${node.authorName})`;
1424
+ }
1425
+ function pushFiles(lines, files) {
1426
+ if (files.length === 0) {
1427
+ return;
1428
+ }
1429
+ lines.push(` files (${String(files.length)}):`);
1430
+ for (const file of files) {
1431
+ lines.push(` - ${file}`);
1432
+ }
1433
+ }
1434
+ function pushContexts(lines, contexts) {
1435
+ if (contexts.length === 0) {
1436
+ return;
1437
+ }
1438
+ lines.push(` contexts (${String(contexts.length)}):`);
1439
+ for (const ctx of contexts) {
1440
+ lines.push(` - ${ctx.name}`);
1441
+ }
1442
+ }
1443
+ function pushEdges(lines, label, arrow, nodes) {
1444
+ if (nodes.length === 0) {
1445
+ return;
1446
+ }
1447
+ lines.push(` ${label} (${String(nodes.length)}):`);
1448
+ for (const node of nodes) {
1449
+ lines.push(` ${arrow} ${describeNode(node)}`);
1450
+ }
1451
+ }
1452
+ function formatShowHuman(result) {
1453
+ const d = result.decision;
1454
+ const id = color(renderIdentifier({ shortId: d.shortId, id: d.id }), "orange");
1455
+ const confidence = d.confidence ?? "(unset)";
1456
+ const lines = [
1457
+ `[prim] ${id} \u2014 ${d.intent}`,
1458
+ ` status: ${colorStatus(d.status)}${d.confirmed ? " (confirmed)" : ""} \xB7 confidence: ${confidence} \xB7 reversibility: ${d.reversibility ?? "(unset)"}`
1459
+ ];
1460
+ if (d.supersededBy) {
1461
+ lines.push(` superseded by: ${d.supersededBy}`);
1462
+ }
1463
+ if (d.area) {
1464
+ lines.push(` area: ${color(d.area, colorForArea(d.area))}`);
1465
+ }
1466
+ if (typeof d.fanOut === "number") {
1467
+ lines.push(` fan-out: ${String(d.fanOut)}`);
1468
+ }
1469
+ if (d.respondedAt !== void 0) {
1470
+ lines.push(` responded at: ${new Date(d.respondedAt).toISOString()}`);
1471
+ }
1472
+ if (d.rationale) {
1473
+ lines.push(` rationale: ${d.rationale}`);
1474
+ }
1475
+ if (d.decided && d.decided.length > 0) {
1476
+ lines.push(` decided (${String(d.decided.length)}):`);
1477
+ for (const point of d.decided) {
1478
+ lines.push(` - ${point}`);
1479
+ }
1480
+ }
1481
+ if (d.alternatives.length > 0) {
1482
+ lines.push(` alternatives: ${d.alternatives.join(" | ")}`);
1483
+ }
1484
+ pushFiles(lines, result.files);
1485
+ pushContexts(lines, result.contexts);
1486
+ pushEdges(lines, "dependents", "\u2192", result.dependents);
1487
+ pushEdges(lines, "depends on", "\u2190", result.dependsOn);
1488
+ if (result.flags.length > 0) {
1489
+ lines.push(` flags (${String(result.flags.length)}):`);
1490
+ for (const flag of result.flags) {
1491
+ lines.push(` \xB7 ${describeFlag(flag)}`);
1492
+ }
1493
+ }
1494
+ if (result.truncated) {
1495
+ lines.push(" (partial \u2014 some related rows were truncated by a join cap)");
1496
+ }
1497
+ return lines.join("\n");
1498
+ }
1499
+ function formatShowJson(result) {
1500
+ return JSON.stringify(result, null, 2);
1501
+ }
1502
+
1503
+ // src/commands/decisions.ts
1504
+ var EXIT_NOT_FOUND = 4;
1505
+ function registerDecisionsCommands(program2) {
1506
+ const decisions = program2.command("decisions").description("Inspect the project Decision Graph");
1507
+ decisions.command("check").description("Look up active decisions that reference one or more file paths").requiredOption(
1508
+ "--files <files>",
1509
+ "Comma-separated file paths to check against the Decision Graph"
1510
+ ).action(async (opts) => {
1511
+ const filePaths = opts.files.split(",").map((s) => s.trim()).filter(Boolean);
1512
+ const result = await checkAffectedDecisions(filePaths);
1513
+ const warning = formatDecisionsWarning(result);
1514
+ if (warning) {
1515
+ console.error(warning);
1516
+ }
1517
+ printJson(result);
1518
+ });
1519
+ decisions.command("recent").description("Show the team-wide chronological decision feed").option("--limit <n>", "Maximum number of rows to return (default 10)").option(
1520
+ "--since <duration>",
1521
+ "Lookback window \u2014 accepts `Nm`, `Nh`, `Nd` (minutes / hours / days) or absolute epoch ms"
1522
+ ).action(async (opts) => {
1523
+ const result = await fetchRecent({
1524
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
1525
+ since: opts.since
1526
+ });
1527
+ console.error(formatRecentHuman(result));
1528
+ console.log(formatRecentJson(result));
1529
+ });
1530
+ decisions.command("show <idOrShortId>").description("Show full detail for one decision (intent, rationale, flags, refs, edges)").action(async (idOrShortId) => {
1531
+ try {
1532
+ const result = await fetchShow(idOrShortId);
1533
+ console.error(formatShowHuman(result));
1534
+ console.log(formatShowJson(result));
1535
+ } catch (err) {
1536
+ if (err instanceof DecisionNotFoundError) {
1537
+ console.error(`[prim] ${err.message}`);
1538
+ process.exitCode = EXIT_NOT_FOUND;
1539
+ return;
1540
+ }
1541
+ throw err;
1542
+ }
1543
+ });
1544
+ decisions.command("cascade <idOrShortId>").description("Render the local cascade subgraph (upstream knowledge + downstream dependents)").action(async (idOrShortId) => {
1545
+ try {
1546
+ const result = await fetchCascade(idOrShortId);
1547
+ console.error(renderCascade(result));
1548
+ console.log(formatCascadeJson(result));
1549
+ } catch (err) {
1550
+ if (err instanceof CascadeNotFoundError) {
1551
+ console.error(`[prim] ${err.message}`);
1552
+ process.exitCode = EXIT_NOT_FOUND;
1553
+ return;
1554
+ }
1555
+ throw err;
1556
+ }
1557
+ });
1558
+ decisions.command("confirm <idOrShortId>").description("Acknowledge a confirmation prompt for the named decision").option("--reject", "Record a rejection (sets the decision's confirmed flag to false)").action(async (idOrShortId, opts) => {
1559
+ const confirmed = !opts.reject;
1560
+ try {
1561
+ const result = await fetchConfirm(idOrShortId, confirmed);
1562
+ console.error(formatConfirmHuman(result));
1563
+ console.log(formatConfirmJson(result));
1564
+ } catch (err) {
1565
+ if (err instanceof ConfirmNotFoundError) {
1566
+ console.error(`[prim] ${err.message}`);
1567
+ process.exitCode = EXIT_NOT_FOUND;
1568
+ return;
1569
+ }
1570
+ throw err;
1571
+ }
1572
+ });
1573
+ }
1574
+
376
1575
  // src/commands/hooks.ts
377
1576
  import { execSync } from "child_process";
378
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
1577
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
379
1578
  import { resolve } from "path";
380
1579
  import { Option } from "commander";
381
1580
  var HOOK_SCRIPT = `#!/bin/sh
@@ -409,13 +1608,13 @@ function getGitRoot() {
409
1608
  }
410
1609
  function detectHusky(gitRoot) {
411
1610
  const huskyDir = resolve(gitRoot, ".husky");
412
- if (!existsSync2(huskyDir)) return false;
413
- if (existsSync2(resolve(huskyDir, "_"))) return true;
414
- if (existsSync2(resolve(huskyDir, "pre-commit"))) return true;
1611
+ if (!existsSync4(huskyDir)) return false;
1612
+ if (existsSync4(resolve(huskyDir, "_"))) return true;
1613
+ if (existsSync4(resolve(huskyDir, "pre-commit"))) return true;
415
1614
  const pkgPath = resolve(gitRoot, "package.json");
416
- if (existsSync2(pkgPath)) {
1615
+ if (existsSync4(pkgPath)) {
417
1616
  try {
418
- const pkg2 = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1617
+ const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
419
1618
  const scripts = pkg2.scripts ?? {};
420
1619
  if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
421
1620
  return true;
@@ -442,20 +1641,20 @@ async function askConfirmation(question) {
442
1641
  }
443
1642
  function installToHusky(gitRoot) {
444
1643
  const hookPath = resolve(gitRoot, ".husky", "pre-commit");
445
- if (existsSync2(hookPath)) {
446
- const existing = readFileSync3(hookPath, "utf-8");
1644
+ if (existsSync4(hookPath)) {
1645
+ const existing = readFileSync5(hookPath, "utf-8");
447
1646
  if (containsPrimHook(existing)) {
448
1647
  console.log("Prim pre-commit hook is already installed in .husky/pre-commit.");
449
1648
  return;
450
1649
  }
451
1650
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
452
- writeFileSync2(hookPath, `${existing}${separator}${PRIM_HUSKY_BLOCK}
1651
+ writeFileSync3(hookPath, `${existing}${separator}${PRIM_HUSKY_BLOCK}
453
1652
  `, {
454
1653
  mode: 493
455
1654
  });
456
1655
  console.log("Appended prim hook block to .husky/pre-commit.");
457
1656
  } else {
458
- writeFileSync2(hookPath, `#!/bin/sh
1657
+ writeFileSync3(hookPath, `#!/bin/sh
459
1658
 
460
1659
  ${PRIM_HUSKY_BLOCK}
461
1660
  `, {
@@ -467,11 +1666,11 @@ ${PRIM_HUSKY_BLOCK}
467
1666
  function installToDotGit(gitRoot) {
468
1667
  const hooksDir = resolve(gitRoot, ".git", "hooks");
469
1668
  const hookPath = resolve(hooksDir, "pre-commit");
470
- if (!existsSync2(hooksDir)) {
471
- mkdirSync2(hooksDir, { recursive: true });
1669
+ if (!existsSync4(hooksDir)) {
1670
+ mkdirSync3(hooksDir, { recursive: true });
472
1671
  }
473
- if (existsSync2(hookPath)) {
474
- const existing = readFileSync3(hookPath, "utf-8");
1672
+ if (existsSync4(hookPath)) {
1673
+ const existing = readFileSync5(hookPath, "utf-8");
475
1674
  if (containsPrimHook(existing)) {
476
1675
  console.log("Prim pre-commit hook is already installed at .git/hooks/pre-commit.");
477
1676
  return;
@@ -480,7 +1679,7 @@ function installToDotGit(gitRoot) {
480
1679
  console.log("To replace it, run: prim hooks uninstall && prim hooks install");
481
1680
  return;
482
1681
  }
483
- writeFileSync2(hookPath, HOOK_SCRIPT, { mode: 493 });
1682
+ writeFileSync3(hookPath, HOOK_SCRIPT, { mode: 493 });
484
1683
  console.log(`Installed pre-commit hook at ${hookPath}`);
485
1684
  }
486
1685
  function registerHooksCommands(program2) {
@@ -522,15 +1721,147 @@ function registerHooksCommands(program2) {
522
1721
  hooks.command("uninstall").description("Remove the prim pre-commit hook").action(() => {
523
1722
  const gitRoot = getGitRoot();
524
1723
  const hookPath = resolve(gitRoot, ".git", "hooks", "pre-commit");
525
- if (!existsSync2(hookPath)) {
1724
+ if (!existsSync4(hookPath)) {
526
1725
  console.log("No pre-commit hook found.");
527
1726
  return;
528
1727
  }
529
- unlinkSync(hookPath);
1728
+ unlinkSync2(hookPath);
530
1729
  console.log(`Removed pre-commit hook at ${hookPath}`);
531
1730
  });
532
1731
  }
533
1732
 
1733
+ // src/commands/moves.ts
1734
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
1735
+ import { join as join4 } from "path";
1736
+
1737
+ // src/flusher.ts
1738
+ import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
1739
+ var BATCH_SIZE = 500;
1740
+ var HTTP_TIMEOUT_MS = 1e4;
1741
+ var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
1742
+ async function drainPath(path) {
1743
+ const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
1744
+ try {
1745
+ renameSync2(path, tmpPath);
1746
+ } catch (err) {
1747
+ if (err.code === "ENOENT") {
1748
+ return 0;
1749
+ }
1750
+ throw err;
1751
+ }
1752
+ const moves = readMovesFromPath(tmpPath);
1753
+ if (moves.length === 0) {
1754
+ unlinkSync3(tmpPath);
1755
+ return 0;
1756
+ }
1757
+ const client = getClient();
1758
+ for (let i = 0; i < moves.length; i += BATCH_SIZE) {
1759
+ const batch = moves.slice(i, i + BATCH_SIZE);
1760
+ await client.post(
1761
+ "/api/cli/moves/ingest",
1762
+ { batch },
1763
+ { signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) }
1764
+ );
1765
+ }
1766
+ unlinkSync3(tmpPath);
1767
+ return moves.length;
1768
+ }
1769
+ async function flush() {
1770
+ let total = 0;
1771
+ for (const { path } of listBuckets()) {
1772
+ total += await drainPath(path);
1773
+ }
1774
+ return { flushed: total };
1775
+ }
1776
+ async function flushIfNeeded() {
1777
+ try {
1778
+ const stats = bucketStats();
1779
+ if (stats.length === 0) {
1780
+ return;
1781
+ }
1782
+ const oldest = stats.reduce((min, s) => s.mtimeMs < min ? s.mtimeMs : min, stats[0].mtimeMs);
1783
+ if (Date.now() - oldest > OPPORTUNISTIC_FLUSH_AFTER_MS) {
1784
+ await flush();
1785
+ }
1786
+ } catch {
1787
+ }
1788
+ }
1789
+
1790
+ // src/commands/moves.ts
1791
+ var MS_PER_SECOND = 1e3;
1792
+ var DEFAULT_TAIL_LINES = "20";
1793
+ var RADIX_DECIMAL = 10;
1794
+ var ID_PREFIX_LEN = 8;
1795
+ var EVENT_COL_WIDTH = 20;
1796
+ var BUCKET_COL_WIDTH = 20;
1797
+ var TAIL_BUCKET_COL_WIDTH = 12;
1798
+ var DIR_MODE = 448;
1799
+ var FILE_MODE2 = 384;
1800
+ var WORKSPACE_FILE = ".prim/workspace.json";
1801
+ function registerMovesCommands(program2) {
1802
+ const moves = program2.command("moves").description("Decision Event Pipeline \u2014 local journal");
1803
+ moves.command("flush").description("Drain all local move journals to the server").action(async () => {
1804
+ const { flushed } = await flush();
1805
+ console.log(`[prim] flushed ${String(flushed)} move${flushed === 1 ? "" : "s"}`);
1806
+ });
1807
+ moves.command("status").description("Show per-bucket pending stats").action(() => {
1808
+ const stats = bucketStats();
1809
+ if (stats.length === 0) {
1810
+ console.log("[prim] journal: empty");
1811
+ return;
1812
+ }
1813
+ console.log(`[prim] root: ${JOURNAL_DIR}`);
1814
+ for (const s of stats) {
1815
+ const ageS = Math.round((Date.now() - s.mtimeMs) / MS_PER_SECOND);
1816
+ console.log(
1817
+ ` ${s.bucket.padEnd(BUCKET_COL_WIDTH)} ${String(s.lineCount).padStart(5)} pending, ${String(s.sizeBytes).padStart(8)} bytes, last write ${String(ageS)}s ago`
1818
+ );
1819
+ }
1820
+ });
1821
+ moves.command("tail").description("Pretty-print recent journal entries across all buckets").option("-n, --lines <n>", "number of lines to tail", DEFAULT_TAIL_LINES).action((opts) => {
1822
+ const lines = Number.parseInt(opts.lines, RADIX_DECIMAL);
1823
+ if (!Number.isInteger(lines) || lines < 1) {
1824
+ console.error("[prim] --lines must be a positive integer");
1825
+ process.exitCode = 1;
1826
+ return;
1827
+ }
1828
+ const all = bucketStats().flatMap((s) => readMovesFromPath(s.path).map((m) => ({ bucket: s.bucket, move: m }))).sort((a, b) => a.move.capturedAt - b.move.capturedAt);
1829
+ if (all.length === 0) {
1830
+ console.log("[prim] journal: empty");
1831
+ return;
1832
+ }
1833
+ const tail = all.slice(-lines);
1834
+ for (const { bucket, move: m } of tail) {
1835
+ const t = new Date(m.capturedAt).toISOString();
1836
+ const session = m.sessionId.slice(0, ID_PREFIX_LEN) || "anon";
1837
+ const move = m.moveId.slice(0, ID_PREFIX_LEN);
1838
+ console.log(
1839
+ `${t} ${m.eventType.padEnd(EVENT_COL_WIDTH)} bucket=${bucket.padEnd(TAIL_BUCKET_COL_WIDTH)} session=${session} move=${move}`
1840
+ );
1841
+ }
1842
+ });
1843
+ moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
1844
+ const dir = join4(process.cwd(), ".prim");
1845
+ if (!existsSync5(dir)) {
1846
+ mkdirSync4(dir, { recursive: true, mode: DIR_MODE });
1847
+ }
1848
+ const file = join4(process.cwd(), WORKSPACE_FILE);
1849
+ writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
1850
+ mode: FILE_MODE2
1851
+ });
1852
+ console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
1853
+ });
1854
+ moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
1855
+ const file = join4(process.cwd(), WORKSPACE_FILE);
1856
+ if (!existsSync5(file)) {
1857
+ console.log("[prim] no workspace binding in cwd");
1858
+ return;
1859
+ }
1860
+ unlinkSync4(file);
1861
+ console.log(`[prim] dropped workspace binding from ${process.cwd()}`);
1862
+ });
1863
+ }
1864
+
534
1865
  // src/commands/project.ts
535
1866
  function registerProjectCommands(program2) {
536
1867
  const project = program2.command("project").description("Manage projects");
@@ -553,24 +1884,170 @@ function registerProjectCommands(program2) {
553
1884
  });
554
1885
  }
555
1886
 
1887
+ // src/commands/reconcile.ts
1888
+ var EXIT_OK = 0;
1889
+ var EXIT_USAGE = 2;
1890
+ var EXIT_SERVER = 3;
1891
+ var HTTP_CLIENT_ERROR_MIN = 400;
1892
+ var HTTP_SERVER_ERROR_MIN = 500;
1893
+ function isOk(value) {
1894
+ if (typeof value !== "object" || value === null) {
1895
+ return false;
1896
+ }
1897
+ const v = value;
1898
+ return v.ok === true && typeof v.bypassId === "string" && typeof v.expiresAt === "number";
1899
+ }
1900
+ function formatExpiresIn(expiresAt) {
1901
+ const remainingMs = expiresAt - Date.now();
1902
+ if (remainingMs <= 0) {
1903
+ return "expired";
1904
+ }
1905
+ const SECONDS_PER_MINUTE = 60;
1906
+ const minutes = Math.floor(remainingMs / (SECONDS_PER_MINUTE * 1e3));
1907
+ const seconds = Math.floor(remainingMs / 1e3 % SECONDS_PER_MINUTE);
1908
+ if (minutes === 0) {
1909
+ return `${seconds}s`;
1910
+ }
1911
+ return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
1912
+ }
1913
+ function renderDecisionIdentifier(short, id) {
1914
+ return short ? `dec_${short}` : id;
1915
+ }
1916
+ function isDomainRejection(err) {
1917
+ return err instanceof HttpError && err.status >= HTTP_CLIENT_ERROR_MIN && err.status < HTTP_SERVER_ERROR_MIN;
1918
+ }
1919
+ async function performReconcile(idOrShortId, opts = {}) {
1920
+ const client = getClient();
1921
+ const body = { idOrShortId };
1922
+ if (opts.flag) {
1923
+ body.conflictFlagId = opts.flag;
1924
+ }
1925
+ let response;
1926
+ try {
1927
+ response = await client.post("/api/cli/reconcile/issue", body);
1928
+ } catch (err) {
1929
+ if (isDomainRejection(err)) {
1930
+ process.stderr.write(`[prim] reconcile rejected: ${err.message}
1931
+ `);
1932
+ console.log(JSON.stringify({ ok: false, status: err.status, error: err.message }, null, 2));
1933
+ process.exitCode = EXIT_USAGE;
1934
+ return;
1935
+ }
1936
+ const message = err instanceof Error ? err.message : String(err);
1937
+ process.stderr.write(`[prim] reconcile failed: ${message}
1938
+ `);
1939
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
1940
+ process.exitCode = EXIT_SERVER;
1941
+ return;
1942
+ }
1943
+ if (isOk(response)) {
1944
+ const ident = renderDecisionIdentifier(response.decisionShortId, response.decisionId);
1945
+ const verb = response.reissued ? "reissued" : "issued";
1946
+ process.stderr.write(
1947
+ `[prim] reconcile bypass ${verb} for ${ident} (expires in ${formatExpiresIn(response.expiresAt)})
1948
+ `
1949
+ );
1950
+ console.log(JSON.stringify(response, null, 2));
1951
+ process.exitCode = EXIT_OK;
1952
+ return;
1953
+ }
1954
+ process.stderr.write("[prim] reconcile: malformed server response\n");
1955
+ console.log(JSON.stringify({ ok: false, response }, null, 2));
1956
+ process.exitCode = EXIT_SERVER;
1957
+ }
1958
+ function registerReconcileCommands(program2) {
1959
+ program2.command("reconcile <idOrShortId>").description(
1960
+ "Issue a single-use bypass for a flagged decision (used by the cooperative reconcile loop)"
1961
+ ).option(
1962
+ "--flag <conflictFlagId>",
1963
+ "Specific flag id to bind the bypass to (default: the decision's latest unack'd flag)"
1964
+ ).option("--json", "(reserved; STDOUT is always JSON)").action(async (idOrShortId, opts) => {
1965
+ await performReconcile(idOrShortId, opts);
1966
+ });
1967
+ }
1968
+
1969
+ // src/commands/session.ts
1970
+ import {
1971
+ existsSync as existsSync6,
1972
+ mkdirSync as mkdirSync5,
1973
+ readFileSync as readFileSync6,
1974
+ readdirSync,
1975
+ unlinkSync as unlinkSync5,
1976
+ writeFileSync as writeFileSync5
1977
+ } from "fs";
1978
+ import { join as join5 } from "path";
1979
+ var DIR_MODE2 = 448;
1980
+ var FILE_MODE3 = 384;
1981
+ function ensureDir() {
1982
+ if (!existsSync6(SESSIONS_DIR)) {
1983
+ mkdirSync5(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
1984
+ }
1985
+ }
1986
+ function markerPath(sessionId) {
1987
+ return join5(SESSIONS_DIR, `${sessionId}.json`);
1988
+ }
1989
+ function registerSessionCommands(program2) {
1990
+ const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
1991
+ session.command("start <sessionId>").description("Pin a Claude Code session to an org").requiredOption("--orgId <orgId>", "Convex organization id").action((sessionId, opts) => {
1992
+ ensureDir();
1993
+ const marker = {
1994
+ orgId: opts.orgId,
1995
+ startedAt: Date.now()
1996
+ };
1997
+ writeFileSync5(markerPath(sessionId), JSON.stringify(marker, null, 2), {
1998
+ mode: FILE_MODE3
1999
+ });
2000
+ console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
2001
+ });
2002
+ session.command("list").description("List active session markers").action(() => {
2003
+ if (!existsSync6(SESSIONS_DIR)) {
2004
+ console.log("[prim] no session markers");
2005
+ return;
2006
+ }
2007
+ const files = readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
2008
+ if (files.length === 0) {
2009
+ console.log("[prim] no session markers");
2010
+ return;
2011
+ }
2012
+ for (const f of files) {
2013
+ const sessionId = f.replace(/\.json$/, "");
2014
+ try {
2015
+ const m = JSON.parse(readFileSync6(join5(SESSIONS_DIR, f), "utf-8"));
2016
+ console.log(`${sessionId} org=${m.orgId}`);
2017
+ } catch {
2018
+ }
2019
+ }
2020
+ });
2021
+ session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
2022
+ const p = markerPath(sessionId);
2023
+ if (!existsSync6(p)) {
2024
+ console.log(`[prim] no marker for session ${sessionId}`);
2025
+ return;
2026
+ }
2027
+ unlinkSync5(p);
2028
+ console.log(`[prim] dropped session marker ${sessionId}`);
2029
+ });
2030
+ }
2031
+
556
2032
  // src/commands/skill.ts
557
2033
  import {
558
- closeSync,
559
- existsSync as existsSync3,
560
- fsyncSync,
561
- openSync,
562
- readFileSync as readFileSync4,
563
- renameSync,
564
- writeFileSync as writeFileSync3
2034
+ closeSync as closeSync2,
2035
+ existsSync as existsSync7,
2036
+ fsyncSync as fsyncSync2,
2037
+ openSync as openSync2,
2038
+ readFileSync as readFileSync7,
2039
+ renameSync as renameSync3,
2040
+ writeFileSync as writeFileSync6
565
2041
  } from "fs";
566
- import { dirname as dirname2, resolve as resolve2 } from "path";
2042
+ import { dirname as dirname3, resolve as resolve2 } from "path";
567
2043
  import { fileURLToPath } from "url";
568
2044
  import { createPatch } from "diff";
569
- var __dirname = dirname2(fileURLToPath(import.meta.url));
2045
+ var __dirname = dirname3(fileURLToPath(import.meta.url));
570
2046
  var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
571
2047
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
572
2048
  var TARGET_CANDIDATES = [
573
2049
  "CLAUDE.md",
2050
+ "AGENTS.md",
574
2051
  ".cursor/rules",
575
2052
  ".windsurfrules",
576
2053
  ".github/instructions/primitive.md"
@@ -578,15 +2055,15 @@ var TARGET_CANDIDATES = [
578
2055
  var DEFAULT_TARGET = "CLAUDE.md";
579
2056
  function loadSkill() {
580
2057
  let dir = __dirname;
581
- while (dir !== dirname2(dir)) {
2058
+ while (dir !== dirname3(dir)) {
582
2059
  const p = resolve2(dir, "SKILL.md");
583
- if (existsSync3(p)) return readFileSync4(p, "utf-8");
584
- dir = dirname2(dir);
2060
+ if (existsSync7(p)) return readFileSync7(p, "utf-8");
2061
+ dir = dirname3(dir);
585
2062
  }
586
2063
  throw new Error("SKILL.md not found in package");
587
2064
  }
588
2065
  function detectTargets(cwd) {
589
- return TARGET_CANDIDATES.filter((p) => existsSync3(resolve2(cwd, p)));
2066
+ return TARGET_CANDIDATES.filter((p) => existsSync7(resolve2(cwd, p)));
590
2067
  }
591
2068
  function detectNewline(content) {
592
2069
  return content.includes("\r\n") ? "\r\n" : "\n";
@@ -612,16 +2089,16 @@ function removeBlock(existing) {
612
2089
  const out = existing.slice(0, b) + existing.slice(e + SKILL_END.length);
613
2090
  return out.replace(/(\r?\n){2,}$/, "$1");
614
2091
  }
615
- function atomicWrite(target, content) {
2092
+ function atomicWrite2(target, content) {
616
2093
  const tmp = `${target}.tmp`;
617
- writeFileSync3(tmp, content);
618
- const fd = openSync(tmp, "r+");
2094
+ writeFileSync6(tmp, content);
2095
+ const fd = openSync2(tmp, "r+");
619
2096
  try {
620
- fsyncSync(fd);
2097
+ fsyncSync2(fd);
621
2098
  } finally {
622
- closeSync(fd);
2099
+ closeSync2(fd);
623
2100
  }
624
- renameSync(tmp, target);
2101
+ renameSync3(tmp, target);
625
2102
  }
626
2103
  function resolveTarget(cwd, override) {
627
2104
  if (override) return resolve2(cwd, override);
@@ -635,7 +2112,7 @@ function resolveTarget(cwd, override) {
635
2112
  function runInstall(cwd, opts) {
636
2113
  const target = resolveTarget(cwd, opts.target);
637
2114
  if (target === null) return 1;
638
- const existing = existsSync3(target) ? readFileSync4(target, "utf-8") : "";
2115
+ const existing = existsSync7(target) ? readFileSync7(target, "utf-8") : "";
639
2116
  const eol = existing ? detectNewline(existing) : "\n";
640
2117
  const block = composeBlock(loadSkill(), eol);
641
2118
  const next = applyBlock(existing, block, eol);
@@ -647,34 +2124,34 @@ function runInstall(cwd, opts) {
647
2124
  process.stdout.write(createPatch(target, existing, next, "current", "proposed"));
648
2125
  return 0;
649
2126
  }
650
- atomicWrite(target, next);
2127
+ atomicWrite2(target, next);
651
2128
  console.log(`Wrote ${Buffer.byteLength(next)} bytes to ${target}`);
652
2129
  return 0;
653
2130
  }
654
2131
  function runUninstall(cwd, opts) {
655
2132
  const target = resolveTarget(cwd, opts.target);
656
2133
  if (target === null) return 1;
657
- if (!existsSync3(target)) {
2134
+ if (!existsSync7(target)) {
658
2135
  console.log(`Skill block not present at ${target}`);
659
2136
  return 0;
660
2137
  }
661
- const existing = readFileSync4(target, "utf-8");
2138
+ const existing = readFileSync7(target, "utf-8");
662
2139
  const next = removeBlock(existing);
663
2140
  if (next === null) {
664
2141
  console.log(`Skill block not present at ${target}`);
665
2142
  return 0;
666
2143
  }
667
- atomicWrite(target, next);
2144
+ atomicWrite2(target, next);
668
2145
  console.log(`Removed skill block from ${target}`);
669
2146
  return 0;
670
2147
  }
671
2148
  function runStatus(cwd, opts) {
672
2149
  const target = resolveTarget(cwd, opts.target);
673
2150
  if (target === null) return 1;
674
- const fileExists = existsSync3(target);
2151
+ const fileExists = existsSync7(target);
675
2152
  let installed = false;
676
2153
  if (fileExists) {
677
- const content = readFileSync4(target, "utf-8");
2154
+ const content = readFileSync7(target, "utf-8");
678
2155
  installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
679
2156
  }
680
2157
  if (opts.json) {
@@ -711,7 +2188,7 @@ function registerSkillCommands(program2) {
711
2188
  }
712
2189
 
713
2190
  // src/commands/spec.ts
714
- import { readFileSync as readFileSync5 } from "fs";
2191
+ import { readFileSync as readFileSync8 } from "fs";
715
2192
  function registerSpecCommands(program2) {
716
2193
  const spec = program2.command("spec").description("Manage spec documents");
717
2194
  spec.command("list").description("List spec documents").option("-t, --project-id <projectId>", "List spec for a specific root project").option("--json", "Output as JSON").action(async (opts) => {
@@ -765,7 +2242,7 @@ ${contexts.length} spec(s)`);
765
2242
  const client = getClient();
766
2243
  let text = opts.text;
767
2244
  if (opts.file) {
768
- text = readFileSync5(opts.file, "utf-8");
2245
+ text = readFileSync8(opts.file, "utf-8");
769
2246
  }
770
2247
  const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
771
2248
  let linkedBranch;
@@ -806,7 +2283,7 @@ ${contexts.length} spec(s)`);
806
2283
  const client = getClient();
807
2284
  let text = opts.text;
808
2285
  if (opts.file) {
809
- text = readFileSync5(opts.file, "utf-8");
2286
+ text = readFileSync8(opts.file, "utf-8");
810
2287
  }
811
2288
  if (!(text || opts.name)) {
812
2289
  console.error("Provide --text, --file, or --name to update.");
@@ -976,9 +2453,64 @@ ${preview}`);
976
2453
  }
977
2454
  }
978
2455
 
2456
+ // src/commands/statusline.ts
2457
+ import { readFileSync as readFileSync9 } from "fs";
2458
+ import { dirname as dirname4, resolve as resolve3 } from "path";
2459
+ import { fileURLToPath as fileURLToPath2 } from "url";
2460
+ var STATUSLINE_TIMEOUT_MS = 200;
2461
+ function readPackageVersion() {
2462
+ try {
2463
+ const here = dirname4(fileURLToPath2(import.meta.url));
2464
+ const candidates = [resolve3(here, "../../package.json"), resolve3(here, "../package.json")];
2465
+ for (const path of candidates) {
2466
+ try {
2467
+ const pkg2 = JSON.parse(readFileSync9(path, "utf-8"));
2468
+ if (pkg2.version) {
2469
+ return pkg2.version;
2470
+ }
2471
+ } catch {
2472
+ }
2473
+ }
2474
+ } catch {
2475
+ }
2476
+ return "0.0.0";
2477
+ }
2478
+ function debug(msg) {
2479
+ if (process.env.PRIM_STATUSLINE_DEBUG === "1") {
2480
+ process.stderr.write(`[prim-statusline] ${msg}
2481
+ `);
2482
+ }
2483
+ }
2484
+ async function renderStatusline() {
2485
+ const version = readPackageVersion();
2486
+ const snapshot = await daemonRequest(
2487
+ "status_snapshot",
2488
+ {},
2489
+ { timeoutMs: STATUSLINE_TIMEOUT_MS }
2490
+ );
2491
+ if (!snapshot) {
2492
+ debug("daemon snapshot missing");
2493
+ return `primitive ${version} (daemon: down)`;
2494
+ }
2495
+ if (snapshot.presenceStale) {
2496
+ return `primitive ${version} (daemon: live \xB7 presence: stale)`;
2497
+ }
2498
+ const team = typeof snapshot.onlineCount === "number" ? `team: ${String(snapshot.onlineCount)} online` : "team: \u2014";
2499
+ return `primitive ${version} (daemon: live \xB7 ${team})`;
2500
+ }
2501
+ function registerStatuslineCommands(program2) {
2502
+ program2.command("statusline").description("Render the Claude Code statusLine for the prim companion daemon").action(async () => {
2503
+ try {
2504
+ const line = await renderStatusline();
2505
+ process.stdout.write(line);
2506
+ } catch {
2507
+ }
2508
+ });
2509
+ }
2510
+
979
2511
  // src/index.ts
980
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
981
- var pkg = JSON.parse(readFileSync6(resolve3(__dirname2, "../package.json"), "utf-8"));
2512
+ var __dirname2 = dirname5(fileURLToPath3(import.meta.url));
2513
+ var pkg = JSON.parse(readFileSync10(resolve4(__dirname2, "../package.json"), "utf-8"));
982
2514
  updateNotifier({ pkg }).notify();
983
2515
  var program = new Command();
984
2516
  program.name("prim").description("CLI for managing Primitive specs and contexts").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
@@ -991,9 +2523,23 @@ registerSpecCommands(program);
991
2523
  registerProjectCommands(program);
992
2524
  registerHooksCommands(program);
993
2525
  registerSkillCommands(program);
2526
+ registerMovesCommands(program);
2527
+ registerSessionCommands(program);
2528
+ registerDecisionsCommands(program);
2529
+ registerClaudeCommands(program);
2530
+ registerCodexCommands(program);
2531
+ registerDaemonCommands(program);
2532
+ registerReconcileCommands(program);
2533
+ registerStatuslineCommands(program);
994
2534
  process.on("unhandledRejection", (err) => {
995
2535
  const msg = err instanceof Error ? err.message : String(err);
996
2536
  console.error(msg);
997
2537
  process.exit(1);
998
2538
  });
2539
+ var argv = process.argv.slice(2);
2540
+ var isExplicitFlush = argv[0] === "moves" && argv[1] === "flush";
2541
+ if (!isExplicitFlush) {
2542
+ flushIfNeeded().catch(() => {
2543
+ });
2544
+ }
999
2545
  program.parse();