From 6633f53b413ef52a0206c7a8616ae2bbe424ee13 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Thu, 18 Jun 2026 11:26:51 +0200 Subject: [PATCH] feat: wire headroom token-saving into gx (advisory part + GUARDEX_COMPRESS_CMD) Two backward-compatible layers, both graceful when headroom is absent (gitguardex is published, so headroom is never a hard dependency): Advisory: add a prompt-only 'headroom' part to AI_SETUP_PARTS (mirrors the rtk part) + compress/compression/headroom-mcp aliases, and a headroom bullet in the managed AGENTS companion-tooling block. Teaches agents to route large gx output / logs / dumps through headroom_compress (reversible) when present. Runtime: src/output gains resolveCompressCommand/compressBlock/printCompressible; gx prompt --snippet now routes through compressBlock. When GUARDEX_COMPRESS_CMD is set, gx pipes large narrative output through that filter (shell:false argv, timeout, fail-open). Gated on terse/non-TTY mode + size threshold; never compresses machine-readable (JSON) output or the --exec path. Unset = output is byte-for-byte identical to today. Tests: 23 pass (compressBlock unit coverage incl. passthrough/transform/ fail-open/json-skip/threshold/terse-gate; prompt integration for the part, aliases, exec rejection, and snippet compression). OpenSpec: agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50 --- .../.openspec.yaml | 2 + .../proposal.md | 33 +++++++ .../spec.md | 36 +++++++ .../tasks.md | 34 +++++++ src/cli/commands/prompt.js | 5 +- src/context.js | 14 +++ src/output/index.js | 97 +++++++++++++++++++ templates/AGENTS.multiagent-safety.md | 1 + test/output.test.js | 62 +++++++++++- test/prompt.test.js | 53 ++++++++++ 10 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/.openspec.yaml create mode 100644 openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/proposal.md create mode 100644 openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/specs/wire-headroom-token-saving-skills-into-gx-output/spec.md create mode 100644 openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/tasks.md diff --git a/openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/.openspec.yaml b/openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/.openspec.yaml new file mode 100644 index 00000000..95ae5a2c --- /dev/null +++ b/openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-18 diff --git a/openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/proposal.md b/openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/proposal.md new file mode 100644 index 00000000..64611334 --- /dev/null +++ b/openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/proposal.md @@ -0,0 +1,33 @@ +## Why + +AI agents read `gx` output into their context window, so verbose output costs tokens +every turn. gitguardex already wires one token-saving tool into agents (the `rtk` +command-compression prompt part) and ships terse-by-default output, but the +**headroom** context-compression tool is referenced nowhere. Agents working in a +gx-wired repo are never told to compress large `gx` output / logs / dumps, and gx +has no hook to route its own output through a compressor. + +## What Changes + +Two backward-compatible layers, both graceful when headroom is absent (gitguardex is +a published npm package, so headroom must never be a hard dependency): + +- **Advisory** — add a prompt-only `headroom` part to the AI setup checklist + (`gx prompt`), mirroring the existing `rtk` part, plus `compress` / `compression` / + `headroom-mcp` aliases. Add a headroom bullet to the managed AGENTS companion-tooling + block so the guidance propagates into every consumer repo. +- **Runtime** — add `GUARDEX_COMPRESS_CMD` support: when set, gx pipes its large + narrative output (currently the `gx prompt --snippet` block) through the configured + filter. Gated on terse/non-TTY mode + a size threshold; skips machine-readable + (JSON-looking) text; fails open to the original text on any error. + +## Impact + +- Affected surfaces: `src/context.js` (AI_SETUP_PARTS + aliases), `src/output/index.js` + (new `compressBlock` / `printCompressible` helpers), `src/cli/commands/prompt.js` + (snippet routed through the compressor), `templates/AGENTS.multiagent-safety.md`. +- Default behavior is unchanged: with no `GUARDEX_COMPRESS_CMD` set, output is + byte-for-byte identical to today. `gx prompt --exec` never includes the prompt-only + headroom part. No API/schema changes; new env knob only. +- Risk: low. The compressor runs with `shell:false` argv (no shell interpolation), + a timeout, and a fail-open fallback; machine-readable output is never compressed. diff --git a/openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/specs/wire-headroom-token-saving-skills-into-gx-output/spec.md b/openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/specs/wire-headroom-token-saving-skills-into-gx-output/spec.md new file mode 100644 index 00000000..12afb0b2 --- /dev/null +++ b/openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/specs/wire-headroom-token-saving-skills-into-gx-output/spec.md @@ -0,0 +1,36 @@ +## ADDED Requirements + +### Requirement: Headroom advisory prompt part +The AI setup prompt machinery SHALL expose a prompt-only `headroom` part that teaches +agents to compress large `gx` output, logs, and dumps through headroom when available, +and SHALL fall back gracefully (the part is advisory text; it imposes no dependency). + +#### Scenario: Full prompt includes headroom guidance +- **WHEN** `gx prompt` is run with no part filter +- **THEN** the output includes a "Headroom context compression" section +- **AND** it names `headroom_compress`, `headroom_retrieve`, and `GUARDEX_COMPRESS_CMD`. + +#### Scenario: Part is selectable and aliased +- **WHEN** `gx prompt --part headroom` (or the alias `--part compress`) is run +- **THEN** only the headroom slice is printed +- **AND** `gx prompt --list-parts` includes `headroom`. + +#### Scenario: Prompt-only part is excluded from exec output +- **WHEN** `gx prompt --exec --part headroom` is run +- **THEN** the command exits non-zero with a "not available with --exec" error +- **AND** `gx prompt --exec` (no filter) omits the headroom part. + +### Requirement: GUARDEX_COMPRESS_CMD runtime compression +gx SHALL route large narrative output through an external compressor when +`GUARDEX_COMPRESS_CMD` is set, and SHALL leave output unchanged otherwise. + +#### Scenario: Compressor applied when configured +- **WHEN** `GUARDEX_COMPRESS_CMD` is set to a filter and a large narrative block is + emitted to a non-TTY (terse) stream +- **THEN** the block is piped through the filter and the filter output is printed. + +#### Scenario: Unchanged by default and fail-open +- **WHEN** `GUARDEX_COMPRESS_CMD` is unset, or the configured command fails, or the + block is machine-readable (JSON) or below the size threshold +- **THEN** the original block is printed byte-for-byte +- **AND** regressions are covered by tests. diff --git a/openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/tasks.md b/openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/tasks.md new file mode 100644 index 00000000..9dd6936b --- /dev/null +++ b/openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/tasks.md @@ -0,0 +1,34 @@ +## Definition of Done + +This change is complete only when **all** of the following are true: + +- Every checkbox below is checked. +- The agent branch reaches `MERGED` state on `origin` and the PR URL + state are recorded in the completion handoff. +- If any step blocks (test failure, conflict, ambiguous result), append a `BLOCKED:` line under section 4 explaining the blocker and **STOP**. Do not tick remaining cleanup boxes; do not silently skip the cleanup pipeline. + +## Handoff + +- Handoff: change=`agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50`; branch=`agent//`; scope=`TODO`; action=`continue this sandbox or finish cleanup after a usage-limit/manual takeover`. +- Copy prompt: Continue `agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50` on branch `agent//`. Work inside the existing sandbox, review `openspec/changes/agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50/tasks.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent// --base dev --via-pr --wait-for-merge --cleanup`. + +## 1. Specification + +- [x] 1.1 Finalize proposal scope and acceptance criteria for `agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50`. +- [x] 1.2 Define normative requirements in `specs/wire-headroom-token-saving-skills-into-gx-output/spec.md`. + +## 2. Implementation + +- [x] 2.1 Implement scoped behavior changes (headroom advisory part + aliases in `src/context.js`, AGENTS companion bullet, `compressBlock`/`printCompressible` in `src/output/index.js`, snippet wiring in `src/cli/commands/prompt.js`). +- [x] 2.2 Add/update focused regression coverage (`test/prompt.test.js` headroom + snippet-compression; `test/output.test.js` compressBlock unit tests). + +## 3. Verification + +- [x] 3.1 Run targeted project verification commands (`node --test test/output.test.js test/prompt.test.js` -> 23 pass, 0 fail; module-load sanity OK). +- [x] 3.2 Run `openspec validate agent-claude-wire-headroom-token-saving-skills-into-g-2026-06-18-10-50 --type change --strict` (valid). +- [x] 3.3 Run `openspec validate --specs` (133 passed, 0 failed). + +## 4. Cleanup (mandatory; run before claiming completion) + +- [ ] 4.1 Run the cleanup pipeline: `gx branch finish --branch agent// --base dev --via-pr --wait-for-merge --cleanup`. This handles commit -> push -> PR create -> merge wait -> worktree prune in one invocation. +- [ ] 4.2 Record the PR URL and final merge state (`MERGED`) in the completion handoff. +- [ ] 4.3 Confirm the sandbox worktree is gone (`git worktree list` no longer shows the agent path; `git branch -a` shows no surviving local/remote refs for the branch). diff --git a/src/cli/commands/prompt.js b/src/cli/commands/prompt.js index 7028f1ec..7a947eb0 100644 --- a/src/cli/commands/prompt.js +++ b/src/cli/commands/prompt.js @@ -10,10 +10,13 @@ const { AI_SETUP_PROMPT, AI_SETUP_COMMANDS, } = require('../../context'); +const { compressBlock } = require('../../output'); function printAgentsSnippet() { const snippetPath = path.join(TEMPLATE_ROOT, 'AGENTS.multiagent-safety.md'); - process.stdout.write(fs.readFileSync(snippetPath, 'utf8')); + // Route the large AGENTS narrative through the optional compressor + // (GUARDEX_COMPRESS_CMD). No compressor configured -> unchanged passthrough. + process.stdout.write(compressBlock(fs.readFileSync(snippetPath, 'utf8'))); } function copyPrompt() { diff --git a/src/context.js b/src/context.js index 0265320d..b75fa094 100644 --- a/src/context.js +++ b/src/context.js @@ -474,6 +474,9 @@ const AI_SETUP_PART_ALIASES = new Map([ ['compact-commands', 'rtk'], ['command-compression', 'rtk'], ['token-commands', 'rtk'], + ['compress', 'headroom'], + ['compression', 'headroom'], + ['headroom-mcp', 'headroom'], ['reviewbot', 'review-bot'], ['forksync', 'fork-sync'], ]); @@ -537,6 +540,17 @@ const AI_SETUP_PARTS = [ 'Do not wrap machine-readable commands with RTK when code parses stdout (`--porcelain`, `--json`, NUL-delimited output, or exact stdout contracts).', ], }, + { + name: 'headroom', + label: 'Headroom context compression', + promptLines: [ + 'When the headroom MCP/proxy is available, compress large blobs before they enter context; it is reversible (retrieve restores the original), so prefer it over truncating. Fall back to plain text when headroom is absent.', + 'Compress noisy `gx` output, long logs, and large file/diff dumps with headroom_compress; keep PR URLs, branch names, and file paths visible so you can still act on them.', + 'Pull a dropped detail back with headroom_retrieve ; audit savings with headroom_stats.', + 'Runtime hook: set GUARDEX_COMPRESS_CMD="" so gx routes its own large narrative output (e.g. `gx prompt --snippet`) through your compressor; unset leaves output byte-for-byte unchanged.', + 'Do not compress machine-readable output (`--json`, `--porcelain`, NUL-delimited, exact stdout contracts) or any value you must use verbatim.', + ], + }, { name: 'integrate', label: 'Integrate', diff --git a/src/output/index.js b/src/output/index.js index 3b401f8e..f35226f1 100644 --- a/src/output/index.js +++ b/src/output/index.js @@ -1,4 +1,5 @@ const { + cp, path, packageJson, TOOL_NAME, @@ -640,6 +641,99 @@ function printAutoFinishSummary(summary, options = {}) { } } +// --------------------------------------------------------------------------- +// Optional output compression (headroom / GUARDEX_COMPRESS_CMD) +// +// gitguardex is a published package, so the headroom compressor is never a hard +// dependency. When GUARDEX_COMPRESS_CMD is set to a stdin->stdout filter +// command, gx routes its large narrative output through it before printing so +// the blob costs fewer tokens once it lands in an AI agent's context. The +// default path (env unset) is byte-for-byte identical to plain console.log, and +// every failure mode falls back to the original text — fail-open, mirroring +// headroom's own proxy posture. +// +// Guards: only compress in terse (agent / non-TTY) mode, only blocks at or +// above COMPRESS_MIN_CHARS, and never machine-readable (JSON) payloads whose +// exact stdout other tools parse. +// --------------------------------------------------------------------------- + +const COMPRESS_MIN_CHARS = 400; + +// resolveCompressCommand parses GUARDEX_COMPRESS_CMD into an argv array (split +// on whitespace, run with shell:false so no shell interpolation). Returns null +// when unset/empty so callers fall straight through to plain output. Commands +// needing args with embedded spaces should be wrapped in a small script. +function resolveCompressCommand(env = process.env) { + const raw = String(env.GUARDEX_COMPRESS_CMD || '').trim(); + if (!raw) { + return null; + } + const argv = raw.split(/\s+/).filter(Boolean); + return argv.length > 0 ? argv : null; +} + +// looksMachineReadable keeps JSON / structured payloads (status --json, lock +// files, MCP responses) from ever being compressed; those have exact stdout +// contracts downstream parsers depend on. This is a first-character heuristic +// (leading `{` or `[`), which fully covers the current narrative call-sites +// (the markdown AGENTS snippet). A future caller that pipes other +// machine-readable formats (NUL-delimited, TSV, YAML) through compressBlock +// must guard that itself. +function looksMachineReadable(text) { + const head = String(text || '').trimStart()[0]; + return head === '{' || head === '['; +} + +// compressBlock returns `text` unchanged unless a compressor is configured AND +// we are in terse (agent / non-TTY) mode AND the block is large enough AND it +// is not machine-readable. Any failure (missing binary, non-zero exit, empty +// output, timeout) falls back to the original text. Never throws. Pass +// options.force to bypass the terse-mode gate (used by tests). +function compressBlock(text, options = {}) { + const input = String(text == null ? '' : text); + const env = options.env || process.env; + const argv = resolveCompressCommand(env); + if (!argv) { + return input; + } + if (!options.force && !isTerseMode()) { + return input; + } + if (input.length < COMPRESS_MIN_CHARS) { + return input; + } + if (looksMachineReadable(input)) { + return input; + } + let result; + try { + result = cp.spawnSync(argv[0], argv.slice(1), { + input, + encoding: 'utf8', + maxBuffer: 64 * 1024 * 1024, + timeout: Number(env.GUARDEX_COMPRESS_TIMEOUT_MS) || 5000, + }); + } catch { + return input; + } + if (!result || result.error) { + return input; + } + // Non-zero exit OR signal-killed (status === null, e.g. timeout/OOM) -> fall + // back, even if the process emitted partial stdout before dying. + if (result.signal || (typeof result.status === 'number' && result.status !== 0)) { + return input; + } + const out = String(result.stdout || ''); + return out.trim().length > 0 ? out : input; +} + +// printCompressible writes a large narrative block to stdout, compressed when a +// compressor is configured (see compressBlock). Default path === console.log. +function printCompressible(text, options = {}) { + console.log(compressBlock(text, options)); +} + module.exports = { runtimeVersion, supportsAnsiColors, @@ -664,4 +758,7 @@ module.exports = { detectRecoverableAutoFinishConflict, summarizeAutoFinishDetail, printAutoFinishSummary, + resolveCompressCommand, + compressBlock, + printCompressible, }; diff --git a/templates/AGENTS.multiagent-safety.md b/templates/AGENTS.multiagent-safety.md index 2214f91d..b4e0ce36 100644 --- a/templates/AGENTS.multiagent-safety.md +++ b/templates/AGENTS.multiagent-safety.md @@ -138,6 +138,7 @@ Persist unresolved questions or blockers into `openspec/plan//open-qu - **fff MCP** (file search): prefer for all file search; fall back to `rtk grep`/`rtk find` or `rg`. - **rtk** (shell compression): wrap noisy discovery (`rtk ls`/`grep`/`find`/`read`), git/gh (`rtk git status`/`gh pr list`), and verification (`rtk tsc`/`lint`/`test`). Do **not** wrap machine-readable commands (`--porcelain`, `--json`, exact stdout contracts). +- **headroom** (context compression): when available, run large `gx` output, long logs, and big file/diff dumps through `headroom_compress` before reasoning over them (reversible — `headroom_retrieve` restores). Or set `GUARDEX_COMPRESS_CMD=""` so gx routes its own large narrative output through your compressor. Keep PR URLs, branch names, and file paths visible; never compress `--json`/`--porcelain` or values you act on verbatim. - **OpenSpec**: keep `openspec/changes//tasks.md` current during work, not batched. Validate with `openspec validate --specs` before archive. ### Token / context budget diff --git a/test/output.test.js b/test/output.test.js index 58e71b20..9168133c 100644 --- a/test/output.test.js +++ b/test/output.test.js @@ -1,7 +1,15 @@ const test = require('node:test'); const assert = require('node:assert/strict'); -const { printAutoFinishSummary } = require('../src/output'); +const { + printAutoFinishSummary, + compressBlock, + resolveCompressCommand, +} = require('../src/output'); + +// A large lowercase block so the `tr a-z A-Z` stub visibly transforms it and it +// clears the COMPRESS_MIN_CHARS threshold. +const BIG_BLOCK = 'gx headroom compression block under test. '.repeat(20); function captureConsoleLogs(run) { const originalLog = console.log; @@ -74,3 +82,55 @@ test('printAutoFinishSummary keeps hidden failure counts explicit when compact o assert.match(lines[1], /\[fail\] agent\/fail-one: unexpected auth outage/); assert.match(lines[2], /7 more branch result\(s\) hidden: fail=1, skip=6/); }); + +test('resolveCompressCommand returns null when unset and argv when set', () => { + assert.equal(resolveCompressCommand({}), null); + assert.equal(resolveCompressCommand({ GUARDEX_COMPRESS_CMD: ' ' }), null); + assert.deepEqual(resolveCompressCommand({ GUARDEX_COMPRESS_CMD: 'tr a-z A-Z' }), ['tr', 'a-z', 'A-Z']); +}); + +test('compressBlock passes text through unchanged when no compressor is configured', () => { + assert.equal(compressBlock(BIG_BLOCK, { env: {}, force: true }), BIG_BLOCK); +}); + +test('compressBlock runs the configured compressor on large blocks', () => { + const out = compressBlock(BIG_BLOCK, { env: { GUARDEX_COMPRESS_CMD: 'tr a-z A-Z' }, force: true }); + assert.equal(out, BIG_BLOCK.toUpperCase()); +}); + +test('compressBlock falls back to the original text when the compressor fails', () => { + const out = compressBlock(BIG_BLOCK, { + env: { GUARDEX_COMPRESS_CMD: 'guardex-no-such-binary-zzz' }, + force: true, + }); + assert.equal(out, BIG_BLOCK); +}); + +test('compressBlock never compresses machine-readable JSON payloads', () => { + const json = `{"data":"${'x'.repeat(600)}"}`; + const out = compressBlock(json, { env: { GUARDEX_COMPRESS_CMD: 'tr a-z A-Z' }, force: true }); + assert.equal(out, json); +}); + +test('compressBlock skips blocks below the size threshold', () => { + const small = 'short line'; + const out = compressBlock(small, { env: { GUARDEX_COMPRESS_CMD: 'tr a-z A-Z' }, force: true }); + assert.equal(out, small); +}); + +test('compressBlock respects the terse-mode gate (skips when verbose)', () => { + const prev = process.env.GUARDEX_VERBOSE; + process.env.GUARDEX_VERBOSE = '1'; + try { + // force omitted -> the terse gate (isTerseMode) applies; GUARDEX_VERBOSE + // forces non-terse, so the compressor is skipped even though it is set. + const out = compressBlock(BIG_BLOCK, { env: { GUARDEX_COMPRESS_CMD: 'tr a-z A-Z' } }); + assert.equal(out, BIG_BLOCK); + } finally { + if (prev === undefined) { + delete process.env.GUARDEX_VERBOSE; + } else { + process.env.GUARDEX_VERBOSE = prev; + } + } +}); diff --git a/test/prompt.test.js b/test/prompt.test.js index 1e636e6f..9b1f7131 100644 --- a/test/prompt.test.js +++ b/test/prompt.test.js @@ -86,6 +86,9 @@ test('prompt outputs AI setup instructions', () => { assert.match(result.stdout, /https:\/\/github\.com\/apps\/pull/); assert.match(result.stdout, /https:\/\/github\.com\/apps\/cr-gpt/); assert.match(result.stdout, /OPENAI_API_KEY/); + assert.match(result.stdout, /Headroom context compression/); + assert.match(result.stdout, /headroom_compress/); + assert.match(result.stdout, /GUARDEX_COMPRESS_CMD/); }); @@ -138,6 +141,7 @@ test('prompt --list-parts prints the available prompt slices', () => { assert.match(result.stdout, /^install$/m); assert.match(result.stdout, /^task-loop$/m); assert.match(result.stdout, /^rtk$/m); + assert.match(result.stdout, /^headroom$/m); assert.match(result.stdout, /^openspec$/m); assert.match(result.stdout, /^review-bot$/m); }); @@ -150,6 +154,31 @@ test('prompt --exec rejects prompt-only parts', () => { assert.match(result.stderr, /Exec-capable parts:/); }); +test('prompt --part headroom outputs the headroom compression slice', () => { + const repoDir = initRepo(); + const result = runNode(['prompt', '--part', 'headroom'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /^Headroom context compression:/m); + assert.match(result.stdout, /headroom_compress/); + assert.match(result.stdout, /headroom_retrieve /); + assert.match(result.stdout, /GUARDEX_COMPRESS_CMD/); + assert.doesNotMatch(result.stdout, /GitGuardex \(gx\) setup checklist/); +}); + +test('prompt --part compress aliases to the headroom slice', () => { + const repoDir = initRepo(); + const result = runNode(['prompt', '--part', 'compress'], repoDir); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /^Headroom context compression:/m); +}); + +test('prompt --exec rejects the prompt-only headroom part', () => { + const repoDir = initRepo(); + const result = runNode(['prompt', '--exec', '--part', 'headroom'], repoDir); + assert.equal(result.status, 1, 'exec mode should reject the prompt-only headroom part'); + assert.match(result.stderr, /Prompt part 'headroom' is not available with --exec/); +}); + test('prompt --snippet prints the managed AGENTS template with token budget rules', () => { const repoDir = initRepo(); const result = runNode(['prompt', '--snippet'], repoDir); @@ -166,6 +195,30 @@ test('prompt --snippet prints the managed AGENTS template with token budget rule assert.match(result.stdout, //); }); +test('prompt --snippet routes through GUARDEX_COMPRESS_CMD when set', () => { + const repoDir = initRepo(); + const plain = runNode(['prompt', '--snippet'], repoDir); + assert.equal(plain.status, 0, plain.stderr || plain.stdout); + assert.match(plain.stdout, //); + + const compressed = runNodeWithEnv(['prompt', '--snippet'], repoDir, { + GUARDEX_COMPRESS_CMD: 'tr a-z A-Z', + }); + assert.equal(compressed.status, 0, compressed.stderr || compressed.stdout); + // `tr a-z A-Z` uppercases the whole block, proving it was piped through. + assert.match(compressed.stdout, //); + assert.doesNotMatch(compressed.stdout, //); +}); + +test('prompt --snippet is unchanged when GUARDEX_COMPRESS_CMD points at a missing binary', () => { + const repoDir = initRepo(); + const result = runNodeWithEnv(['prompt', '--snippet'], repoDir, { + GUARDEX_COMPRESS_CMD: 'guardex-no-such-binary-zzz', + }); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, //); +}); + test('deprecated copy-prompt alias still works and warns', () => { const repoDir = initRepo();