@primitive.ai/prim 0.1.0-alpha.15 → 0.1.0-alpha.16

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,41 @@
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
+ formatDecisionsWarning,
10
+ getGitContext
11
+ } from "./chunk-S47B4VGC.js";
12
+ import {
13
+ HttpError,
3
14
  REFRESH_TOKEN_PATH,
4
15
  TOKEN_EXPIRES_PATH,
5
16
  TOKEN_FILE_PATH,
6
17
  getAuthToken,
7
18
  getClient,
8
- getGitContext,
9
19
  getSiteUrl,
10
20
  getTokenExpiresAt,
11
21
  saveTokenExpiry
12
- } from "./chunk-SHLF6OL2.js";
22
+ } from "./chunk-6SIEWWUL.js";
23
+ import {
24
+ JOURNAL_DIR,
25
+ SESSIONS_DIR,
26
+ bucketStats,
27
+ listBuckets,
28
+ readMovesFromPath
29
+ } from "./chunk-JZGWQDM5.js";
30
+ import {
31
+ daemonIsLive,
32
+ daemonRequest
33
+ } from "./chunk-UTKQTZHL.js";
13
34
 
14
35
  // 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";
36
+ import { readFileSync as readFileSync10 } from "fs";
37
+ import { dirname as dirname5, resolve as resolve4 } from "path";
38
+ import { fileURLToPath as fileURLToPath3 } from "url";
18
39
  import { Command } from "commander";
19
40
  import updateNotifier from "update-notifier";
20
41
 
@@ -112,10 +133,10 @@ function registerAuthCommands(program2) {
112
133
  process.exit(1);
113
134
  });
114
135
  });
115
- const port = await new Promise((resolve4) => {
136
+ const port = await new Promise((resolve5) => {
116
137
  server.listen(CALLBACK_PORT, LOCALHOST, () => {
117
138
  const addr = server.address();
118
- resolve4(typeof addr === "object" && addr ? addr.port : 0);
139
+ resolve5(typeof addr === "object" && addr ? addr.port : 0);
119
140
  });
120
141
  });
121
142
  const redirectUri = `http://${LOCALHOST}:${port}/callback`;
@@ -256,8 +277,263 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
256
277
  return data.access_token;
257
278
  }
258
279
 
280
+ // src/commands/claude-install.ts
281
+ import {
282
+ closeSync,
283
+ existsSync as existsSync2,
284
+ fsyncSync,
285
+ mkdirSync as mkdirSync2,
286
+ openSync,
287
+ readFileSync as readFileSync2,
288
+ renameSync,
289
+ writeFileSync as writeFileSync2
290
+ } from "fs";
291
+ import { homedir } from "os";
292
+ import { dirname as dirname2, join } from "path";
293
+ var CAPTURE_COMMAND = "prim-hook";
294
+ var GATE_COMMAND = "prim-pre-tool-use";
295
+ var POST_TOOL_USE_COMMAND = "prim-post-tool-use";
296
+ var SESSION_START_COMMAND = "prim-session-start";
297
+ var SESSION_END_COMMAND = "prim-session-end";
298
+ var STATUSLINE_COMMAND = "prim statusline";
299
+ var STATUSLINE_REFRESH_SECONDS = 5;
300
+ var PRIM_COMMANDS = /* @__PURE__ */ new Set([
301
+ CAPTURE_COMMAND,
302
+ GATE_COMMAND,
303
+ POST_TOOL_USE_COMMAND,
304
+ SESSION_START_COMMAND,
305
+ SESSION_END_COMMAND
306
+ ]);
307
+ var JSON_INDENT = 2;
308
+ var USER_SCOPE_PATH = join(homedir(), ".claude", "settings.json");
309
+ var PROJECT_SCOPE_PATH = join(process.cwd(), ".claude", "settings.json");
310
+ var CAPTURE_EVENTS = [
311
+ "SessionStart",
312
+ "UserPromptSubmit",
313
+ "PreToolUse",
314
+ "PostToolUse",
315
+ "Stop",
316
+ "SessionEnd",
317
+ "SubagentStop"
318
+ ];
319
+ var REGISTRATIONS = [
320
+ ...CAPTURE_EVENTS.map((event) => ({ event, matcher: "*", command: CAPTURE_COMMAND })),
321
+ { event: "PreToolUse", matcher: "Edit|Write|MultiEdit", command: GATE_COMMAND },
322
+ { event: "PostToolUse", matcher: "Edit|Write|MultiEdit", command: POST_TOOL_USE_COMMAND },
323
+ { event: "SessionStart", matcher: "*", command: SESSION_START_COMMAND },
324
+ { event: "SessionEnd", matcher: "*", command: SESSION_END_COMMAND }
325
+ ];
326
+ function settingsPathFor(scope) {
327
+ return scope === "user" ? USER_SCOPE_PATH : PROJECT_SCOPE_PATH;
328
+ }
329
+ function readSettings(path) {
330
+ if (!existsSync2(path)) {
331
+ return {};
332
+ }
333
+ const raw = readFileSync2(path, "utf-8");
334
+ try {
335
+ return JSON.parse(raw);
336
+ } catch (err) {
337
+ const detail = err instanceof Error ? err.message : String(err);
338
+ throw new Error(`${path} is not valid JSON: ${detail}`);
339
+ }
340
+ }
341
+ function entryHasCommand(entry, command) {
342
+ return entry.hooks?.some((h) => h.command === command) ?? false;
343
+ }
344
+ function canonicalEntry(reg) {
345
+ return { matcher: reg.matcher, hooks: [{ type: "command", command: reg.command }] };
346
+ }
347
+ function stripCommand(list, command) {
348
+ const out = [];
349
+ for (const e of list) {
350
+ const hooks = (e.hooks ?? []).filter((h) => h.command !== command);
351
+ if (hooks.length > 0) {
352
+ out.push({ ...e, hooks });
353
+ }
354
+ }
355
+ return out;
356
+ }
357
+ function ensureRegistration(list, reg, force) {
358
+ const hasCanonical = list.some(
359
+ (e) => e.matcher === reg.matcher && e.hooks?.length === 1 && e.hooks[0].command === reg.command
360
+ );
361
+ if (hasCanonical && !force) {
362
+ return list;
363
+ }
364
+ return [...stripCommand(list, reg.command), canonicalEntry(reg)];
365
+ }
366
+ function isPrimStatusLine(settings) {
367
+ const s = settings.statusLine;
368
+ return s?.type === "command" && s?.command === STATUSLINE_COMMAND;
369
+ }
370
+ function applyStatusLine(settings) {
371
+ if (settings.statusLine && !isPrimStatusLine(settings)) {
372
+ return settings;
373
+ }
374
+ return {
375
+ ...settings,
376
+ statusLine: {
377
+ type: "command",
378
+ command: STATUSLINE_COMMAND,
379
+ refreshInterval: STATUSLINE_REFRESH_SECONDS
380
+ }
381
+ };
382
+ }
383
+ function applyInstall(settings, options = {}) {
384
+ const hooks = { ...settings.hooks ?? {} };
385
+ for (const reg of REGISTRATIONS) {
386
+ hooks[reg.event] = ensureRegistration(hooks[reg.event] ?? [], reg, options.force ?? false);
387
+ }
388
+ return applyStatusLine({ ...settings, hooks });
389
+ }
390
+ function applyUninstall(settings) {
391
+ const source = settings.hooks ?? {};
392
+ const hooks = {};
393
+ for (const event of Object.keys(source)) {
394
+ let list = source[event] ?? [];
395
+ for (const command of PRIM_COMMANDS) {
396
+ list = stripCommand(list, command);
397
+ }
398
+ if (list.length > 0) {
399
+ hooks[event] = list;
400
+ }
401
+ }
402
+ const next = { ...settings, hooks };
403
+ if (isPrimStatusLine(next)) {
404
+ next.statusLine = void 0;
405
+ }
406
+ return next;
407
+ }
408
+ function captureInstalled(settings) {
409
+ return CAPTURE_EVENTS.some(
410
+ (event) => (settings.hooks?.[event] ?? []).some((e) => entryHasCommand(e, CAPTURE_COMMAND))
411
+ );
412
+ }
413
+ function statuslineInstalled(settings) {
414
+ return isPrimStatusLine(settings);
415
+ }
416
+ function isGateInstalled(settings) {
417
+ return (settings.hooks?.PreToolUse ?? []).some((e) => entryHasCommand(e, GATE_COMMAND));
418
+ }
419
+ function atomicWrite(path, content) {
420
+ const dir = dirname2(path);
421
+ if (!existsSync2(dir)) {
422
+ mkdirSync2(dir, { recursive: true });
423
+ }
424
+ const tmp = `${path}.tmp.${String(Date.now())}`;
425
+ writeFileSync2(tmp, `${JSON.stringify(content, null, JSON_INDENT)}
426
+ `, "utf-8");
427
+ const fd = openSync(tmp, "r+");
428
+ try {
429
+ fsyncSync(fd);
430
+ } finally {
431
+ closeSync(fd);
432
+ }
433
+ renameSync(tmp, path);
434
+ }
435
+ function performInstall(scope, force) {
436
+ const path = settingsPathFor(scope);
437
+ const before = readSettings(path);
438
+ const after = applyInstall(before, { force });
439
+ const changed = JSON.stringify(before) !== JSON.stringify(after);
440
+ if (changed) {
441
+ atomicWrite(path, after);
442
+ }
443
+ return {
444
+ scope,
445
+ path,
446
+ gate: isGateInstalled(after),
447
+ capture: captureInstalled(after),
448
+ statusline: statuslineInstalled(after),
449
+ changed
450
+ };
451
+ }
452
+ function performUninstall(scope) {
453
+ const path = settingsPathFor(scope);
454
+ const before = readSettings(path);
455
+ const after = applyUninstall(before);
456
+ const changed = JSON.stringify(before) !== JSON.stringify(after);
457
+ if (changed) {
458
+ atomicWrite(path, after);
459
+ }
460
+ return {
461
+ scope,
462
+ path,
463
+ gate: isGateInstalled(after),
464
+ capture: captureInstalled(after),
465
+ statusline: statuslineInstalled(after),
466
+ changed
467
+ };
468
+ }
469
+ function performStatus() {
470
+ const statusFor = (path) => {
471
+ const settings = readSettings(path);
472
+ return {
473
+ path,
474
+ gate: isGateInstalled(settings),
475
+ capture: captureInstalled(settings),
476
+ statusline: statuslineInstalled(settings)
477
+ };
478
+ };
479
+ return { user: statusFor(USER_SCOPE_PATH), project: statusFor(PROJECT_SCOPE_PATH) };
480
+ }
481
+ function resolveScope(input) {
482
+ if (input === void 0 || input === "user") {
483
+ return "user";
484
+ }
485
+ if (input === "project") {
486
+ return "project";
487
+ }
488
+ console.error(`[prim] unknown --scope "${input}" (expected: user or project)`);
489
+ process.exit(1);
490
+ }
491
+ function registerClaudeCommands(program2) {
492
+ const claude = program2.command("claude").description("Manage the prim Claude Code integration (capture, gate, ingest, presence)");
493
+ claude.command("install").description("Register the prim hooks + statusline in Claude Code's settings.json").option(
494
+ "--scope <scope>",
495
+ "user (default, ~/.claude/settings.json) or project (./.claude/settings.json)"
496
+ ).option("--force", "Replace any drifted prim hook entries").action((opts) => {
497
+ const scope = resolveScope(opts.scope);
498
+ const result = performInstall(scope, opts.force ?? false);
499
+ if (result.changed) {
500
+ console.error(
501
+ `[prim] Claude Code integration installed (${scope} scope) at ${result.path}`
502
+ );
503
+ } else {
504
+ console.error(
505
+ `[prim] Claude Code integration already present at ${result.path} (no changes)`
506
+ );
507
+ }
508
+ console.log(JSON.stringify(result, null, JSON_INDENT));
509
+ });
510
+ claude.command("uninstall").description("Remove all prim hooks + the prim statusline from settings.json").option(
511
+ "--scope <scope>",
512
+ "user (default, ~/.claude/settings.json) or project (./.claude/settings.json)"
513
+ ).action((opts) => {
514
+ const scope = resolveScope(opts.scope);
515
+ const result = performUninstall(scope);
516
+ if (result.changed) {
517
+ console.error(`[prim] prim hooks removed from ${result.path}`);
518
+ } else {
519
+ console.error(`[prim] no prim hooks to remove at ${result.path} (nothing changed)`);
520
+ }
521
+ console.log(JSON.stringify(result, null, JSON_INDENT));
522
+ });
523
+ claude.command("status").description(
524
+ "Report whether each prim surface (gate, capture, statusline) is installed per scope"
525
+ ).action(() => {
526
+ const result = performStatus();
527
+ const mark = (b) => b ? "\u2713" : "\u2717";
528
+ const line = (label, s) => `[prim] ${label}: gate ${mark(s.gate)} \xB7 capture ${mark(s.capture)} \xB7 statusline ${mark(s.statusline)} (${s.path})`;
529
+ console.error(`${line("user", result.user)}
530
+ ${line("project", result.project)}`);
531
+ console.log(JSON.stringify(result, null, JSON_INDENT));
532
+ });
533
+ }
534
+
259
535
  // src/commands/context.ts
260
- import { readFileSync as readFileSync2 } from "fs";
536
+ import { readFileSync as readFileSync3 } from "fs";
261
537
  function registerContextCommands(program2) {
262
538
  const context = program2.command("context").description("Manage contexts");
263
539
  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 +562,7 @@ function registerContextCommands(program2) {
286
562
  const client = getClient();
287
563
  let text = opts.text;
288
564
  if (opts.file) {
289
- text = readFileSync2(opts.file, "utf-8");
565
+ text = readFileSync3(opts.file, "utf-8");
290
566
  }
291
567
  const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
292
568
  const result = await client.post("/api/cli/contexts", {
@@ -309,7 +585,7 @@ function registerContextCommands(program2) {
309
585
  const client = getClient();
310
586
  let text = opts.text;
311
587
  if (opts.file) {
312
- text = readFileSync2(opts.file, "utf-8");
588
+ text = readFileSync3(opts.file, "utf-8");
313
589
  }
314
590
  await client.patch(`/api/cli/contexts/${contextId}`, {
315
591
  name: opts.name,
@@ -373,9 +649,772 @@ function printContextList(contexts) {
373
649
  ${contexts.length} context(s)`);
374
650
  }
375
651
 
652
+ // src/commands/daemon.ts
653
+ import { spawn } from "child_process";
654
+ import { existsSync as existsSync3, readFileSync as readFileSync4, unlinkSync } from "fs";
655
+ import { homedir as homedir2 } from "os";
656
+ import { join as join2 } from "path";
657
+ var DAEMON_BIN = "prim-daemon-server";
658
+ var PID_PATH = join2(homedir2(), ".config", "prim", "daemon.pid");
659
+ var SOCK_PATH = join2(homedir2(), ".config", "prim", "sock");
660
+ var STOP_TIMEOUT_MS = 5e3;
661
+ var STOP_POLL_MS = 100;
662
+ var STATUS_PROBE_TIMEOUT_MS = 500;
663
+ var POST_START_WAIT_MS = 400;
664
+ var EXIT_NOT_RUNNING = 2;
665
+ function readPidfile() {
666
+ if (!existsSync3(PID_PATH)) {
667
+ return null;
668
+ }
669
+ const raw = readFileSync4(PID_PATH, "utf-8").trim();
670
+ const pid = Number(raw);
671
+ if (!Number.isInteger(pid) || pid <= 0) {
672
+ return null;
673
+ }
674
+ return { pid, alive: processIsAlive(pid) };
675
+ }
676
+ function processIsAlive(pid) {
677
+ try {
678
+ process.kill(pid, 0);
679
+ return true;
680
+ } catch {
681
+ return false;
682
+ }
683
+ }
684
+ function clearStaleArtifacts() {
685
+ try {
686
+ unlinkSync(PID_PATH);
687
+ } catch {
688
+ }
689
+ try {
690
+ unlinkSync(SOCK_PATH);
691
+ } catch {
692
+ }
693
+ }
694
+ function sleep(ms) {
695
+ return new Promise((resolve5) => {
696
+ const timer = setTimeout(resolve5, ms);
697
+ timer.unref();
698
+ });
699
+ }
700
+ async function daemonStart(opts) {
701
+ const existing = readPidfile();
702
+ if (existing?.alive) {
703
+ process.stderr.write(`[prim] daemon already running (pid=${existing.pid})
704
+ `);
705
+ console.log(JSON.stringify({ started: false, pid: existing.pid }, null, 2));
706
+ return;
707
+ }
708
+ if (existing && !existing.alive) {
709
+ clearStaleArtifacts();
710
+ }
711
+ if (opts.foreground) {
712
+ const child2 = spawn(DAEMON_BIN, [], { stdio: "inherit" });
713
+ child2.on("exit", (code) => {
714
+ process.exit(code ?? 0);
715
+ });
716
+ return;
717
+ }
718
+ const child = spawn(DAEMON_BIN, [], {
719
+ detached: true,
720
+ stdio: ["ignore", "ignore", "ignore"]
721
+ });
722
+ child.unref();
723
+ await sleep(POST_START_WAIT_MS);
724
+ const after = readPidfile();
725
+ if (after?.alive) {
726
+ process.stderr.write(`[prim] daemon started (pid=${after.pid}, socket=${SOCK_PATH})
727
+ `);
728
+ console.log(JSON.stringify({ started: true, pid: after.pid }, null, 2));
729
+ return;
730
+ }
731
+ process.stderr.write(
732
+ "[prim] daemon start: bin spawned but no pidfile observed (check that `prim-daemon-server` is on PATH)\n"
733
+ );
734
+ console.log(JSON.stringify({ started: false }, null, 2));
735
+ }
736
+ async function daemonStop() {
737
+ const existing = readPidfile();
738
+ if (!existing) {
739
+ process.stderr.write("[prim] daemon not running (no pidfile)\n");
740
+ console.log(JSON.stringify({ stopped: false, wasRunning: false }, null, 2));
741
+ return;
742
+ }
743
+ if (!existing.alive) {
744
+ clearStaleArtifacts();
745
+ process.stderr.write("[prim] daemon not running (cleared stale pidfile)\n");
746
+ console.log(JSON.stringify({ stopped: false, wasRunning: false }, null, 2));
747
+ return;
748
+ }
749
+ try {
750
+ process.kill(existing.pid, "SIGTERM");
751
+ } catch (err) {
752
+ process.stderr.write(
753
+ `[prim] could not signal pid=${existing.pid}: ${err instanceof Error ? err.message : String(err)}
754
+ `
755
+ );
756
+ console.log(JSON.stringify({ stopped: false, pid: existing.pid }, null, 2));
757
+ return;
758
+ }
759
+ const deadline = Date.now() + STOP_TIMEOUT_MS;
760
+ while (Date.now() < deadline) {
761
+ if (!processIsAlive(existing.pid)) {
762
+ clearStaleArtifacts();
763
+ process.stderr.write(`[prim] daemon stopped (pid=${existing.pid})
764
+ `);
765
+ console.log(JSON.stringify({ stopped: true, pid: existing.pid }, null, 2));
766
+ return;
767
+ }
768
+ await sleep(STOP_POLL_MS);
769
+ }
770
+ process.stderr.write(
771
+ `[prim] daemon did not exit within ${STOP_TIMEOUT_MS}ms (pid=${existing.pid} still alive)
772
+ `
773
+ );
774
+ console.log(JSON.stringify({ stopped: false, pid: existing.pid }, null, 2));
775
+ }
776
+ async function daemonStatus() {
777
+ const pid = readPidfile();
778
+ if (!pid?.alive) {
779
+ process.stderr.write("[prim] \u2717 daemon down\n");
780
+ console.log(JSON.stringify({ running: false }, null, 2));
781
+ if (!process.exitCode) {
782
+ process.exitCode = EXIT_NOT_RUNNING;
783
+ }
784
+ return;
785
+ }
786
+ const live = await daemonIsLive(STATUS_PROBE_TIMEOUT_MS);
787
+ if (!live) {
788
+ process.stderr.write(`[prim] \u2717 daemon pid=${pid.pid} alive but socket not responding
789
+ `);
790
+ console.log(JSON.stringify({ running: true, responding: false, pid: pid.pid }, null, 2));
791
+ if (!process.exitCode) {
792
+ process.exitCode = EXIT_NOT_RUNNING;
793
+ }
794
+ return;
795
+ }
796
+ const snapshot = await daemonRequest(
797
+ "status_snapshot",
798
+ {},
799
+ { timeoutMs: STATUS_PROBE_TIMEOUT_MS }
800
+ );
801
+ if (!snapshot) {
802
+ process.stderr.write("[prim] \u2713 daemon live (no snapshot)\n");
803
+ console.log(JSON.stringify({ running: true, responding: true }, null, 2));
804
+ return;
805
+ }
806
+ process.stderr.write(
807
+ `[prim] \u2713 daemon live \xB7 pid=${snapshot.pid} \xB7 uptime=${Math.round(snapshot.uptimeMs / 1e3)}s \xB7 session=${snapshot.sessionId}
808
+ `
809
+ );
810
+ console.log(JSON.stringify({ running: true, responding: true, ...snapshot }, null, 2));
811
+ }
812
+ async function daemonRestart(opts) {
813
+ await daemonStop();
814
+ await daemonStart(opts);
815
+ }
816
+ function registerDaemonCommands(program2) {
817
+ const daemon = program2.command("daemon").description("Manage the prim companion daemon (latency unlock + presence + broadcast)");
818
+ 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) => {
819
+ await daemonStart(opts);
820
+ });
821
+ daemon.command("stop").description("Send SIGTERM to the running daemon and clean up the socket").action(async () => {
822
+ await daemonStop();
823
+ });
824
+ daemon.command("status").description("Report daemon liveness + a snapshot if responding").action(async () => {
825
+ await daemonStatus();
826
+ });
827
+ daemon.command("restart").description("Stop, then start (preserves no state today)").option("--foreground", "Restart in the foreground").action(async (opts) => {
828
+ await daemonRestart(opts);
829
+ });
830
+ }
831
+
832
+ // src/decisions/cascade-renderer.ts
833
+ var DEPENDENTS_INLINE_LIMIT = 5;
834
+ var KNOWLEDGE_INLINE_LIMIT = 4;
835
+ var ISO_DATE_LENGTH = 10;
836
+ var INTENT_TRUNC = 60;
837
+ var DEFAULT_WIDTH = 80;
838
+ var SOFT_WRAP_INDENT = " ";
839
+ function terminalWidth() {
840
+ return process.stdout.columns ?? DEFAULT_WIDTH;
841
+ }
842
+ function softWrap(line, opts) {
843
+ const width = opts?.width ?? terminalWidth();
844
+ const indent = opts?.indent ?? "";
845
+ if (stripAnsi(line).length <= width) {
846
+ return [line];
847
+ }
848
+ const words = line.split(" ");
849
+ const out = [];
850
+ let current = "";
851
+ for (const w of words) {
852
+ if (current === "") {
853
+ current = w;
854
+ continue;
855
+ }
856
+ const tentative = `${current} ${w}`;
857
+ if (stripAnsi(tentative).length > width) {
858
+ out.push(current);
859
+ current = `${indent}${w}`;
860
+ continue;
861
+ }
862
+ current = tentative;
863
+ }
864
+ if (current.length > 0) {
865
+ out.push(current);
866
+ }
867
+ return out;
868
+ }
869
+ function formatDate(ms) {
870
+ return new Date(ms).toISOString().slice(0, ISO_DATE_LENGTH);
871
+ }
872
+ function truncate(s, max) {
873
+ if (s.length <= max) {
874
+ return s;
875
+ }
876
+ return `${s.slice(0, max - 1)}\u2026`;
877
+ }
878
+ function bracketed(label) {
879
+ return `[${label}]`;
880
+ }
881
+ function knowledgeRow(files, contexts, triggerFile, triggerContextName) {
882
+ const tokens = [];
883
+ for (const ctx of contexts.slice(0, KNOWLEDGE_INLINE_LIMIT)) {
884
+ const star = ctx.name === triggerContextName ? " *" : "";
885
+ tokens.push(bracketed(`${ctx.name}${star}`));
886
+ }
887
+ for (const f of files.slice(0, Math.max(0, KNOWLEDGE_INLINE_LIMIT - contexts.length))) {
888
+ const star = f === triggerFile ? " *" : "";
889
+ tokens.push(bracketed(`${f}${star}`));
890
+ }
891
+ const overflow = files.length + contexts.length - tokens.length > 0 ? ` (+${String(files.length + contexts.length - tokens.length)} more)` : "";
892
+ if (tokens.length === 0) {
893
+ return [" (no upstream knowledge refs)"];
894
+ }
895
+ return [` ${tokens.join(" ")}${overflow}`];
896
+ }
897
+ function areaChip(area) {
898
+ if (!area) {
899
+ return color("[--]", "gray");
900
+ }
901
+ return color(`[${area}]`, colorForArea(area));
902
+ }
903
+ function countCrossAreaDependents(parentArea, dependents) {
904
+ if (!parentArea) {
905
+ const areaCounts = /* @__PURE__ */ new Map();
906
+ for (const d of dependents) {
907
+ if (d.area) {
908
+ areaCounts.set(d.area, (areaCounts.get(d.area) ?? 0) + 1);
909
+ }
910
+ }
911
+ if (areaCounts.size <= 1) {
912
+ return 0;
913
+ }
914
+ let dominantCount = 0;
915
+ for (const c of areaCounts.values()) {
916
+ if (c > dominantCount) {
917
+ dominantCount = c;
918
+ }
919
+ }
920
+ return dependents.filter((d) => d.area).length - dominantCount;
921
+ }
922
+ let count = 0;
923
+ for (const d of dependents) {
924
+ if (d.area && d.area !== parentArea) {
925
+ count++;
926
+ }
927
+ }
928
+ return count;
929
+ }
930
+ function dependentsBox(dependents) {
931
+ if (dependents.length === 0) {
932
+ return [" (no downstream dependents)"];
933
+ }
934
+ const inlineCount = Math.min(dependents.length, DEPENDENTS_INLINE_LIMIT);
935
+ const header = `${String(dependents.length)} affected:`;
936
+ const lines = [` ${header}`];
937
+ for (const d of dependents.slice(0, inlineCount)) {
938
+ lines.push(` \u2022 ${areaChip(d.area)} ${truncate(d.intent, INTENT_TRUNC)}`);
939
+ }
940
+ if (dependents.length > inlineCount) {
941
+ lines.push(` + ${String(dependents.length - inlineCount)} more`);
942
+ }
943
+ return lines;
944
+ }
945
+ function triggerHeadline(t) {
946
+ const at = formatDate(t.flaggedAt);
947
+ if (t.type === "file_edit" && t.file) {
948
+ return `trigger: file '${t.file}' was edited; cascade fired at ${at}.`;
949
+ }
950
+ if (t.type === "context_edit" && t.contextName) {
951
+ return `trigger: context '${t.contextName}' was edited; cascade fired at ${at}.`;
952
+ }
953
+ if (t.type === "supersession") {
954
+ return `trigger: an upstream decision was superseded; cascade fired at ${at}.`;
955
+ }
956
+ if (t.type === "invalidation") {
957
+ return `trigger: an upstream decision was invalidated; cascade fired at ${at}.`;
958
+ }
959
+ if (t.type === "confirmation_request") {
960
+ return `trigger: asking-policy confirmation request opened at ${at}.`;
961
+ }
962
+ return `trigger: ${t.type} at ${at}.`;
963
+ }
964
+ function triggerLine(result) {
965
+ const t = result.trigger;
966
+ if (!t) {
967
+ return [];
968
+ }
969
+ const lines = [triggerHeadline(t)];
970
+ if (t.authorName) {
971
+ lines.push(` by ${t.authorName}`);
972
+ }
973
+ if (t.reason) {
974
+ lines.push(` reason: ${t.reason}`);
975
+ }
976
+ return lines;
977
+ }
978
+ function renderCascade(result) {
979
+ const d = result.decision;
980
+ const id = d.shortId ? `dec_${d.shortId}` : d.id;
981
+ const idColored = color(id, "orange");
982
+ const header = `what this would break \xB7 ${String(result.fanOut)} decision(s) \xB7 enforcing`;
983
+ const lines = [header, "", "knowledge"];
984
+ lines.push(
985
+ ...knowledgeRow(
986
+ result.upstream.files,
987
+ result.upstream.contexts,
988
+ result.trigger?.file,
989
+ result.trigger?.contextName
990
+ )
991
+ );
992
+ if (result.trigger && (result.trigger.file || result.trigger.contextName)) {
993
+ lines.push(" |");
994
+ lines.push(" | refs (just edited)");
995
+ lines.push(" \u25BC");
996
+ }
997
+ const decisionLine = `\u2022 ${idColored} ${truncate(d.intent, INTENT_TRUNC)}`;
998
+ const fanOutFragment = result.fanOut > 0 ? ` \xB7 ${String(result.fanOut)} decision(s) depend on this` : "";
999
+ const meta = ` ${d.authorName} \xB7 ${formatDate(d.classifiedAt)}${fanOutFragment} \xB7 ${result.reversibility ?? "(unset)"} reversibility`;
1000
+ lines.push("", decisionLine, meta);
1001
+ lines.push("");
1002
+ lines.push("dependents");
1003
+ lines.push(...dependentsBox(result.downstream));
1004
+ const triggered = triggerLine(result);
1005
+ if (triggered.length > 0) {
1006
+ lines.push("");
1007
+ for (const t of triggered) {
1008
+ lines.push(...softWrap(t, { indent: SOFT_WRAP_INDENT }));
1009
+ }
1010
+ }
1011
+ const crossArea = countCrossAreaDependents(d.area, result.downstream);
1012
+ const crossAreaFragment = crossArea > 0 ? ` \xB7 ${String(crossArea)} cross-area dependency` : "";
1013
+ const noEdgesFragment = result.downstream.length === 0 ? " (no edges yet)" : "";
1014
+ lines.push(
1015
+ `impact: ${String(result.fanOut)} decision(s) need review${noEdgesFragment}${crossAreaFragment}.`
1016
+ );
1017
+ if (result.truncated) {
1018
+ lines.push(
1019
+ " \u26A0 blast radius truncated \u2014 more refs/dependents than the server returns per request; not all shown."
1020
+ );
1021
+ }
1022
+ return lines.join("\n");
1023
+ }
1024
+
1025
+ // src/decisions/cascade.ts
1026
+ var NOT_FOUND_RE = /not found/i;
1027
+ var CASCADE_TIMEOUT_MS = 1e4;
1028
+ var defaultDeps = { getClient };
1029
+ var CascadeNotFoundError = class extends Error {
1030
+ constructor(idOrShortId) {
1031
+ super(`Decision not found: ${idOrShortId}`);
1032
+ this.name = "CascadeNotFoundError";
1033
+ }
1034
+ };
1035
+ async function fetchCascade(idOrShortId, deps = defaultDeps) {
1036
+ const params = new URLSearchParams({ id: idOrShortId });
1037
+ const client = deps.getClient();
1038
+ try {
1039
+ return await client.get(`/api/cli/decisions/cascade?${params.toString()}`, {
1040
+ signal: AbortSignal.timeout(CASCADE_TIMEOUT_MS)
1041
+ });
1042
+ } catch (err) {
1043
+ if (err instanceof Error && NOT_FOUND_RE.test(err.message)) {
1044
+ throw new CascadeNotFoundError(idOrShortId);
1045
+ }
1046
+ throw err;
1047
+ }
1048
+ }
1049
+ function formatCascadeJson(result) {
1050
+ return JSON.stringify(result, null, 2);
1051
+ }
1052
+
1053
+ // src/decisions/recent.ts
1054
+ var RECENT_TIMEOUT_MS = 1e4;
1055
+ var defaultDeps2 = { getClient };
1056
+ async function fetchRecent(args, deps = defaultDeps2) {
1057
+ const params = new URLSearchParams();
1058
+ if (args.limit !== void 0) {
1059
+ params.set("limit", String(args.limit));
1060
+ }
1061
+ if (args.since !== void 0) {
1062
+ params.set("since", args.since);
1063
+ }
1064
+ const client = deps.getClient();
1065
+ try {
1066
+ const res = await client.get(`/api/cli/decisions/recent?${params.toString()}`, {
1067
+ signal: AbortSignal.timeout(RECENT_TIMEOUT_MS)
1068
+ });
1069
+ const result = { decisions: res.decisions };
1070
+ if (res.unavailable !== void 0) {
1071
+ result.unavailable = res.unavailable;
1072
+ }
1073
+ return result;
1074
+ } catch (err) {
1075
+ const detail = err instanceof Error ? err.message : String(err);
1076
+ return { decisions: [], unavailable: `recent feed failed: ${detail}` };
1077
+ }
1078
+ }
1079
+ var SHORT_ID_PREFIX = "dec_";
1080
+ var ZERO_PAD_TWO = 2;
1081
+ function pad2(n) {
1082
+ return n.toString().padStart(ZERO_PAD_TWO, "0");
1083
+ }
1084
+ function formatClock(ms) {
1085
+ const d = new Date(ms);
1086
+ return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
1087
+ }
1088
+ function authorLabel(row) {
1089
+ if (!row.authorIsSelf) {
1090
+ return row.authorName;
1091
+ }
1092
+ switch (row.producerKind) {
1093
+ case "claude_code":
1094
+ return "Your Claude Code";
1095
+ case "chat":
1096
+ return "Your chat";
1097
+ case "spec_edit":
1098
+ return "Your spec edit";
1099
+ case "cli":
1100
+ return "Your CLI";
1101
+ default:
1102
+ return `Your ${row.authorName}`;
1103
+ }
1104
+ }
1105
+ var AUTHOR_WIDTH = 18;
1106
+ function padRight(s, width) {
1107
+ return s.length >= width ? `${s.slice(0, width - 1)} ` : s.padEnd(width, " ");
1108
+ }
1109
+ function formatRecentHuman(result) {
1110
+ if (result.unavailable !== void 0) {
1111
+ return `[prim] recent \xB7 feed not verified \u2014 ${result.unavailable}`;
1112
+ }
1113
+ if (result.decisions.length === 0) {
1114
+ return "[prim] recent \xB7 0 decisions";
1115
+ }
1116
+ const lines = [`[prim] recent \xB7 ${String(result.decisions.length)} decision(s)`];
1117
+ for (const row of result.decisions) {
1118
+ const clock = formatClock(row.classifiedAt);
1119
+ const author = padRight(authorLabel(row), AUTHOR_WIDTH);
1120
+ const areaText = row.area ? `\u2022 ${row.area}` : "\u2022";
1121
+ const areaPlain = padRight(areaText, 12);
1122
+ const areaCol = row.area ? areaPlain.replace("\u2022", color("\u2022", colorForArea(row.area))) : areaPlain;
1123
+ lines.push(` ${clock} ${author}${areaCol}${row.intent}`);
1124
+ }
1125
+ return lines.join("\n");
1126
+ }
1127
+ function formatRecentJson(result) {
1128
+ return JSON.stringify(result, null, 2);
1129
+ }
1130
+ function renderIdentifier(row) {
1131
+ if (row.shortId) {
1132
+ return `${SHORT_ID_PREFIX}${row.shortId}`;
1133
+ }
1134
+ return row.id;
1135
+ }
1136
+
1137
+ // src/decisions/confirm.ts
1138
+ var CONFIRM_TIMEOUT_MS = 1e4;
1139
+ var defaultDeps3 = { getClient };
1140
+ var NOT_FOUND_RE2 = /not found/i;
1141
+ var AMBIGUOUS_RE = /ambiguous/i;
1142
+ var NOT_AUTHOR_RE = /author/i;
1143
+ var ConfirmNotFoundError = class extends Error {
1144
+ constructor(idOrShortId) {
1145
+ super(`Decision not found: ${idOrShortId}`);
1146
+ this.name = "ConfirmNotFoundError";
1147
+ }
1148
+ };
1149
+ async function fetchConfirm(idOrShortId, confirmed, deps = defaultDeps3) {
1150
+ const request = { idOrShortId, confirmed };
1151
+ const client = deps.getClient();
1152
+ try {
1153
+ const outcome = await client.post(
1154
+ "/api/cli/decisions/confirm",
1155
+ { id: idOrShortId, confirmed },
1156
+ { signal: AbortSignal.timeout(CONFIRM_TIMEOUT_MS) }
1157
+ );
1158
+ return { request, outcome };
1159
+ } catch (err) {
1160
+ if (err instanceof Error) {
1161
+ if (NOT_FOUND_RE2.test(err.message)) {
1162
+ throw new ConfirmNotFoundError(idOrShortId);
1163
+ }
1164
+ if (AMBIGUOUS_RE.test(err.message)) {
1165
+ return { request, outcome: { outcome: "ambiguous" } };
1166
+ }
1167
+ if (NOT_AUTHOR_RE.test(err.message)) {
1168
+ return { request, outcome: { outcome: "not_author" } };
1169
+ }
1170
+ }
1171
+ throw err;
1172
+ }
1173
+ }
1174
+ function intentWord(confirmed) {
1175
+ return confirmed ? "confirmed" : "rejected";
1176
+ }
1177
+ function formatConfirmHuman(result) {
1178
+ const { request, outcome } = result;
1179
+ switch (outcome.outcome) {
1180
+ case "confirmed":
1181
+ case "corrected":
1182
+ case "stale": {
1183
+ const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
1184
+ if (outcome.outcome === "stale") {
1185
+ return `[prim] ${id} ${intentWord(request.confirmed)} \u2014 the prompt had gone stale; recorded against the current decision.`;
1186
+ }
1187
+ if (outcome.outcome === "corrected") {
1188
+ return `[prim] ${id} ${intentWord(request.confirmed)} with a correction.`;
1189
+ }
1190
+ return `[prim] ${id} ${intentWord(request.confirmed)}.`;
1191
+ }
1192
+ case "already_responded": {
1193
+ const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
1194
+ const priorWord = outcome.confirmed === void 0 ? "already answered" : `already ${intentWord(outcome.confirmed)}`;
1195
+ const when = new Date(outcome.respondedAt).toISOString();
1196
+ return `[prim] ${id} was ${priorWord} (responded ${when}); nothing to change.`;
1197
+ }
1198
+ case "no_pending_prompt": {
1199
+ const id = renderIdentifier({ shortId: outcome.shortId, id: outcome.decisionId });
1200
+ return `[prim] ${id} has no pending confirmation request \u2014 nothing to acknowledge.`;
1201
+ }
1202
+ case "ambiguous":
1203
+ return `[prim] shortId "${request.idOrShortId}" is ambiguous in this organization \u2014 retry with the full decision id.`;
1204
+ default:
1205
+ return "[prim] only the decision's author can respond to its confirmation prompt.";
1206
+ }
1207
+ }
1208
+ function formatConfirmJson(result) {
1209
+ return JSON.stringify(result.outcome, null, 2);
1210
+ }
1211
+
1212
+ // src/decisions/show.ts
1213
+ var NOT_FOUND_RE3 = /not found/i;
1214
+ function colorStatus(status) {
1215
+ if (status === "under_review") {
1216
+ return color(status, "orange");
1217
+ }
1218
+ if (status === "active") {
1219
+ return color(status, "green");
1220
+ }
1221
+ return color(status, "gray");
1222
+ }
1223
+ var SHOW_TIMEOUT_MS = 1e4;
1224
+ var defaultDeps4 = { getClient };
1225
+ var DecisionNotFoundError = class extends Error {
1226
+ constructor(idOrShortId) {
1227
+ super(`Decision not found: ${idOrShortId}`);
1228
+ this.name = "DecisionNotFoundError";
1229
+ }
1230
+ };
1231
+ async function fetchShow(idOrShortId, deps = defaultDeps4) {
1232
+ const params = new URLSearchParams({ id: idOrShortId });
1233
+ const client = deps.getClient();
1234
+ try {
1235
+ return await client.get(`/api/cli/decisions/show?${params.toString()}`, {
1236
+ signal: AbortSignal.timeout(SHOW_TIMEOUT_MS)
1237
+ });
1238
+ } catch (err) {
1239
+ if (err instanceof Error && NOT_FOUND_RE3.test(err.message)) {
1240
+ throw new DecisionNotFoundError(idOrShortId);
1241
+ }
1242
+ throw err;
1243
+ }
1244
+ }
1245
+ var GATED_FLAG_KINDS = /* @__PURE__ */ new Set(["file_edit", "supersession", "context_edit"]);
1246
+ function describeFlag(flag) {
1247
+ const detail = flag.reason ? ` \u2014 ${flag.reason}` : "";
1248
+ if (flag.acknowledgedAt !== void 0) {
1249
+ return `acknowledged ${flag.type}${detail}`;
1250
+ }
1251
+ if (flag.type === "confirmation_request") {
1252
+ return `pending confirmation request${detail}`;
1253
+ }
1254
+ if (GATED_FLAG_KINDS.has(flag.type)) {
1255
+ const verdict = flag.gateVerdict ?? "unknown";
1256
+ return `pending ${flag.type} (verdict: ${verdict})${detail}`;
1257
+ }
1258
+ return `pending ${flag.type}${detail}`;
1259
+ }
1260
+ function describeNode(node) {
1261
+ const id = renderIdentifier({ shortId: node.shortId, id: node.id });
1262
+ const area = node.area ? ` \u2022 ${node.area}` : "";
1263
+ return `${id}${area} ${node.intent} (${node.authorName})`;
1264
+ }
1265
+ function pushFiles(lines, files) {
1266
+ if (files.length === 0) {
1267
+ return;
1268
+ }
1269
+ lines.push(` files (${String(files.length)}):`);
1270
+ for (const file of files) {
1271
+ lines.push(` - ${file}`);
1272
+ }
1273
+ }
1274
+ function pushContexts(lines, contexts) {
1275
+ if (contexts.length === 0) {
1276
+ return;
1277
+ }
1278
+ lines.push(` contexts (${String(contexts.length)}):`);
1279
+ for (const ctx of contexts) {
1280
+ lines.push(` - ${ctx.name}`);
1281
+ }
1282
+ }
1283
+ function pushEdges(lines, label, arrow, nodes) {
1284
+ if (nodes.length === 0) {
1285
+ return;
1286
+ }
1287
+ lines.push(` ${label} (${String(nodes.length)}):`);
1288
+ for (const node of nodes) {
1289
+ lines.push(` ${arrow} ${describeNode(node)}`);
1290
+ }
1291
+ }
1292
+ function formatShowHuman(result) {
1293
+ const d = result.decision;
1294
+ const id = color(renderIdentifier({ shortId: d.shortId, id: d.id }), "orange");
1295
+ const confidence = d.confidence ?? "(unset)";
1296
+ const lines = [
1297
+ `[prim] ${id} \u2014 ${d.intent}`,
1298
+ ` status: ${colorStatus(d.status)}${d.confirmed ? " (confirmed)" : ""} \xB7 confidence: ${confidence} \xB7 reversibility: ${d.reversibility ?? "(unset)"}`
1299
+ ];
1300
+ if (d.supersededBy) {
1301
+ lines.push(` superseded by: ${d.supersededBy}`);
1302
+ }
1303
+ if (d.area) {
1304
+ lines.push(` area: ${color(d.area, colorForArea(d.area))}`);
1305
+ }
1306
+ if (typeof d.fanOut === "number") {
1307
+ lines.push(` fan-out: ${String(d.fanOut)}`);
1308
+ }
1309
+ if (d.respondedAt !== void 0) {
1310
+ lines.push(` responded at: ${new Date(d.respondedAt).toISOString()}`);
1311
+ }
1312
+ if (d.rationale) {
1313
+ lines.push(` rationale: ${d.rationale}`);
1314
+ }
1315
+ if (d.decided && d.decided.length > 0) {
1316
+ lines.push(` decided (${String(d.decided.length)}):`);
1317
+ for (const point of d.decided) {
1318
+ lines.push(` - ${point}`);
1319
+ }
1320
+ }
1321
+ if (d.alternatives.length > 0) {
1322
+ lines.push(` alternatives: ${d.alternatives.join(" | ")}`);
1323
+ }
1324
+ pushFiles(lines, result.files);
1325
+ pushContexts(lines, result.contexts);
1326
+ pushEdges(lines, "dependents", "\u2192", result.dependents);
1327
+ pushEdges(lines, "depends on", "\u2190", result.dependsOn);
1328
+ if (result.flags.length > 0) {
1329
+ lines.push(` flags (${String(result.flags.length)}):`);
1330
+ for (const flag of result.flags) {
1331
+ lines.push(` \xB7 ${describeFlag(flag)}`);
1332
+ }
1333
+ }
1334
+ if (result.truncated) {
1335
+ lines.push(" (partial \u2014 some related rows were truncated by a join cap)");
1336
+ }
1337
+ return lines.join("\n");
1338
+ }
1339
+ function formatShowJson(result) {
1340
+ return JSON.stringify(result, null, 2);
1341
+ }
1342
+
1343
+ // src/commands/decisions.ts
1344
+ var EXIT_NOT_FOUND = 4;
1345
+ function registerDecisionsCommands(program2) {
1346
+ const decisions = program2.command("decisions").description("Inspect the project Decision Graph");
1347
+ decisions.command("check").description("Look up active decisions that reference one or more file paths").requiredOption(
1348
+ "--files <files>",
1349
+ "Comma-separated file paths to check against the Decision Graph"
1350
+ ).action(async (opts) => {
1351
+ const filePaths = opts.files.split(",").map((s) => s.trim()).filter(Boolean);
1352
+ const result = await checkAffectedDecisions(filePaths);
1353
+ const warning = formatDecisionsWarning(result);
1354
+ if (warning) {
1355
+ console.error(warning);
1356
+ }
1357
+ printJson(result);
1358
+ });
1359
+ decisions.command("recent").description("Show the team-wide chronological decision feed").option("--limit <n>", "Maximum number of rows to return (default 10)").option(
1360
+ "--since <duration>",
1361
+ "Lookback window \u2014 accepts `Nm`, `Nh`, `Nd` (minutes / hours / days) or absolute epoch ms"
1362
+ ).action(async (opts) => {
1363
+ const result = await fetchRecent({
1364
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : void 0,
1365
+ since: opts.since
1366
+ });
1367
+ console.error(formatRecentHuman(result));
1368
+ console.log(formatRecentJson(result));
1369
+ });
1370
+ decisions.command("show <idOrShortId>").description("Show full detail for one decision (intent, rationale, flags, refs, edges)").action(async (idOrShortId) => {
1371
+ try {
1372
+ const result = await fetchShow(idOrShortId);
1373
+ console.error(formatShowHuman(result));
1374
+ console.log(formatShowJson(result));
1375
+ } catch (err) {
1376
+ if (err instanceof DecisionNotFoundError) {
1377
+ console.error(`[prim] ${err.message}`);
1378
+ process.exitCode = EXIT_NOT_FOUND;
1379
+ return;
1380
+ }
1381
+ throw err;
1382
+ }
1383
+ });
1384
+ decisions.command("cascade <idOrShortId>").description("Render the local cascade subgraph (upstream knowledge + downstream dependents)").action(async (idOrShortId) => {
1385
+ try {
1386
+ const result = await fetchCascade(idOrShortId);
1387
+ console.error(renderCascade(result));
1388
+ console.log(formatCascadeJson(result));
1389
+ } catch (err) {
1390
+ if (err instanceof CascadeNotFoundError) {
1391
+ console.error(`[prim] ${err.message}`);
1392
+ process.exitCode = EXIT_NOT_FOUND;
1393
+ return;
1394
+ }
1395
+ throw err;
1396
+ }
1397
+ });
1398
+ 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) => {
1399
+ const confirmed = !opts.reject;
1400
+ try {
1401
+ const result = await fetchConfirm(idOrShortId, confirmed);
1402
+ console.error(formatConfirmHuman(result));
1403
+ console.log(formatConfirmJson(result));
1404
+ } catch (err) {
1405
+ if (err instanceof ConfirmNotFoundError) {
1406
+ console.error(`[prim] ${err.message}`);
1407
+ process.exitCode = EXIT_NOT_FOUND;
1408
+ return;
1409
+ }
1410
+ throw err;
1411
+ }
1412
+ });
1413
+ }
1414
+
376
1415
  // src/commands/hooks.ts
377
1416
  import { execSync } from "child_process";
378
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
1417
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
379
1418
  import { resolve } from "path";
380
1419
  import { Option } from "commander";
381
1420
  var HOOK_SCRIPT = `#!/bin/sh
@@ -409,13 +1448,13 @@ function getGitRoot() {
409
1448
  }
410
1449
  function detectHusky(gitRoot) {
411
1450
  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;
1451
+ if (!existsSync4(huskyDir)) return false;
1452
+ if (existsSync4(resolve(huskyDir, "_"))) return true;
1453
+ if (existsSync4(resolve(huskyDir, "pre-commit"))) return true;
415
1454
  const pkgPath = resolve(gitRoot, "package.json");
416
- if (existsSync2(pkgPath)) {
1455
+ if (existsSync4(pkgPath)) {
417
1456
  try {
418
- const pkg2 = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1457
+ const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
419
1458
  const scripts = pkg2.scripts ?? {};
420
1459
  if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
421
1460
  return true;
@@ -442,20 +1481,20 @@ async function askConfirmation(question) {
442
1481
  }
443
1482
  function installToHusky(gitRoot) {
444
1483
  const hookPath = resolve(gitRoot, ".husky", "pre-commit");
445
- if (existsSync2(hookPath)) {
446
- const existing = readFileSync3(hookPath, "utf-8");
1484
+ if (existsSync4(hookPath)) {
1485
+ const existing = readFileSync5(hookPath, "utf-8");
447
1486
  if (containsPrimHook(existing)) {
448
1487
  console.log("Prim pre-commit hook is already installed in .husky/pre-commit.");
449
1488
  return;
450
1489
  }
451
1490
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
452
- writeFileSync2(hookPath, `${existing}${separator}${PRIM_HUSKY_BLOCK}
1491
+ writeFileSync3(hookPath, `${existing}${separator}${PRIM_HUSKY_BLOCK}
453
1492
  `, {
454
1493
  mode: 493
455
1494
  });
456
1495
  console.log("Appended prim hook block to .husky/pre-commit.");
457
1496
  } else {
458
- writeFileSync2(hookPath, `#!/bin/sh
1497
+ writeFileSync3(hookPath, `#!/bin/sh
459
1498
 
460
1499
  ${PRIM_HUSKY_BLOCK}
461
1500
  `, {
@@ -467,11 +1506,11 @@ ${PRIM_HUSKY_BLOCK}
467
1506
  function installToDotGit(gitRoot) {
468
1507
  const hooksDir = resolve(gitRoot, ".git", "hooks");
469
1508
  const hookPath = resolve(hooksDir, "pre-commit");
470
- if (!existsSync2(hooksDir)) {
471
- mkdirSync2(hooksDir, { recursive: true });
1509
+ if (!existsSync4(hooksDir)) {
1510
+ mkdirSync3(hooksDir, { recursive: true });
472
1511
  }
473
- if (existsSync2(hookPath)) {
474
- const existing = readFileSync3(hookPath, "utf-8");
1512
+ if (existsSync4(hookPath)) {
1513
+ const existing = readFileSync5(hookPath, "utf-8");
475
1514
  if (containsPrimHook(existing)) {
476
1515
  console.log("Prim pre-commit hook is already installed at .git/hooks/pre-commit.");
477
1516
  return;
@@ -480,7 +1519,7 @@ function installToDotGit(gitRoot) {
480
1519
  console.log("To replace it, run: prim hooks uninstall && prim hooks install");
481
1520
  return;
482
1521
  }
483
- writeFileSync2(hookPath, HOOK_SCRIPT, { mode: 493 });
1522
+ writeFileSync3(hookPath, HOOK_SCRIPT, { mode: 493 });
484
1523
  console.log(`Installed pre-commit hook at ${hookPath}`);
485
1524
  }
486
1525
  function registerHooksCommands(program2) {
@@ -522,15 +1561,147 @@ function registerHooksCommands(program2) {
522
1561
  hooks.command("uninstall").description("Remove the prim pre-commit hook").action(() => {
523
1562
  const gitRoot = getGitRoot();
524
1563
  const hookPath = resolve(gitRoot, ".git", "hooks", "pre-commit");
525
- if (!existsSync2(hookPath)) {
1564
+ if (!existsSync4(hookPath)) {
526
1565
  console.log("No pre-commit hook found.");
527
1566
  return;
528
1567
  }
529
- unlinkSync(hookPath);
1568
+ unlinkSync2(hookPath);
530
1569
  console.log(`Removed pre-commit hook at ${hookPath}`);
531
1570
  });
532
1571
  }
533
1572
 
1573
+ // src/commands/moves.ts
1574
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
1575
+ import { join as join3 } from "path";
1576
+
1577
+ // src/flusher.ts
1578
+ import { renameSync as renameSync2, unlinkSync as unlinkSync3 } from "fs";
1579
+ var BATCH_SIZE = 500;
1580
+ var HTTP_TIMEOUT_MS = 1e4;
1581
+ var OPPORTUNISTIC_FLUSH_AFTER_MS = 6e4;
1582
+ async function drainPath(path) {
1583
+ const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
1584
+ try {
1585
+ renameSync2(path, tmpPath);
1586
+ } catch (err) {
1587
+ if (err.code === "ENOENT") {
1588
+ return 0;
1589
+ }
1590
+ throw err;
1591
+ }
1592
+ const moves = readMovesFromPath(tmpPath);
1593
+ if (moves.length === 0) {
1594
+ unlinkSync3(tmpPath);
1595
+ return 0;
1596
+ }
1597
+ const client = getClient();
1598
+ for (let i = 0; i < moves.length; i += BATCH_SIZE) {
1599
+ const batch = moves.slice(i, i + BATCH_SIZE);
1600
+ await client.post(
1601
+ "/api/cli/moves/ingest",
1602
+ { batch },
1603
+ { signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) }
1604
+ );
1605
+ }
1606
+ unlinkSync3(tmpPath);
1607
+ return moves.length;
1608
+ }
1609
+ async function flush() {
1610
+ let total = 0;
1611
+ for (const { path } of listBuckets()) {
1612
+ total += await drainPath(path);
1613
+ }
1614
+ return { flushed: total };
1615
+ }
1616
+ async function flushIfNeeded() {
1617
+ try {
1618
+ const stats = bucketStats();
1619
+ if (stats.length === 0) {
1620
+ return;
1621
+ }
1622
+ const oldest = stats.reduce((min, s) => s.mtimeMs < min ? s.mtimeMs : min, stats[0].mtimeMs);
1623
+ if (Date.now() - oldest > OPPORTUNISTIC_FLUSH_AFTER_MS) {
1624
+ await flush();
1625
+ }
1626
+ } catch {
1627
+ }
1628
+ }
1629
+
1630
+ // src/commands/moves.ts
1631
+ var MS_PER_SECOND = 1e3;
1632
+ var DEFAULT_TAIL_LINES = "20";
1633
+ var RADIX_DECIMAL = 10;
1634
+ var ID_PREFIX_LEN = 8;
1635
+ var EVENT_COL_WIDTH = 20;
1636
+ var BUCKET_COL_WIDTH = 20;
1637
+ var TAIL_BUCKET_COL_WIDTH = 12;
1638
+ var DIR_MODE = 448;
1639
+ var FILE_MODE2 = 384;
1640
+ var WORKSPACE_FILE = ".prim/workspace.json";
1641
+ function registerMovesCommands(program2) {
1642
+ const moves = program2.command("moves").description("Decision Event Pipeline \u2014 local journal");
1643
+ moves.command("flush").description("Drain all local move journals to the server").action(async () => {
1644
+ const { flushed } = await flush();
1645
+ console.log(`[prim] flushed ${String(flushed)} move${flushed === 1 ? "" : "s"}`);
1646
+ });
1647
+ moves.command("status").description("Show per-bucket pending stats").action(() => {
1648
+ const stats = bucketStats();
1649
+ if (stats.length === 0) {
1650
+ console.log("[prim] journal: empty");
1651
+ return;
1652
+ }
1653
+ console.log(`[prim] root: ${JOURNAL_DIR}`);
1654
+ for (const s of stats) {
1655
+ const ageS = Math.round((Date.now() - s.mtimeMs) / MS_PER_SECOND);
1656
+ console.log(
1657
+ ` ${s.bucket.padEnd(BUCKET_COL_WIDTH)} ${String(s.lineCount).padStart(5)} pending, ${String(s.sizeBytes).padStart(8)} bytes, last write ${String(ageS)}s ago`
1658
+ );
1659
+ }
1660
+ });
1661
+ 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) => {
1662
+ const lines = Number.parseInt(opts.lines, RADIX_DECIMAL);
1663
+ if (!Number.isInteger(lines) || lines < 1) {
1664
+ console.error("[prim] --lines must be a positive integer");
1665
+ process.exitCode = 1;
1666
+ return;
1667
+ }
1668
+ const all = bucketStats().flatMap((s) => readMovesFromPath(s.path).map((m) => ({ bucket: s.bucket, move: m }))).sort((a, b) => a.move.capturedAt - b.move.capturedAt);
1669
+ if (all.length === 0) {
1670
+ console.log("[prim] journal: empty");
1671
+ return;
1672
+ }
1673
+ const tail = all.slice(-lines);
1674
+ for (const { bucket, move: m } of tail) {
1675
+ const t = new Date(m.capturedAt).toISOString();
1676
+ const session = m.sessionId.slice(0, ID_PREFIX_LEN) || "anon";
1677
+ const move = m.moveId.slice(0, ID_PREFIX_LEN);
1678
+ console.log(
1679
+ `${t} ${m.eventType.padEnd(EVENT_COL_WIDTH)} bucket=${bucket.padEnd(TAIL_BUCKET_COL_WIDTH)} session=${session} move=${move}`
1680
+ );
1681
+ }
1682
+ });
1683
+ moves.command("bind").description("Pin the current directory to an org via .prim/workspace.json").requiredOption("--orgId <orgId>", "Convex organization id").action((opts) => {
1684
+ const dir = join3(process.cwd(), ".prim");
1685
+ if (!existsSync5(dir)) {
1686
+ mkdirSync4(dir, { recursive: true, mode: DIR_MODE });
1687
+ }
1688
+ const file = join3(process.cwd(), WORKSPACE_FILE);
1689
+ writeFileSync4(file, JSON.stringify({ orgId: opts.orgId, boundAt: Date.now() }, null, 2), {
1690
+ mode: FILE_MODE2
1691
+ });
1692
+ console.log(`[prim] bound ${process.cwd()} to org ${opts.orgId}`);
1693
+ });
1694
+ moves.command("drop").description("Remove the .prim/workspace.json binding from the cwd").action(() => {
1695
+ const file = join3(process.cwd(), WORKSPACE_FILE);
1696
+ if (!existsSync5(file)) {
1697
+ console.log("[prim] no workspace binding in cwd");
1698
+ return;
1699
+ }
1700
+ unlinkSync4(file);
1701
+ console.log(`[prim] dropped workspace binding from ${process.cwd()}`);
1702
+ });
1703
+ }
1704
+
534
1705
  // src/commands/project.ts
535
1706
  function registerProjectCommands(program2) {
536
1707
  const project = program2.command("project").description("Manage projects");
@@ -553,20 +1724,165 @@ function registerProjectCommands(program2) {
553
1724
  });
554
1725
  }
555
1726
 
1727
+ // src/commands/reconcile.ts
1728
+ var EXIT_OK = 0;
1729
+ var EXIT_USAGE = 2;
1730
+ var EXIT_SERVER = 3;
1731
+ var HTTP_CLIENT_ERROR_MIN = 400;
1732
+ var HTTP_SERVER_ERROR_MIN = 500;
1733
+ function isOk(value) {
1734
+ if (typeof value !== "object" || value === null) {
1735
+ return false;
1736
+ }
1737
+ const v = value;
1738
+ return v.ok === true && typeof v.bypassId === "string" && typeof v.expiresAt === "number";
1739
+ }
1740
+ function formatExpiresIn(expiresAt) {
1741
+ const remainingMs = expiresAt - Date.now();
1742
+ if (remainingMs <= 0) {
1743
+ return "expired";
1744
+ }
1745
+ const SECONDS_PER_MINUTE = 60;
1746
+ const minutes = Math.floor(remainingMs / (SECONDS_PER_MINUTE * 1e3));
1747
+ const seconds = Math.floor(remainingMs / 1e3 % SECONDS_PER_MINUTE);
1748
+ if (minutes === 0) {
1749
+ return `${seconds}s`;
1750
+ }
1751
+ return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
1752
+ }
1753
+ function renderDecisionIdentifier(short, id) {
1754
+ return short ? `dec_${short}` : id;
1755
+ }
1756
+ function isDomainRejection(err) {
1757
+ return err instanceof HttpError && err.status >= HTTP_CLIENT_ERROR_MIN && err.status < HTTP_SERVER_ERROR_MIN;
1758
+ }
1759
+ async function performReconcile(idOrShortId, opts = {}) {
1760
+ const client = getClient();
1761
+ const body = { idOrShortId };
1762
+ if (opts.flag) {
1763
+ body.conflictFlagId = opts.flag;
1764
+ }
1765
+ let response;
1766
+ try {
1767
+ response = await client.post("/api/cli/reconcile/issue", body);
1768
+ } catch (err) {
1769
+ if (isDomainRejection(err)) {
1770
+ process.stderr.write(`[prim] reconcile rejected: ${err.message}
1771
+ `);
1772
+ console.log(JSON.stringify({ ok: false, status: err.status, error: err.message }, null, 2));
1773
+ process.exitCode = EXIT_USAGE;
1774
+ return;
1775
+ }
1776
+ const message = err instanceof Error ? err.message : String(err);
1777
+ process.stderr.write(`[prim] reconcile failed: ${message}
1778
+ `);
1779
+ console.log(JSON.stringify({ ok: false, error: message }, null, 2));
1780
+ process.exitCode = EXIT_SERVER;
1781
+ return;
1782
+ }
1783
+ if (isOk(response)) {
1784
+ const ident = renderDecisionIdentifier(response.decisionShortId, response.decisionId);
1785
+ const verb = response.reissued ? "reissued" : "issued";
1786
+ process.stderr.write(
1787
+ `[prim] reconcile bypass ${verb} for ${ident} (expires in ${formatExpiresIn(response.expiresAt)})
1788
+ `
1789
+ );
1790
+ console.log(JSON.stringify(response, null, 2));
1791
+ process.exitCode = EXIT_OK;
1792
+ return;
1793
+ }
1794
+ process.stderr.write("[prim] reconcile: malformed server response\n");
1795
+ console.log(JSON.stringify({ ok: false, response }, null, 2));
1796
+ process.exitCode = EXIT_SERVER;
1797
+ }
1798
+ function registerReconcileCommands(program2) {
1799
+ program2.command("reconcile <idOrShortId>").description(
1800
+ "Issue a single-use bypass for a flagged decision (used by the cooperative reconcile loop)"
1801
+ ).option(
1802
+ "--flag <conflictFlagId>",
1803
+ "Specific flag id to bind the bypass to (default: the decision's latest unack'd flag)"
1804
+ ).option("--json", "(reserved; STDOUT is always JSON)").action(async (idOrShortId, opts) => {
1805
+ await performReconcile(idOrShortId, opts);
1806
+ });
1807
+ }
1808
+
1809
+ // src/commands/session.ts
1810
+ import {
1811
+ existsSync as existsSync6,
1812
+ mkdirSync as mkdirSync5,
1813
+ readFileSync as readFileSync6,
1814
+ readdirSync,
1815
+ unlinkSync as unlinkSync5,
1816
+ writeFileSync as writeFileSync5
1817
+ } from "fs";
1818
+ import { join as join4 } from "path";
1819
+ var DIR_MODE2 = 448;
1820
+ var FILE_MODE3 = 384;
1821
+ function ensureDir() {
1822
+ if (!existsSync6(SESSIONS_DIR)) {
1823
+ mkdirSync5(SESSIONS_DIR, { recursive: true, mode: DIR_MODE2 });
1824
+ }
1825
+ }
1826
+ function markerPath(sessionId) {
1827
+ return join4(SESSIONS_DIR, `${sessionId}.json`);
1828
+ }
1829
+ function registerSessionCommands(program2) {
1830
+ const session = program2.command("session").description("Decision Event Pipeline \u2014 session binding markers");
1831
+ session.command("start <sessionId>").description("Pin a Claude Code session to an org").requiredOption("--orgId <orgId>", "Convex organization id").action((sessionId, opts) => {
1832
+ ensureDir();
1833
+ const marker = {
1834
+ orgId: opts.orgId,
1835
+ startedAt: Date.now()
1836
+ };
1837
+ writeFileSync5(markerPath(sessionId), JSON.stringify(marker, null, 2), {
1838
+ mode: FILE_MODE3
1839
+ });
1840
+ console.log(`[prim] session ${sessionId} bound to org ${opts.orgId}`);
1841
+ });
1842
+ session.command("list").description("List active session markers").action(() => {
1843
+ if (!existsSync6(SESSIONS_DIR)) {
1844
+ console.log("[prim] no session markers");
1845
+ return;
1846
+ }
1847
+ const files = readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
1848
+ if (files.length === 0) {
1849
+ console.log("[prim] no session markers");
1850
+ return;
1851
+ }
1852
+ for (const f of files) {
1853
+ const sessionId = f.replace(/\.json$/, "");
1854
+ try {
1855
+ const m = JSON.parse(readFileSync6(join4(SESSIONS_DIR, f), "utf-8"));
1856
+ console.log(`${sessionId} org=${m.orgId}`);
1857
+ } catch {
1858
+ }
1859
+ }
1860
+ });
1861
+ session.command("drop <sessionId>").description("Remove a session marker").action((sessionId) => {
1862
+ const p = markerPath(sessionId);
1863
+ if (!existsSync6(p)) {
1864
+ console.log(`[prim] no marker for session ${sessionId}`);
1865
+ return;
1866
+ }
1867
+ unlinkSync5(p);
1868
+ console.log(`[prim] dropped session marker ${sessionId}`);
1869
+ });
1870
+ }
1871
+
556
1872
  // src/commands/skill.ts
557
1873
  import {
558
- closeSync,
559
- existsSync as existsSync3,
560
- fsyncSync,
561
- openSync,
562
- readFileSync as readFileSync4,
563
- renameSync,
564
- writeFileSync as writeFileSync3
1874
+ closeSync as closeSync2,
1875
+ existsSync as existsSync7,
1876
+ fsyncSync as fsyncSync2,
1877
+ openSync as openSync2,
1878
+ readFileSync as readFileSync7,
1879
+ renameSync as renameSync3,
1880
+ writeFileSync as writeFileSync6
565
1881
  } from "fs";
566
- import { dirname as dirname2, resolve as resolve2 } from "path";
1882
+ import { dirname as dirname3, resolve as resolve2 } from "path";
567
1883
  import { fileURLToPath } from "url";
568
1884
  import { createPatch } from "diff";
569
- var __dirname = dirname2(fileURLToPath(import.meta.url));
1885
+ var __dirname = dirname3(fileURLToPath(import.meta.url));
570
1886
  var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
571
1887
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
572
1888
  var TARGET_CANDIDATES = [
@@ -578,15 +1894,15 @@ var TARGET_CANDIDATES = [
578
1894
  var DEFAULT_TARGET = "CLAUDE.md";
579
1895
  function loadSkill() {
580
1896
  let dir = __dirname;
581
- while (dir !== dirname2(dir)) {
1897
+ while (dir !== dirname3(dir)) {
582
1898
  const p = resolve2(dir, "SKILL.md");
583
- if (existsSync3(p)) return readFileSync4(p, "utf-8");
584
- dir = dirname2(dir);
1899
+ if (existsSync7(p)) return readFileSync7(p, "utf-8");
1900
+ dir = dirname3(dir);
585
1901
  }
586
1902
  throw new Error("SKILL.md not found in package");
587
1903
  }
588
1904
  function detectTargets(cwd) {
589
- return TARGET_CANDIDATES.filter((p) => existsSync3(resolve2(cwd, p)));
1905
+ return TARGET_CANDIDATES.filter((p) => existsSync7(resolve2(cwd, p)));
590
1906
  }
591
1907
  function detectNewline(content) {
592
1908
  return content.includes("\r\n") ? "\r\n" : "\n";
@@ -612,16 +1928,16 @@ function removeBlock(existing) {
612
1928
  const out = existing.slice(0, b) + existing.slice(e + SKILL_END.length);
613
1929
  return out.replace(/(\r?\n){2,}$/, "$1");
614
1930
  }
615
- function atomicWrite(target, content) {
1931
+ function atomicWrite2(target, content) {
616
1932
  const tmp = `${target}.tmp`;
617
- writeFileSync3(tmp, content);
618
- const fd = openSync(tmp, "r+");
1933
+ writeFileSync6(tmp, content);
1934
+ const fd = openSync2(tmp, "r+");
619
1935
  try {
620
- fsyncSync(fd);
1936
+ fsyncSync2(fd);
621
1937
  } finally {
622
- closeSync(fd);
1938
+ closeSync2(fd);
623
1939
  }
624
- renameSync(tmp, target);
1940
+ renameSync3(tmp, target);
625
1941
  }
626
1942
  function resolveTarget(cwd, override) {
627
1943
  if (override) return resolve2(cwd, override);
@@ -635,7 +1951,7 @@ function resolveTarget(cwd, override) {
635
1951
  function runInstall(cwd, opts) {
636
1952
  const target = resolveTarget(cwd, opts.target);
637
1953
  if (target === null) return 1;
638
- const existing = existsSync3(target) ? readFileSync4(target, "utf-8") : "";
1954
+ const existing = existsSync7(target) ? readFileSync7(target, "utf-8") : "";
639
1955
  const eol = existing ? detectNewline(existing) : "\n";
640
1956
  const block = composeBlock(loadSkill(), eol);
641
1957
  const next = applyBlock(existing, block, eol);
@@ -647,34 +1963,34 @@ function runInstall(cwd, opts) {
647
1963
  process.stdout.write(createPatch(target, existing, next, "current", "proposed"));
648
1964
  return 0;
649
1965
  }
650
- atomicWrite(target, next);
1966
+ atomicWrite2(target, next);
651
1967
  console.log(`Wrote ${Buffer.byteLength(next)} bytes to ${target}`);
652
1968
  return 0;
653
1969
  }
654
1970
  function runUninstall(cwd, opts) {
655
1971
  const target = resolveTarget(cwd, opts.target);
656
1972
  if (target === null) return 1;
657
- if (!existsSync3(target)) {
1973
+ if (!existsSync7(target)) {
658
1974
  console.log(`Skill block not present at ${target}`);
659
1975
  return 0;
660
1976
  }
661
- const existing = readFileSync4(target, "utf-8");
1977
+ const existing = readFileSync7(target, "utf-8");
662
1978
  const next = removeBlock(existing);
663
1979
  if (next === null) {
664
1980
  console.log(`Skill block not present at ${target}`);
665
1981
  return 0;
666
1982
  }
667
- atomicWrite(target, next);
1983
+ atomicWrite2(target, next);
668
1984
  console.log(`Removed skill block from ${target}`);
669
1985
  return 0;
670
1986
  }
671
1987
  function runStatus(cwd, opts) {
672
1988
  const target = resolveTarget(cwd, opts.target);
673
1989
  if (target === null) return 1;
674
- const fileExists = existsSync3(target);
1990
+ const fileExists = existsSync7(target);
675
1991
  let installed = false;
676
1992
  if (fileExists) {
677
- const content = readFileSync4(target, "utf-8");
1993
+ const content = readFileSync7(target, "utf-8");
678
1994
  installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
679
1995
  }
680
1996
  if (opts.json) {
@@ -711,7 +2027,7 @@ function registerSkillCommands(program2) {
711
2027
  }
712
2028
 
713
2029
  // src/commands/spec.ts
714
- import { readFileSync as readFileSync5 } from "fs";
2030
+ import { readFileSync as readFileSync8 } from "fs";
715
2031
  function registerSpecCommands(program2) {
716
2032
  const spec = program2.command("spec").description("Manage spec documents");
717
2033
  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 +2081,7 @@ ${contexts.length} spec(s)`);
765
2081
  const client = getClient();
766
2082
  let text = opts.text;
767
2083
  if (opts.file) {
768
- text = readFileSync5(opts.file, "utf-8");
2084
+ text = readFileSync8(opts.file, "utf-8");
769
2085
  }
770
2086
  const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
771
2087
  let linkedBranch;
@@ -806,7 +2122,7 @@ ${contexts.length} spec(s)`);
806
2122
  const client = getClient();
807
2123
  let text = opts.text;
808
2124
  if (opts.file) {
809
- text = readFileSync5(opts.file, "utf-8");
2125
+ text = readFileSync8(opts.file, "utf-8");
810
2126
  }
811
2127
  if (!(text || opts.name)) {
812
2128
  console.error("Provide --text, --file, or --name to update.");
@@ -976,9 +2292,64 @@ ${preview}`);
976
2292
  }
977
2293
  }
978
2294
 
2295
+ // src/commands/statusline.ts
2296
+ import { readFileSync as readFileSync9 } from "fs";
2297
+ import { dirname as dirname4, resolve as resolve3 } from "path";
2298
+ import { fileURLToPath as fileURLToPath2 } from "url";
2299
+ var STATUSLINE_TIMEOUT_MS = 200;
2300
+ function readPackageVersion() {
2301
+ try {
2302
+ const here = dirname4(fileURLToPath2(import.meta.url));
2303
+ const candidates = [resolve3(here, "../../package.json"), resolve3(here, "../package.json")];
2304
+ for (const path of candidates) {
2305
+ try {
2306
+ const pkg2 = JSON.parse(readFileSync9(path, "utf-8"));
2307
+ if (pkg2.version) {
2308
+ return pkg2.version;
2309
+ }
2310
+ } catch {
2311
+ }
2312
+ }
2313
+ } catch {
2314
+ }
2315
+ return "0.0.0";
2316
+ }
2317
+ function debug(msg) {
2318
+ if (process.env.PRIM_STATUSLINE_DEBUG === "1") {
2319
+ process.stderr.write(`[prim-statusline] ${msg}
2320
+ `);
2321
+ }
2322
+ }
2323
+ async function renderStatusline() {
2324
+ const version = readPackageVersion();
2325
+ const snapshot = await daemonRequest(
2326
+ "status_snapshot",
2327
+ {},
2328
+ { timeoutMs: STATUSLINE_TIMEOUT_MS }
2329
+ );
2330
+ if (!snapshot) {
2331
+ debug("daemon snapshot missing");
2332
+ return `primitive ${version} (daemon: down)`;
2333
+ }
2334
+ if (snapshot.presenceStale) {
2335
+ return `primitive ${version} (daemon: live \xB7 presence: stale)`;
2336
+ }
2337
+ const team = typeof snapshot.onlineCount === "number" ? `team: ${String(snapshot.onlineCount)} online` : "team: \u2014";
2338
+ return `primitive ${version} (daemon: live \xB7 ${team})`;
2339
+ }
2340
+ function registerStatuslineCommands(program2) {
2341
+ program2.command("statusline").description("Render the Claude Code statusLine for the prim companion daemon").action(async () => {
2342
+ try {
2343
+ const line = await renderStatusline();
2344
+ process.stdout.write(line);
2345
+ } catch {
2346
+ }
2347
+ });
2348
+ }
2349
+
979
2350
  // src/index.ts
980
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
981
- var pkg = JSON.parse(readFileSync6(resolve3(__dirname2, "../package.json"), "utf-8"));
2351
+ var __dirname2 = dirname5(fileURLToPath3(import.meta.url));
2352
+ var pkg = JSON.parse(readFileSync10(resolve4(__dirname2, "../package.json"), "utf-8"));
982
2353
  updateNotifier({ pkg }).notify();
983
2354
  var program = new Command();
984
2355
  program.name("prim").description("CLI for managing Primitive specs and contexts").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
@@ -991,9 +2362,22 @@ registerSpecCommands(program);
991
2362
  registerProjectCommands(program);
992
2363
  registerHooksCommands(program);
993
2364
  registerSkillCommands(program);
2365
+ registerMovesCommands(program);
2366
+ registerSessionCommands(program);
2367
+ registerDecisionsCommands(program);
2368
+ registerClaudeCommands(program);
2369
+ registerDaemonCommands(program);
2370
+ registerReconcileCommands(program);
2371
+ registerStatuslineCommands(program);
994
2372
  process.on("unhandledRejection", (err) => {
995
2373
  const msg = err instanceof Error ? err.message : String(err);
996
2374
  console.error(msg);
997
2375
  process.exit(1);
998
2376
  });
2377
+ var argv = process.argv.slice(2);
2378
+ var isExplicitFlush = argv[0] === "moves" && argv[1] === "flush";
2379
+ if (!isExplicitFlush) {
2380
+ flushIfNeeded().catch(() => {
2381
+ });
2382
+ }
999
2383
  program.parse();