xtrm-cli 2.1.16 → 2.1.19

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.
@@ -77,7 +77,7 @@ describe('main-guard.mjs — MAIN_GUARD_PROTECTED_BRANCHES', () => {
77
77
  );
78
78
  expect(r.status).toBe(2);
79
79
  const out = parseHookJson(r.stdout);
80
- expect(out?.systemMessage).toContain('Bash is restricted');
80
+ expect(out?.systemMessage).toContain('Bash restricted');
81
81
  });
82
82
 
83
83
  it('allows safe Bash commands on protected branch', () => {
@@ -117,10 +117,19 @@ describe('main-guard.mjs — MAIN_GUARD_PROTECTED_BRANCHES', () => {
117
117
  );
118
118
  expect(r.status, `expected exit 2 for: ${command}`).toBe(2);
119
119
  const out = parseHookJson(r.stdout);
120
- expect(out?.systemMessage).toContain('Bash is restricted');
120
+ expect(out?.systemMessage).toContain('Bash restricted');
121
121
  }
122
122
  });
123
123
 
124
+ it('allows touch .beads/.memory-gate-done on protected branch', () => {
125
+ const r = runHook(
126
+ 'main-guard.mjs',
127
+ { tool_name: 'Bash', tool_input: { command: 'touch .beads/.memory-gate-done' } },
128
+ { MAIN_GUARD_PROTECTED_BRANCHES: CURRENT_BRANCH },
129
+ );
130
+ expect(r.status).toBe(0);
131
+ });
132
+
124
133
  it('allows Bash when MAIN_GUARD_ALLOW_BASH=1 is set', () => {
125
134
  const r = runHook(
126
135
  'main-guard.mjs',
@@ -187,9 +196,11 @@ describe('main-guard-post-push.mjs', () => {
187
196
  repoDir,
188
197
  );
189
198
  expect(r.status).toBe(0);
190
- const out = parseHookJson(r.stdout);
191
- expect(out?.systemMessage).toContain('gh pr create --fill');
192
- expect(out?.systemMessage).toContain('gh pr merge --squash');
199
+ expect(r.stdout).toContain('gh pr create --fill');
200
+ expect(r.stdout).toContain('gh pr merge --squash');
201
+ // output must be plain text (agent-only), not a JSON systemMessage banner
202
+ expect(parseHookJson(r.stdout)).toBeNull();
203
+
193
204
  } finally {
194
205
  rmSync(repoDir, { recursive: true, force: true });
195
206
  }
@@ -320,7 +331,7 @@ exit 1
320
331
  { PATH: `${fake.tempDir}:${process.env.PATH ?? ''}` },
321
332
  );
322
333
  expect(r.status).toBe(2);
323
- expect(r.stderr).toContain('no active claim');
334
+ expect(r.stderr).toContain('active claim');
324
335
  } finally {
325
336
  rmSync(fake.tempDir, { recursive: true, force: true });
326
337
  rmSync(projectDir, { recursive: true, force: true });
@@ -445,7 +456,7 @@ exit 1
445
456
  { PATH: `${fake.tempDir}:${process.env.PATH ?? ''}` },
446
457
  );
447
458
  expect(r.status).toBe(2);
448
- expect(r.stderr).toContain('MEMORY GATE');
459
+ expect(r.stderr).toContain('Memory gate');
449
460
  } finally {
450
461
  rmSync(fake.tempDir, { recursive: true, force: true });
451
462
  rmSync(projectDir, { recursive: true, force: true });
@@ -542,51 +553,6 @@ exit 2
542
553
  });
543
554
 
544
555
 
545
- // ── gitnexus-impact-reminder.py ──────────────────────────────────────────────
546
-
547
- function runPythonHook(
548
- hookFile: string,
549
- input: Record<string, unknown>,
550
- ) {
551
- return spawnSync('python3', [path.join(HOOKS_DIR, hookFile)], {
552
- input: JSON.stringify(input),
553
- encoding: 'utf8',
554
- env: { ...process.env },
555
- });
556
- }
557
-
558
- describe('gitnexus-impact-reminder.py', () => {
559
- it('injects additionalContext when prompt contains an edit-intent keyword', () => {
560
- const r = runPythonHook('gitnexus-impact-reminder.py', {
561
- hook_event_name: 'UserPromptSubmit',
562
- prompt: 'fix the broken auth logic in login.ts',
563
- });
564
- expect(r.status).toBe(0);
565
- const out = parseHookJson(r.stdout);
566
- expect(out?.hookSpecificOutput?.additionalContext).toContain('gitnexus impact');
567
- });
568
-
569
- it('does nothing (no output) when prompt has no edit-intent keywords', () => {
570
- const r = runPythonHook('gitnexus-impact-reminder.py', {
571
- hook_event_name: 'UserPromptSubmit',
572
- prompt: 'explain how the beads gate works',
573
- });
574
- expect(r.status).toBe(0);
575
- expect(r.stdout.trim()).toBe('');
576
- });
577
-
578
- it('does nothing for non-UserPromptSubmit events', () => {
579
- const r = runPythonHook('gitnexus-impact-reminder.py', {
580
- hook_event_name: 'PreToolUse',
581
- tool_name: 'Edit',
582
- tool_input: { file_path: 'foo.ts' },
583
- prompt: 'fix something',
584
- });
585
- expect(r.status).toBe(0);
586
- expect(r.stdout.trim()).toBe('');
587
- });
588
- });
589
-
590
556
 
591
557
  // ── beads-gate-core.mjs — decision functions ──────────────────────────────────
592
558
 
@@ -704,3 +670,141 @@ process.exit(ok ? 0 : 1);
704
670
  });
705
671
  });
706
672
  });
673
+
674
+
675
+ // ── beads-compact-save.mjs ───────────────────────────────────────────────────
676
+ describe('beads-compact-save.mjs', () => {
677
+ it('exits 0 silently when no .beads directory exists', () => {
678
+ const r = runHook('beads-compact-save.mjs', { hook_event_name: 'PreCompact', cwd: '/tmp' });
679
+ expect(r.status).toBe(0);
680
+ expect(r.stdout).toBe('');
681
+ });
682
+
683
+ it('exits 0 silently and writes no file when no in_progress issues', () => {
684
+ const projectDir = mkdtempSync(path.join(os.tmpdir(), 'xtrm-compact-save-'));
685
+ mkdirSync(path.join(projectDir, '.beads'));
686
+ const fake = withFakeBdDir(`#!/usr/bin/env bash
687
+ if [[ "$1" == "list" ]]; then
688
+ echo ""
689
+ echo "--------------------------------------------------------------------------------"
690
+ echo "Total: 2 issues (2 open, 0 in progress)"
691
+ exit 0
692
+ fi
693
+ exit 1
694
+ `);
695
+ try {
696
+ const r = runHook(
697
+ 'beads-compact-save.mjs',
698
+ { hook_event_name: 'PreCompact', cwd: projectDir },
699
+ { PATH: `${fake.tempDir}:${process.env.PATH ?? ''}` },
700
+ );
701
+ expect(r.status).toBe(0);
702
+ const { existsSync } = require('node:fs');
703
+ expect(existsSync(path.join(projectDir, '.beads', '.last_active'))).toBe(false);
704
+ } finally {
705
+ rmSync(fake.tempDir, { recursive: true, force: true });
706
+ rmSync(projectDir, { recursive: true, force: true });
707
+ }
708
+ });
709
+
710
+ it('writes .beads/.last_active with in_progress issue IDs', () => {
711
+ const projectDir = mkdtempSync(path.join(os.tmpdir(), 'xtrm-compact-save-'));
712
+ mkdirSync(path.join(projectDir, '.beads'));
713
+ const fake = withFakeBdDir(`#!/usr/bin/env bash
714
+ if [[ "$1" == "list" ]]; then
715
+ cat <<'EOF'
716
+ ◐ proj-abc123 ● P1 First in_progress issue
717
+ ◐ proj-def456 ● P2 Second in_progress issue
718
+
719
+ --------------------------------------------------------------------------------
720
+ Total: 2 issues (0 open, 2 in progress)
721
+ EOF
722
+ exit 0
723
+ fi
724
+ exit 1
725
+ `);
726
+ try {
727
+ const r = runHook(
728
+ 'beads-compact-save.mjs',
729
+ { hook_event_name: 'PreCompact', cwd: projectDir },
730
+ { PATH: `${fake.tempDir}:${process.env.PATH ?? ''}` },
731
+ );
732
+ expect(r.status).toBe(0);
733
+ const { readFileSync: rfs } = require('node:fs');
734
+ const saved = rfs(path.join(projectDir, '.beads', '.last_active'), 'utf8').trim().split('\n');
735
+ expect(saved).toContain('proj-abc123');
736
+ expect(saved).toContain('proj-def456');
737
+ } finally {
738
+ rmSync(fake.tempDir, { recursive: true, force: true });
739
+ rmSync(projectDir, { recursive: true, force: true });
740
+ }
741
+ });
742
+ });
743
+
744
+ // ── beads-compact-restore.mjs ────────────────────────────────────────────────
745
+ describe('beads-compact-restore.mjs', () => {
746
+ it('exits 0 silently when no .beads/.last_active file exists', () => {
747
+ const projectDir = mkdtempSync(path.join(os.tmpdir(), 'xtrm-compact-restore-'));
748
+ mkdirSync(path.join(projectDir, '.beads'));
749
+ try {
750
+ const r = runHook(
751
+ 'beads-compact-restore.mjs',
752
+ { hook_event_name: 'SessionStart', cwd: projectDir },
753
+ );
754
+ expect(r.status).toBe(0);
755
+ expect(r.stdout).toBe('');
756
+ } finally {
757
+ rmSync(projectDir, { recursive: true, force: true });
758
+ }
759
+ });
760
+
761
+ it('restores in_progress status, deletes .last_active, and injects additionalSystemPrompt', () => {
762
+ const projectDir = mkdtempSync(path.join(os.tmpdir(), 'xtrm-compact-restore-'));
763
+ mkdirSync(path.join(projectDir, '.beads'));
764
+ const { writeFileSync: wfs } = require('node:fs');
765
+ wfs(path.join(projectDir, '.beads', '.last_active'), 'proj-abc123\nproj-def456\n', 'utf8');
766
+
767
+ const callLog = path.join(projectDir, 'bd-calls.log');
768
+ const fake = withFakeBdDir(`#!/usr/bin/env bash
769
+ echo "$@" >> "${callLog}"
770
+ exit 0
771
+ `);
772
+ try {
773
+ const r = runHook(
774
+ 'beads-compact-restore.mjs',
775
+ { hook_event_name: 'SessionStart', cwd: projectDir },
776
+ { PATH: `${fake.tempDir}:${process.env.PATH ?? ''}` },
777
+ );
778
+ expect(r.status).toBe(0);
779
+ // .last_active must be deleted
780
+ const { existsSync: exs, readFileSync: rfs } = require('node:fs');
781
+ expect(exs(path.join(projectDir, '.beads', '.last_active'))).toBe(false);
782
+ // bd update called for each ID
783
+ const calls = rfs(callLog, 'utf8');
784
+ expect(calls).toContain('proj-abc123');
785
+ expect(calls).toContain('proj-def456');
786
+ // additionalSystemPrompt injected for agent
787
+ const out = parseHookJson(r.stdout);
788
+ expect(out?.hookSpecificOutput?.additionalSystemPrompt).toMatch(/Restored 2 in_progress issue/);
789
+ } finally {
790
+ rmSync(fake.tempDir, { recursive: true, force: true });
791
+ rmSync(projectDir, { recursive: true, force: true });
792
+ }
793
+ });
794
+ });
795
+
796
+
797
+ // ── hooks.json wiring ────────────────────────────────────────────────────────
798
+ describe('hooks.json — beads-compact hooks wiring', () => {
799
+ it('wires beads-compact-save.mjs to PreCompact event', () => {
800
+ const cfg = JSON.parse(readFileSync(path.join(__dirname, '../../config/hooks.json'), 'utf8'));
801
+ const preCompact: Array<{ script: string }> = cfg.hooks.PreCompact ?? [];
802
+ expect(preCompact.some((h) => h.script === 'beads-compact-save.mjs')).toBe(true);
803
+ });
804
+
805
+ it('wires beads-compact-restore.mjs to SessionStart event', () => {
806
+ const cfg = JSON.parse(readFileSync(path.join(__dirname, '../../config/hooks.json'), 'utf8'));
807
+ const sessionStart: Array<{ script: string }> = cfg.hooks.SessionStart ?? [];
808
+ expect(sessionStart.some((h) => h.script === 'beads-compact-restore.mjs')).toBe(true);
809
+ });
810
+ });