Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion skills/gitguardex/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ Ops: `gx branch start "<task>" "<agent>"`, `gx locks claim --branch "<agent-bran

When inspecting or verifying, prefer `rtk` compact wrappers if available (`rtk git status`, `rtk grep`, `rtk test <cmd>`, and noisy gx reads like `rtk gx status` / `rtk gx doctor`). Do not wrap commands whose stdout is parsed by scripts (`--json`, `--porcelain`, exact stdout contracts) or shell-ready output (`gx prompt --exec`).

To shrink gx's own large narrative output (e.g. `gx prompt`, `gx prompt --snippet`) before it lands in your context, set `GUARDEX_COMPRESS_CMD="<stdin->stdout filter>"`; gx routes that output through the filter (terse/non-TTY mode, fail-open, JSON skipped). Unset = byte-for-byte unchanged.
To shrink gx's own large narrative output (e.g. `gx prompt`, `gx prompt --snippet`) before it lands in your context, set `GUARDEX_COMPRESS_CMD="<stdin->stdout filter>"`; gx routes that output through the filter (terse/non-TTY mode, fail-open, JSON skipped). Unset = byte-for-byte unchanged. Confirm it is wired with `gx status` — it prints a `Token compression` line and flags a configured-but-missing binary.
12 changes: 12 additions & 0 deletions src/cli/commands/status.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const toolchainModule = require('../../toolchain');
const {
runtimeVersion,
statusDot,
describeCompressor,
printToolLogsSummary,
getInvokedCliName,
} = require('../../output');
Expand Down Expand Up @@ -180,6 +181,7 @@ function status(rawArgs) {
: null,
},
detectionError: toolchain.ok ? null : toolchain.error,
compression: describeCompressor(),
};

if (options.json) {
Expand Down Expand Up @@ -214,6 +216,16 @@ function status(rawArgs) {
console.log(` - ${statusDot(service.status)} ${serviceLabel}: ${service.status}`);
}
}
if (payload.compression.configured) {
const { command, available } = payload.compression;
if (available === false) {
console.log(
`[${TOOL_NAME}] Token compression: ${statusDot('degraded')} ${command} — not found on PATH (gx output is not compressed)`,
);
} else {
console.log(`[${TOOL_NAME}] Token compression: ${statusDot('active')} ${command}`);
}
}
const inactiveOptionalCompanions = [...npmServices, ...localCompanionServices]
.filter((service) => service.status !== 'active')
.map((service) => service.displayName || service.name);
Expand Down
52 changes: 52 additions & 0 deletions src/output/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const {
cp,
fs,
path,
packageJson,
TOOL_NAME,
Expand Down Expand Up @@ -744,6 +745,55 @@ function looksMachineReadable(text) {
return head === '{' || head === '[';
}

// isExecutableOnPath resolves whether `command` can be found, mirroring how a
// shell would locate it: an explicit path (contains a separator) is checked
// directly; a bare name is searched across PATH entries (honoring PATHEXT on
// Windows). Defensive — returns null (unknown) on any unexpected error rather
// than throwing, so callers can degrade to "configured, presence unknown".
function isExecutableOnPath(command, env = process.env) {
try {
const cmd = String(command || '').trim();
if (!cmd) {
return false;
}
const exts =
process.platform === 'win32'
? String(env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
.split(';')
.map((ext) => ext.trim())
.filter(Boolean)
: [''];
const candidates = (base) =>
exts.some((ext) => {
try {
return fs.existsSync(ext ? base + ext : base);
} catch {
return false;
}
});
if (cmd.includes('/') || cmd.includes(path.sep)) {
return candidates(cmd);
}
const dirs = String(env.PATH || '').split(path.delimiter).filter(Boolean);
return dirs.some((dir) => candidates(path.join(dir, cmd)));
} catch {
return null;
}
}

// describeCompressor reports the GUARDEX_COMPRESS_CMD token-compression setup so
// `gx status` can surface it. The feature fails open (silently prints raw output
// when the compressor is missing), which makes a misconfiguration invisible and
// quietly wastes tokens — this gives an operator a way to confirm it is wired.
// `available` is true/false when resolvable, or null when presence is unknown.
function describeCompressor(env = process.env) {
const argv = resolveCompressCommand(env);
if (!argv) {
return { configured: false, command: null, available: null };
}
return { configured: true, command: argv[0], available: isExecutableOnPath(argv[0], env) };
}

// 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
Expand Down Expand Up @@ -820,6 +870,8 @@ module.exports = {
printAutoFinishSummary,
tokenizeCommand,
resolveCompressCommand,
isExecutableOnPath,
describeCompressor,
compressBlock,
printCompressible,
};
2 changes: 1 addition & 1 deletion templates/codex/skills/gitguardex/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ Ops: `gx branch start "<task>" "<agent>"`, `gx locks claim --branch "<agent-bran

When inspecting or verifying, prefer `rtk` compact wrappers if available (`rtk git status`, `rtk grep`, `rtk test <cmd>`, and noisy gx reads like `rtk gx status` / `rtk gx doctor`). Do not wrap commands whose stdout is parsed by scripts (`--json`, `--porcelain`, exact stdout contracts) or shell-ready output (`gx prompt --exec`).

To shrink gx's own large narrative output (e.g. `gx prompt`, `gx prompt --snippet`) before it lands in your context, set `GUARDEX_COMPRESS_CMD="<stdin->stdout filter>"`; gx routes that output through the filter (terse/non-TTY mode, fail-open, JSON skipped). Unset = byte-for-byte unchanged.
To shrink gx's own large narrative output (e.g. `gx prompt`, `gx prompt --snippet`) before it lands in your context, set `GUARDEX_COMPRESS_CMD="<stdin->stdout filter>"`; gx routes that output through the filter (terse/non-TTY mode, fail-open, JSON skipped). Unset = byte-for-byte unchanged. Confirm it is wired with `gx status` — it prints a `Token compression` line and flags a configured-but-missing binary.
32 changes: 32 additions & 0 deletions test/output.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const {
compressBlock,
resolveCompressCommand,
tokenizeCommand,
isExecutableOnPath,
describeCompressor,
} = require('../src/output');

// A large lowercase block so the `tr a-z A-Z` stub visibly transforms it and it
Expand Down Expand Up @@ -94,6 +96,36 @@ test('compressBlock passes text through unchanged when no compressor is configur
assert.equal(compressBlock(BIG_BLOCK, { env: {}, force: true }), BIG_BLOCK);
});

test('isExecutableOnPath finds a bare command on PATH and rejects a missing one', () => {
const env = { PATH: process.env.PATH || '' };
// `node` is on PATH (we are running under it); a random name is not.
assert.equal(isExecutableOnPath('node', env), true);
assert.equal(isExecutableOnPath('guardex-no-such-bin-zzz', env), false);
assert.equal(isExecutableOnPath('', env), false);
});

test('isExecutableOnPath checks an explicit path directly', () => {
assert.equal(isExecutableOnPath(process.execPath, { PATH: '' }), true);
assert.equal(isExecutableOnPath('/no/such/guardex/bin', { PATH: '' }), false);
});

test('describeCompressor reports configuration and binary availability', () => {
assert.deepEqual(describeCompressor({}), {
configured: false,
command: null,
available: null,
});
assert.deepEqual(describeCompressor({ GUARDEX_COMPRESS_CMD: 'node --version', PATH: process.env.PATH }), {
configured: true,
command: 'node',
available: true,
});
assert.deepEqual(
describeCompressor({ GUARDEX_COMPRESS_CMD: 'guardex-no-such-bin-zzz', PATH: process.env.PATH }),
{ configured: true, command: 'guardex-no-such-bin-zzz', available: false },
);
});

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());
Expand Down
44 changes: 44 additions & 0 deletions test/status.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,50 @@ exit 1
assert.equal(ghService.status, 'active');
});

test('status --json reports token compression as configured + available when the binary resolves', () => {
const repoDir = initRepo();
const result = runNodeWithEnv(['status', '--target', repoDir, '--json'], repoDir, {
GUARDEX_COMPRESS_CMD: 'tr a-z A-Z',
});
assert.equal(result.status, 0, result.stderr || result.stdout);
const payload = JSON.parse(result.stdout);
assert.equal(payload.compression.configured, true);
assert.equal(payload.compression.command, 'tr');
assert.equal(payload.compression.available, true);
});

test('status --json reports token compression as unconfigured when GUARDEX_COMPRESS_CMD is unset', () => {
const repoDir = initRepo();
const result = runNodeWithEnv(['status', '--target', repoDir, '--json'], repoDir, {
GUARDEX_COMPRESS_CMD: '',
});
assert.equal(result.status, 0, result.stderr || result.stdout);
const payload = JSON.parse(result.stdout);
assert.equal(payload.compression.configured, false);
assert.equal(payload.compression.command, null);
assert.equal(payload.compression.available, null);
});

test('status surfaces a degraded line when the configured compressor binary is missing', () => {
const repoDir = initRepo();
const missing = runNodeWithEnv(['status', '--target', repoDir], repoDir, {
GUARDEX_COMPRESS_CMD: 'guardex-no-such-compressor-zzz',
});
assert.equal(missing.status, 0, missing.stderr || missing.stdout);
assert.match(
missing.stdout,
/Token compression: .*guardex-no-such-compressor-zzz — not found on PATH/,
);

// A resolvable binary prints the active line without the "not found" warning.
const present = runNodeWithEnv(['status', '--target', repoDir], repoDir, {
GUARDEX_COMPRESS_CMD: 'tr a-z A-Z',
});
assert.equal(present.status, 0, present.stderr || present.stdout);
assert.match(present.stdout, /Token compression: .*\btr\b/);
assert.doesNotMatch(present.stdout, /not found on PATH/);
});


test('warning-only degraded status avoids zero-error wording and points humans at doctor', () => {
const repoDir = initRepo();
Expand Down
Loading