@primitive.ai/prim 0.1.0-alpha.14 → 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,5 +1,16 @@
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,
@@ -8,12 +19,23 @@ import {
8
19
  getSiteUrl,
9
20
  getTokenExpiresAt,
10
21
  saveTokenExpiry
11
- } from "./chunk-3APLWTLB.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";
12
34
 
13
35
  // src/index.ts
14
- import { readFileSync as readFileSync6 } from "fs";
15
- import { dirname as dirname3, resolve as resolve3 } from "path";
16
- 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";
17
39
  import { Command } from "commander";
18
40
  import updateNotifier from "update-notifier";
19
41
 
@@ -111,10 +133,10 @@ function registerAuthCommands(program2) {
111
133
  process.exit(1);
112
134
  });
113
135
  });
114
- const port = await new Promise((resolve4) => {
136
+ const port = await new Promise((resolve5) => {
115
137
  server.listen(CALLBACK_PORT, LOCALHOST, () => {
116
138
  const addr = server.address();
117
- resolve4(typeof addr === "object" && addr ? addr.port : 0);
139
+ resolve5(typeof addr === "object" && addr ? addr.port : 0);
118
140
  });
119
141
  });
120
142
  const redirectUri = `http://${LOCALHOST}:${port}/callback`;
@@ -255,8 +277,263 @@ async function exchangeCode(siteUrl, code, codeVerifier, redirectUri) {
255
277
  return data.access_token;
256
278
  }
257
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
+
258
535
  // src/commands/context.ts
259
- import { readFileSync as readFileSync2 } from "fs";
536
+ import { readFileSync as readFileSync3 } from "fs";
260
537
  function registerContextCommands(program2) {
261
538
  const context = program2.command("context").description("Manage contexts");
262
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) => {
@@ -285,7 +562,7 @@ function registerContextCommands(program2) {
285
562
  const client = getClient();
286
563
  let text = opts.text;
287
564
  if (opts.file) {
288
- text = readFileSync2(opts.file, "utf-8");
565
+ text = readFileSync3(opts.file, "utf-8");
289
566
  }
290
567
  const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
291
568
  const result = await client.post("/api/cli/contexts", {
@@ -308,7 +585,7 @@ function registerContextCommands(program2) {
308
585
  const client = getClient();
309
586
  let text = opts.text;
310
587
  if (opts.file) {
311
- text = readFileSync2(opts.file, "utf-8");
588
+ text = readFileSync3(opts.file, "utf-8");
312
589
  }
313
590
  await client.patch(`/api/cli/contexts/${contextId}`, {
314
591
  name: opts.name,
@@ -372,9 +649,772 @@ function printContextList(contexts) {
372
649
  ${contexts.length} context(s)`);
373
650
  }
374
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
+
375
1415
  // src/commands/hooks.ts
376
1416
  import { execSync } from "child_process";
377
- 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";
378
1418
  import { resolve } from "path";
379
1419
  import { Option } from "commander";
380
1420
  var HOOK_SCRIPT = `#!/bin/sh
@@ -408,13 +1448,13 @@ function getGitRoot() {
408
1448
  }
409
1449
  function detectHusky(gitRoot) {
410
1450
  const huskyDir = resolve(gitRoot, ".husky");
411
- if (!existsSync2(huskyDir)) return false;
412
- if (existsSync2(resolve(huskyDir, "_"))) return true;
413
- 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;
414
1454
  const pkgPath = resolve(gitRoot, "package.json");
415
- if (existsSync2(pkgPath)) {
1455
+ if (existsSync4(pkgPath)) {
416
1456
  try {
417
- const pkg2 = JSON.parse(readFileSync3(pkgPath, "utf-8"));
1457
+ const pkg2 = JSON.parse(readFileSync5(pkgPath, "utf-8"));
418
1458
  const scripts = pkg2.scripts ?? {};
419
1459
  if (/husky/i.test(scripts.prepare ?? "") || /husky/i.test(scripts.postinstall ?? "")) {
420
1460
  return true;
@@ -441,20 +1481,20 @@ async function askConfirmation(question) {
441
1481
  }
442
1482
  function installToHusky(gitRoot) {
443
1483
  const hookPath = resolve(gitRoot, ".husky", "pre-commit");
444
- if (existsSync2(hookPath)) {
445
- const existing = readFileSync3(hookPath, "utf-8");
1484
+ if (existsSync4(hookPath)) {
1485
+ const existing = readFileSync5(hookPath, "utf-8");
446
1486
  if (containsPrimHook(existing)) {
447
1487
  console.log("Prim pre-commit hook is already installed in .husky/pre-commit.");
448
1488
  return;
449
1489
  }
450
1490
  const separator = existing.endsWith("\n") ? "\n" : "\n\n";
451
- writeFileSync2(hookPath, `${existing}${separator}${PRIM_HUSKY_BLOCK}
1491
+ writeFileSync3(hookPath, `${existing}${separator}${PRIM_HUSKY_BLOCK}
452
1492
  `, {
453
1493
  mode: 493
454
1494
  });
455
1495
  console.log("Appended prim hook block to .husky/pre-commit.");
456
1496
  } else {
457
- writeFileSync2(hookPath, `#!/bin/sh
1497
+ writeFileSync3(hookPath, `#!/bin/sh
458
1498
 
459
1499
  ${PRIM_HUSKY_BLOCK}
460
1500
  `, {
@@ -466,11 +1506,11 @@ ${PRIM_HUSKY_BLOCK}
466
1506
  function installToDotGit(gitRoot) {
467
1507
  const hooksDir = resolve(gitRoot, ".git", "hooks");
468
1508
  const hookPath = resolve(hooksDir, "pre-commit");
469
- if (!existsSync2(hooksDir)) {
470
- mkdirSync2(hooksDir, { recursive: true });
1509
+ if (!existsSync4(hooksDir)) {
1510
+ mkdirSync3(hooksDir, { recursive: true });
471
1511
  }
472
- if (existsSync2(hookPath)) {
473
- const existing = readFileSync3(hookPath, "utf-8");
1512
+ if (existsSync4(hookPath)) {
1513
+ const existing = readFileSync5(hookPath, "utf-8");
474
1514
  if (containsPrimHook(existing)) {
475
1515
  console.log("Prim pre-commit hook is already installed at .git/hooks/pre-commit.");
476
1516
  return;
@@ -479,7 +1519,7 @@ function installToDotGit(gitRoot) {
479
1519
  console.log("To replace it, run: prim hooks uninstall && prim hooks install");
480
1520
  return;
481
1521
  }
482
- writeFileSync2(hookPath, HOOK_SCRIPT, { mode: 493 });
1522
+ writeFileSync3(hookPath, HOOK_SCRIPT, { mode: 493 });
483
1523
  console.log(`Installed pre-commit hook at ${hookPath}`);
484
1524
  }
485
1525
  function registerHooksCommands(program2) {
@@ -521,15 +1561,147 @@ function registerHooksCommands(program2) {
521
1561
  hooks.command("uninstall").description("Remove the prim pre-commit hook").action(() => {
522
1562
  const gitRoot = getGitRoot();
523
1563
  const hookPath = resolve(gitRoot, ".git", "hooks", "pre-commit");
524
- if (!existsSync2(hookPath)) {
1564
+ if (!existsSync4(hookPath)) {
525
1565
  console.log("No pre-commit hook found.");
526
1566
  return;
527
1567
  }
528
- unlinkSync(hookPath);
1568
+ unlinkSync2(hookPath);
529
1569
  console.log(`Removed pre-commit hook at ${hookPath}`);
530
1570
  });
531
1571
  }
532
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
+
533
1705
  // src/commands/project.ts
534
1706
  function registerProjectCommands(program2) {
535
1707
  const project = program2.command("project").description("Manage projects");
@@ -552,20 +1724,165 @@ function registerProjectCommands(program2) {
552
1724
  });
553
1725
  }
554
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
+
555
1872
  // src/commands/skill.ts
556
1873
  import {
557
- closeSync,
558
- existsSync as existsSync3,
559
- fsyncSync,
560
- openSync,
561
- readFileSync as readFileSync4,
562
- renameSync,
563
- 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
564
1881
  } from "fs";
565
- import { dirname as dirname2, resolve as resolve2 } from "path";
1882
+ import { dirname as dirname3, resolve as resolve2 } from "path";
566
1883
  import { fileURLToPath } from "url";
567
1884
  import { createPatch } from "diff";
568
- var __dirname = dirname2(fileURLToPath(import.meta.url));
1885
+ var __dirname = dirname3(fileURLToPath(import.meta.url));
569
1886
  var SKILL_BEGIN = "<!-- BEGIN PRIM SKILL v1 -->";
570
1887
  var SKILL_END = "<!-- END PRIM SKILL v1 -->";
571
1888
  var TARGET_CANDIDATES = [
@@ -577,15 +1894,15 @@ var TARGET_CANDIDATES = [
577
1894
  var DEFAULT_TARGET = "CLAUDE.md";
578
1895
  function loadSkill() {
579
1896
  let dir = __dirname;
580
- while (dir !== dirname2(dir)) {
1897
+ while (dir !== dirname3(dir)) {
581
1898
  const p = resolve2(dir, "SKILL.md");
582
- if (existsSync3(p)) return readFileSync4(p, "utf-8");
583
- dir = dirname2(dir);
1899
+ if (existsSync7(p)) return readFileSync7(p, "utf-8");
1900
+ dir = dirname3(dir);
584
1901
  }
585
1902
  throw new Error("SKILL.md not found in package");
586
1903
  }
587
1904
  function detectTargets(cwd) {
588
- return TARGET_CANDIDATES.filter((p) => existsSync3(resolve2(cwd, p)));
1905
+ return TARGET_CANDIDATES.filter((p) => existsSync7(resolve2(cwd, p)));
589
1906
  }
590
1907
  function detectNewline(content) {
591
1908
  return content.includes("\r\n") ? "\r\n" : "\n";
@@ -611,16 +1928,16 @@ function removeBlock(existing) {
611
1928
  const out = existing.slice(0, b) + existing.slice(e + SKILL_END.length);
612
1929
  return out.replace(/(\r?\n){2,}$/, "$1");
613
1930
  }
614
- function atomicWrite(target, content) {
1931
+ function atomicWrite2(target, content) {
615
1932
  const tmp = `${target}.tmp`;
616
- writeFileSync3(tmp, content);
617
- const fd = openSync(tmp, "r+");
1933
+ writeFileSync6(tmp, content);
1934
+ const fd = openSync2(tmp, "r+");
618
1935
  try {
619
- fsyncSync(fd);
1936
+ fsyncSync2(fd);
620
1937
  } finally {
621
- closeSync(fd);
1938
+ closeSync2(fd);
622
1939
  }
623
- renameSync(tmp, target);
1940
+ renameSync3(tmp, target);
624
1941
  }
625
1942
  function resolveTarget(cwd, override) {
626
1943
  if (override) return resolve2(cwd, override);
@@ -634,7 +1951,7 @@ function resolveTarget(cwd, override) {
634
1951
  function runInstall(cwd, opts) {
635
1952
  const target = resolveTarget(cwd, opts.target);
636
1953
  if (target === null) return 1;
637
- const existing = existsSync3(target) ? readFileSync4(target, "utf-8") : "";
1954
+ const existing = existsSync7(target) ? readFileSync7(target, "utf-8") : "";
638
1955
  const eol = existing ? detectNewline(existing) : "\n";
639
1956
  const block = composeBlock(loadSkill(), eol);
640
1957
  const next = applyBlock(existing, block, eol);
@@ -646,34 +1963,34 @@ function runInstall(cwd, opts) {
646
1963
  process.stdout.write(createPatch(target, existing, next, "current", "proposed"));
647
1964
  return 0;
648
1965
  }
649
- atomicWrite(target, next);
1966
+ atomicWrite2(target, next);
650
1967
  console.log(`Wrote ${Buffer.byteLength(next)} bytes to ${target}`);
651
1968
  return 0;
652
1969
  }
653
1970
  function runUninstall(cwd, opts) {
654
1971
  const target = resolveTarget(cwd, opts.target);
655
1972
  if (target === null) return 1;
656
- if (!existsSync3(target)) {
1973
+ if (!existsSync7(target)) {
657
1974
  console.log(`Skill block not present at ${target}`);
658
1975
  return 0;
659
1976
  }
660
- const existing = readFileSync4(target, "utf-8");
1977
+ const existing = readFileSync7(target, "utf-8");
661
1978
  const next = removeBlock(existing);
662
1979
  if (next === null) {
663
1980
  console.log(`Skill block not present at ${target}`);
664
1981
  return 0;
665
1982
  }
666
- atomicWrite(target, next);
1983
+ atomicWrite2(target, next);
667
1984
  console.log(`Removed skill block from ${target}`);
668
1985
  return 0;
669
1986
  }
670
1987
  function runStatus(cwd, opts) {
671
1988
  const target = resolveTarget(cwd, opts.target);
672
1989
  if (target === null) return 1;
673
- const fileExists = existsSync3(target);
1990
+ const fileExists = existsSync7(target);
674
1991
  let installed = false;
675
1992
  if (fileExists) {
676
- const content = readFileSync4(target, "utf-8");
1993
+ const content = readFileSync7(target, "utf-8");
677
1994
  installed = content.includes(SKILL_BEGIN) && content.includes(SKILL_END);
678
1995
  }
679
1996
  if (opts.json) {
@@ -710,7 +2027,7 @@ function registerSkillCommands(program2) {
710
2027
  }
711
2028
 
712
2029
  // src/commands/spec.ts
713
- import { readFileSync as readFileSync5 } from "fs";
2030
+ import { readFileSync as readFileSync8 } from "fs";
714
2031
  function registerSpecCommands(program2) {
715
2032
  const spec = program2.command("spec").description("Manage spec documents");
716
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) => {
@@ -759,12 +2076,53 @@ ${contexts.length} spec(s)`);
759
2076
  }
760
2077
  printSpec(ctx);
761
2078
  });
2079
+ spec.command("create").description("Create a new spec document").requiredOption("-s, --scope <scope>", "Scope: project, global, external").requiredOption("-n, --name <name>", "Spec name").option("-t, --text <text>", "Spec text content").option("-f, --file <path>", "Read text content from file").option("--project-id <projectId>", "Link to project(s), comma-separated").option("--branch <branch>", "Link spec to this branch on the current repo").option("--pr <prNumber>", "Optional PR number to attach to the link").option("--json", "Output as JSON").action(
2080
+ async (opts) => {
2081
+ const client = getClient();
2082
+ let text = opts.text;
2083
+ if (opts.file) {
2084
+ text = readFileSync8(opts.file, "utf-8");
2085
+ }
2086
+ const taskIds = opts.projectId ? opts.projectId.split(",").map((id) => id.trim()) : void 0;
2087
+ let linkedBranch;
2088
+ if (opts.branch) {
2089
+ const { repoFullName } = getGitContext();
2090
+ if (!repoFullName) {
2091
+ console.warn(
2092
+ "[prim] --branch supplied but origin remote is not GitHub; skipping link."
2093
+ );
2094
+ } else {
2095
+ linkedBranch = { repoFullName, branch: opts.branch };
2096
+ if (opts.pr) {
2097
+ const n = Number.parseInt(opts.pr, 10);
2098
+ if (Number.isFinite(n)) linkedBranch.prNumber = n;
2099
+ }
2100
+ }
2101
+ }
2102
+ const result = await client.post("/api/cli/contexts", {
2103
+ scope: opts.scope === "project" ? "task" : opts.scope,
2104
+ name: opts.name,
2105
+ text,
2106
+ taskIds,
2107
+ isSpecDocument: true,
2108
+ linkedBranch
2109
+ });
2110
+ if (opts.json) {
2111
+ printJson({ _id: result._id });
2112
+ return;
2113
+ }
2114
+ console.error(
2115
+ `Created spec: ${result._id}${linkedBranch ? ` (linked to ${linkedBranch.branch})` : ""}`
2116
+ );
2117
+ console.log(result._id);
2118
+ }
2119
+ );
762
2120
  spec.command("update <contextId>").description("Update a spec document's text content").option("-t, --text <text>", "New text content").option("-f, --file <path>", "Read text content from file").option("-n, --name <name>", "New name").option("--json", "Output as JSON").action(
763
2121
  async (contextId, opts) => {
764
2122
  const client = getClient();
765
2123
  let text = opts.text;
766
2124
  if (opts.file) {
767
- text = readFileSync5(opts.file, "utf-8");
2125
+ text = readFileSync8(opts.file, "utf-8");
768
2126
  }
769
2127
  if (!(text || opts.name)) {
770
2128
  console.error("Provide --text, --file, or --name to update.");
@@ -806,6 +2164,65 @@ ${contexts.length} spec(s)`);
806
2164
  }
807
2165
  console.log(contextId);
808
2166
  });
2167
+ spec.command("review <contextId>").description("Manually trigger the PR Intent Review bot for a spec").requiredOption("--pr <prNumber>", "PR number to review against").option("--sha <headSha>", "Commit SHA the review runs against (defaults to current HEAD)").action(async (contextId, opts) => {
2168
+ const prNumber = Number.parseInt(opts.pr, 10);
2169
+ if (!Number.isFinite(prNumber)) {
2170
+ console.error("--pr must be an integer.");
2171
+ process.exit(1);
2172
+ }
2173
+ const headSha = opts.sha ?? getGitContext().sha;
2174
+ if (!headSha) {
2175
+ console.error("Could not determine head SHA \u2014 pass --sha or run inside a git checkout.");
2176
+ process.exit(1);
2177
+ }
2178
+ const client = getClient();
2179
+ await client.post(`/api/cli/contexts/${contextId}/review`, {
2180
+ prNumber,
2181
+ headSha
2182
+ });
2183
+ console.log(
2184
+ `Scheduled review: ${contextId} against PR #${String(prNumber)} @ ${headSha.slice(0, 7)}`
2185
+ );
2186
+ });
2187
+ spec.command("drift <contextId>").description("Dispatch the Claude Code drift-fix workflow against a PR").requiredOption("--pr <prNumber>", "PR number to dispatch the drift-fix workflow against").action(async (contextId, opts) => {
2188
+ const prNumber = Number.parseInt(opts.pr, 10);
2189
+ if (!Number.isFinite(prNumber)) {
2190
+ console.error("--pr must be an integer.");
2191
+ process.exit(1);
2192
+ }
2193
+ const client = getClient();
2194
+ const result = await client.post(`/api/cli/contexts/${contextId}/drift`, {
2195
+ prNumber
2196
+ });
2197
+ if (result.dispatched) {
2198
+ const ref = result.runUrl ? `: ${result.runUrl}` : "";
2199
+ console.log(`Dispatched drift-fix workflow${ref}`);
2200
+ } else {
2201
+ console.error(
2202
+ "Drift-fix dispatch failed. Likely causes: actions:write App scope not granted, primitive-drift-fix.yml workflow file missing, or no findings on the latest review."
2203
+ );
2204
+ process.exit(1);
2205
+ }
2206
+ });
2207
+ spec.command("status <taskId>").description(
2208
+ "Show task status, auto-complete suppression flag, and the most-recent bot auto-completion"
2209
+ ).action(async (taskId) => {
2210
+ const client = getClient();
2211
+ const result = await client.get(`/api/cli/tasks/${taskId}/status`);
2212
+ console.log(`status: ${result.status}`);
2213
+ console.log(`auto-complete suppressed: ${result.autoCompleteSuppressed ? "yes" : "no"}`);
2214
+ const last = result.lastAutoCompleteActivity;
2215
+ if (last) {
2216
+ const when = last.createdAt ? new Date(last.createdAt).toISOString() : "\u2014";
2217
+ const pr = last.prNumber ? `#${String(last.prNumber)}` : "\u2014";
2218
+ console.log(`last auto-complete: ${when} (PR ${pr})`);
2219
+ if (last.explanation) {
2220
+ console.log(` ${last.explanation}`);
2221
+ }
2222
+ } else {
2223
+ console.log("last auto-complete: \u2014");
2224
+ }
2225
+ });
809
2226
  spec.command("map <contextId>").description("Map file patterns to a spec (used by pre-commit hook to detect affected specs)").requiredOption(
810
2227
  "-p, --pattern <patterns...>",
811
2228
  'Glob pattern(s) to associate, e.g. "src/auth/**"'
@@ -875,9 +2292,64 @@ ${preview}`);
875
2292
  }
876
2293
  }
877
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
+
878
2350
  // src/index.ts
879
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
880
- 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"));
881
2353
  updateNotifier({ pkg }).notify();
882
2354
  var program = new Command();
883
2355
  program.name("prim").description("CLI for managing Primitive specs and contexts").version(pkg.version).option("-y, --yes", "auto-confirm prompts").option(
@@ -890,9 +2362,22 @@ registerSpecCommands(program);
890
2362
  registerProjectCommands(program);
891
2363
  registerHooksCommands(program);
892
2364
  registerSkillCommands(program);
2365
+ registerMovesCommands(program);
2366
+ registerSessionCommands(program);
2367
+ registerDecisionsCommands(program);
2368
+ registerClaudeCommands(program);
2369
+ registerDaemonCommands(program);
2370
+ registerReconcileCommands(program);
2371
+ registerStatuslineCommands(program);
893
2372
  process.on("unhandledRejection", (err) => {
894
2373
  const msg = err instanceof Error ? err.message : String(err);
895
2374
  console.error(msg);
896
2375
  process.exit(1);
897
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
+ }
898
2383
  program.parse();