mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-07 16:48:01 +02:00
v1.33.1.0 fix(learnings): token-OR query + task-shaped retrieval in 3 long skills (#1442)
* fix(learnings): use token-OR matching in gstack-learnings-search --query
Split the query on whitespace into tokens; a learning matches if ANY
token appears as a substring in ANY of key/insight/files. Previously
the whole query was a single substring, so multi-word queries like
"debug investigation" only matched learnings whose insight contained
that exact contiguous phrase, which is usually nothing.
Whitespace-only query falls through to no-query (matches today's no-flag
behavior). Single-word queries behave exactly as before.
Adds test/gstack-learnings-search.test.ts: 3 assertions covering
multi-token, single-token, and no-query backwards compat.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(resolver): parameterized LEARNINGS_SEARCH with shell-injection guard
The {{LEARNINGS_SEARCH}} macro now accepts a query=KEYWORD argument that
gets interpolated as --query "<keyword>" into the generated bash. Empty
value falls through to no-query (principle of least surprise: a stray
{{LEARNINGS_SEARCH:query=}} placeholder gets today's behavior, not a
build failure). Pattern reuses the parameterized-macro parsing from
composition.ts. The 13 templates that don't pass a query stay
byte-identical in their generated SKILL.md output.
Shell-injection guard: the query value is whitelisted to
^[A-Za-z0-9 _-]+$ at gen-skill-docs time. Any \$(), backticks,
semicolons, or quotes throw a loud build error instead of emitting
executable bash. Static template queries are safe by inspection;
this defends against future contributors writing dangerous values.
Adds 5 assertions to test/gen-skill-docs.test.ts covering no-args,
claude+query=foo bar on both cross-project and project-scoped branches,
codex host variant, empty value semantics, and shell-injection payloads
(\$(whoami), backticks, ;, &, ", \\, \$x) throwing build errors.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(skills): task-shaped queries + mid-flow refresh in /investigate /qa /ship
The three long skills now pull learnings keyed to their theme at the
top, then re-pull at phase boundaries as work shifts to new sub-tasks.
Top-of-skill queries (5-6 token unions, token-OR matched):
- investigate: "debug investigation root cause hypothesis bug fix"
- qa: "qa testing bug regression flake fixture"
- ship: "release ship version changelog merge pr"
Mid-flow refresh blocks (concrete keyword recipe + worked examples):
- investigate: between Phase 1 (hypothesis) and Phase 2 (analysis),
keyed to the hypothesis noun. Examples: auth-cookie, session-expiry.
- qa: between Phase 7 (triage) and Phase 8 (fix loop), keyed to the
buggy component name. Examples: checkout-button, signup-form.
- ship: just before Step 12 (VERSION bump), keyed to the headline
feature. Examples: learnings-search, pacing, worktree-ship.
Keyword recipe enforces alphanumeric+hyphen only (no quotes, slashes,
dots, colons) so dynamic queries cannot inject shell metacharacters.
The other 13 short-lived skills keep the bare {{LEARNINGS_SEARCH}} form.
Backwards-compat verified via diff: their generated SKILL.md output is
byte-identical to before this change.
Golden ship fixtures regenerated to match the new ship/SKILL.md output.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: bump version and changelog (v1.33.1.0)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test: refresh codex+factory ship golden fixtures
Follow-up to 513c9660 — the codex and factory host outputs needed
regeneration too, missed in the initial commit because gen:skill-docs
was only run for the claude host. Now matches gen:skill-docs --host all.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3047,3 +3047,51 @@ describe('GSTACK REVIEW REPORT delete-then-append flow', () => {
|
||||
expect(src).not.toContain('If it was found mid-file, move it');
|
||||
});
|
||||
});
|
||||
|
||||
describe('LEARNINGS_SEARCH resolver: query parameter', () => {
|
||||
// Lazy-load resolver and types after describe block to keep test file self-contained.
|
||||
const { generateLearningsSearch } = require('../scripts/resolvers/learnings');
|
||||
const { HOST_PATHS } = require('../scripts/resolvers/types');
|
||||
|
||||
const claudeCtx = {
|
||||
skillName: 'test',
|
||||
tmplPath: 'test/SKILL.md.tmpl',
|
||||
host: 'claude',
|
||||
paths: HOST_PATHS.claude,
|
||||
};
|
||||
const codexCtx = { ...claudeCtx, host: 'codex', paths: HOST_PATHS.codex };
|
||||
|
||||
test('no args → bash does not contain --query (backwards-compat)', () => {
|
||||
const out = generateLearningsSearch(claudeCtx);
|
||||
expect(out).not.toContain('--query');
|
||||
});
|
||||
|
||||
test('claude host + query=foo bar → both cross-project and project-scoped branches contain --query', () => {
|
||||
const out = generateLearningsSearch(claudeCtx, ['query=foo bar']);
|
||||
// Both branches of the if/else must carry the flag.
|
||||
const lines = out.split('\n').filter(l => l.includes('gstack-learnings-search'));
|
||||
expect(lines.length).toBeGreaterThanOrEqual(2);
|
||||
for (const line of lines) {
|
||||
expect(line).toContain('--query "foo bar"');
|
||||
}
|
||||
});
|
||||
|
||||
test('codex host + query=foo bar → codex bash variant contains --query', () => {
|
||||
const out = generateLearningsSearch(codexCtx, ['query=foo bar']);
|
||||
expect(out).toContain('--query "foo bar"');
|
||||
expect(out).toContain('$GSTACK_BIN/gstack-learnings-search');
|
||||
});
|
||||
|
||||
test('empty value query= → bash does not contain --query (locked semantics: falls through)', () => {
|
||||
const claudeOut = generateLearningsSearch(claudeCtx, ['query=']);
|
||||
expect(claudeOut).not.toContain('--query');
|
||||
const codexOut = generateLearningsSearch(codexCtx, ['query=']);
|
||||
expect(codexOut).not.toContain('--query');
|
||||
});
|
||||
|
||||
test('shell-injection chars in query= → throws at gen-time (defense in depth)', () => {
|
||||
for (const bad of ['$(whoami)', '`cmd`', 'a;b', 'a&b', 'a"b', 'a\\b', 'foo$x']) {
|
||||
expect(() => generateLearningsSearch(claudeCtx, [`query=${bad}`])).toThrow(/alphanumeric/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user