Files
gstack/bin/gstack-skill-route
T
Garry Tanandcapy-ai[bot] 18d557d30c v1.94.33.0 fix(ship-queue, hooks): selector reads the checkout under review; one SWITCH_DEFAULT per hook switch; installs are not checks; timed agent deferral (GST-1088 follow-up) (#3933)
* fix(ship-queue, hooks): selector reads the checkout under review; config lists the hook switches; installs are not checks; one deferral per running agent set (GST-1088 follow-up)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* feat(hooks): one SWITCH_DEFAULT per hook switch, read by setup, the hooks, gstack-config and gstack-skill-route (GST-1088 follow-up)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(hooks): option values are not the check verb, wrapper chains stay linear, agent stalls are timed, report re-checks deferred runs (GST-1088 follow-up review)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(hooks): tests read SWITCH_DEFAULT instead of assuming off; an unreadable config turns both hooks off whatever the default (GST-1088 follow-up)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-11-03 18:53:52 +00:00

63 lines
3.2 KiB
TypeScript
Executable File

#!/usr/bin/env -S bun --no-env-file --config=/dev/null
/**
* gstack-skill-route — show how the skill-route hook scores a prompt (GST-1088).
*
* Usage:
* gstack-skill-route --explain "<prompt>" [--json] [--threshold <n>]
* gstack-skill-route --default <skill_routing|skill_contracts>
*
* Prints the switch state, the threshold, every candidate skill with its
* score and best trigger, the route the hook would take and the exact line it
* would inject. Works with the switch off (a dry run): it never injects
* anything. --default prints a switch's SWITCH_DEFAULT (on or off), the value
* setup and gstack-config use when the switch is unset. Exit 0, or 2 on a usage error. Docs: docs/skill-routing-and-contracts.md.
*/
import {
ROUTE_THRESHOLD, SWITCH_DEFAULT, hintLine, installedSkillsDir, readSkillSwitch, routePrompt, scanInstalledSkills,
} from '../lib/skill-route';
const USAGE = 'Usage: gstack-skill-route --explain "<prompt>" [--json] [--threshold <n>]\n gstack-skill-route --default <skill_routing|skill_contracts>';
const args = process.argv.slice(2);
if (args[0] === '--help' || args[0] === '-h') { process.stdout.write(`${USAGE}\n`); process.exit(0); }
if (args[0] === '--default') {
const name = args[1] ?? '';
if (!Object.hasOwn(SWITCH_DEFAULT, name)) {
process.stderr.write(`SKILL_ROUTE_USAGE: --default needs skill_routing or skill_contracts\n${USAGE}\n`);
process.exit(2);
}
process.stdout.write(`${SWITCH_DEFAULT[name as keyof typeof SWITCH_DEFAULT]}\n`);
process.exit(0);
}
const at = args.indexOf('--explain');
const prompt = at >= 0 ? args[at + 1] : undefined;
const tAt = args.indexOf('--threshold');
const threshold = tAt >= 0 ? Number(args[tAt + 1]) : ROUTE_THRESHOLD;
if (prompt === undefined || !Number.isFinite(threshold) || threshold <= 0 || threshold > 1) {
process.stderr.write(`SKILL_ROUTE_USAGE: --explain needs a prompt; --threshold must be in (0, 1]\n${USAGE}\n`);
process.exit(2);
}
const dir = installedSkillsDir();
const { skills, problems } = scanInstalledSkills(dir);
const decision = routePrompt(prompt, skills, threshold);
const sw = readSkillSwitch('skill_routing');
const hint = decision.route ? hintLine(decision.route) : null;
if (args.includes('--json')) {
process.stdout.write(`${JSON.stringify({
switch: { on: sw.on, source: sw.source }, threshold, skills_dir: dir, skills: skills.length, problems,
candidates: decision.candidates, route: decision.route?.skill ?? null, reason: decision.route ? null : decision.reason, hint,
}, null, 2)}\n`);
process.exit(0);
}
const lines = [
`switch: skill_routing ${sw.on ? 'on' : 'off'} (${sw.source})${sw.invalid !== undefined ? `, unrecognized value ${JSON.stringify(sw.invalid)}` : ''}`,
`threshold: ${threshold}`,
`skills: ${skills.length} scanned in ${dir}${problems.length ? ` (${problems.join('; ')})` : ''}`,
'candidates:',
...(decision.candidates.length
? decision.candidates.map(c => ` ${c.score.toFixed(2)} /${c.skill} "${c.trigger}" (${c.matched} words)`)
: [' (none)']),
decision.route ? `route: /${decision.route.skill}` : `route: none (${decision.reason})`,
...(hint ? [`hint: ${hint}`] : []),
];
process.stdout.write(`${lines.join('\n')}\n`);