Files
gstack/scripts/resolvers/learnings.ts
T
Garry Tan 1a4f0c9c15 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>
2026-05-11 19:34:33 -07:00

118 lines
5.0 KiB
TypeScript

/**
* Learnings resolver — cross-skill institutional memory
*
* Learnings are stored per-project at ~/.gstack/projects/{slug}/learnings.jsonl.
* Each entry is a JSONL line with: ts, skill, type, key, insight, confidence,
* source, branch, commit, files[].
*
* Storage is append-only. Duplicates (same key+type) are resolved at read time
* by gstack-learnings-search ("latest winner" per key+type).
*
* Cross-project discovery is opt-in. The resolver asks the user once via
* AskUserQuestion and persists the preference via gstack-config.
*/
import type { TemplateContext } from './types';
// Whitelist for query= macro values. Allows alphanumeric, space, hyphen, underscore.
// Anything else (e.g. $, backticks, quotes, ;) is a shell-injection vector when the
// emitted bash interpolates the value into `--query "${queryArg}"`. Static template
// queries hand-written in gstack are safe, but the resolver API must defend against
// future contributors writing dangerous values.
const QUERY_SAFE_RE = /^[A-Za-z0-9 _-]+$/;
export function generateLearningsSearch(ctx: TemplateContext, args?: string[]): string {
// Parse query= arg. Empty value falls through to no-query (principle of least surprise:
// a stray {{LEARNINGS_SEARCH:query=}} placeholder gets today's behavior, not a build error).
const queryArg = (args || [])
.filter(a => a.startsWith('query='))
.map(a => a.slice(6))
.filter(Boolean)[0];
if (queryArg && !QUERY_SAFE_RE.test(queryArg)) {
throw new Error(
`{{LEARNINGS_SEARCH:query=...}} value must match ${QUERY_SAFE_RE} (alphanumeric, space, hyphen, underscore). Got: ${JSON.stringify(queryArg)}`
);
}
const queryFlag = queryArg ? ` --query "${queryArg}"` : '';
if (ctx.host === 'codex') {
// Codex: simpler version, no cross-project, uses $GSTACK_BIN
return `## Prior Learnings
Search for relevant learnings from previous sessions on this project:
\`\`\`bash
$GSTACK_BIN/gstack-learnings-search --limit 10${queryFlag} 2>/dev/null || true
\`\`\`
If learnings are found, incorporate them into your analysis. When a review finding
matches a past learning, note it: "Prior learning applied: [key] (confidence N, from [date])"`;
}
return `## Prior Learnings
Search for relevant learnings from previous sessions:
\`\`\`bash
_CROSS_PROJ=$(${ctx.paths.binDir}/gstack-config get cross_project_learnings 2>/dev/null || echo "unset")
echo "CROSS_PROJECT: $_CROSS_PROJ"
if [ "$_CROSS_PROJ" = "true" ]; then
${ctx.paths.binDir}/gstack-learnings-search --limit 10${queryFlag} --cross-project 2>/dev/null || true
else
${ctx.paths.binDir}/gstack-learnings-search --limit 10${queryFlag} 2>/dev/null || true
fi
\`\`\`
If \`CROSS_PROJECT\` is \`unset\` (first time): Use AskUserQuestion:
> gstack can search learnings from your other projects on this machine to find
> patterns that might apply here. This stays local (no data leaves your machine).
> Recommended for solo developers. Skip if you work on multiple client codebases
> where cross-contamination would be a concern.
Options:
- A) Enable cross-project learnings (recommended)
- B) Keep learnings project-scoped only
If A: run \`${ctx.paths.binDir}/gstack-config set cross_project_learnings true\`
If B: run \`${ctx.paths.binDir}/gstack-config set cross_project_learnings false\`
Then re-run the search with the appropriate flag.
If learnings are found, incorporate them into your analysis. When a review finding
matches a past learning, display:
**"Prior learning applied: [key] (confidence N/10, from [date])"**
This makes the compounding visible. The user should see that gstack is getting
smarter on their codebase over time.`;
}
export function generateLearningsLog(ctx: TemplateContext): string {
const binDir = ctx.host === 'codex' ? '$GSTACK_BIN' : ctx.paths.binDir;
return `## Capture Learnings
If you discovered a non-obvious pattern, pitfall, or architectural insight during
this session, log it for future sessions:
\`\`\`bash
${binDir}/gstack-learnings-log '{"skill":"${ctx.skillName}","type":"TYPE","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":N,"source":"SOURCE","files":["path/to/relevant/file"]}'
\`\`\`
**Types:** \`pattern\` (reusable approach), \`pitfall\` (what NOT to do), \`preference\`
(user stated), \`architecture\` (structural decision), \`tool\` (library/framework insight),
\`operational\` (project environment/CLI/workflow knowledge).
**Sources:** \`observed\` (you found this in the code), \`user-stated\` (user told you),
\`inferred\` (AI deduction), \`cross-model\` (both Claude and Codex agree).
**Confidence:** 1-10. Be honest. An observed pattern you verified in the code is 8-9.
An inference you're not sure about is 4-5. A user preference they explicitly stated is 10.
**files:** Include the specific file paths this learning references. This enables
staleness detection: if those files are later deleted, the learning can be flagged.
**Only log genuine discoveries.** Don't log obvious things. Don't log things the user
already knows. A good test: would this insight save time in a future session? If yes, log it.`;
}