mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-13 20:07:08 +01:00
* investigate: opt-in gate that holds root cause before fix, through Bash too (GST-1461) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * investigate gate: posix paths in edit blocks, CI fixes (GST-1461) - hook: the edit branch computes the repo-relative path once in posix form, so the marker and deny text read src/total.ts on Windows too - lib/review-depth.ts is back to main's pinned bytes; lib/investigate-gate.ts keeps its own copy of TEST_PATH - gstack-skill-gates report: destructure session_id (the pregate secrets scan read the old line as a secret-named assignment) - tests: pin core.autocrlf=false in the git fixtures, skip the symlink swap on Windows, give setup-hook-syntax-gate's fixture the shared TypeScript entry a shim runs, unit-test excludedReason, and check the smoke's pinned Claude Code version against docs/skill-gates.md Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes (GST-1461) - checkTree saves the bytes a restore replaces in the session directory and the block names the copy, so a concurrent editor save is never lost - a new commit is undone to its newest ancestor that existed before the baseline or that a remote-tracking ref holds, not to the baseline HEAD: a fix committed on another branch moves that branch back to its own tip, and pull/merge of fetched upstream commits is not undone; the block lists the commit's other changes left staged - runGateHook takes the switch reading from its caller, so the investigate_gate config key has a literal reader (gstack-config-defaults) - the slow checkout case gets an explicit 30 s test timeout Review-Finding: p1/specialist-batched#1 reader=security Review-Finding: p1/red-team#1 reader=red-team Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes, round 2 (GST-1461) - an amend or rebase of commits from before the baseline on the same branch is undone to the baseline HEAD, not to its parent; on a different branch the gate blocks but leaves the commit in place - existedBefore answers reachability with one rev-list call instead of one merge-base per ref, so a fix commit in a repo with hundreds of refs is still undone inside the 5 s hook deadline Review-Finding: p2/specialist-batched#1 reader=testing Review-Finding: p2/red-team#1 reader=red-team 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>
63 lines
3.2 KiB
TypeScript
Executable File
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|investigate_gate>
|
|
*
|
|
* 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|investigate_gate>';
|
|
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, skill_contracts or investigate_gate\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`);
|