learnings.ts
144 lines · scripts/resolvers/learnings.ts at 3f79d81
1/**
2 * Learnings resolver — cross-skill institutional memory
3 *
4 * Learnings are stored per-project at ~/.gstack/projects/{slug}/learnings.jsonl.
5 * Each entry is a JSONL line with: ts, skill, type, key, insight, confidence,
6 * source, branch, commit, files[].
7 *
8 * Storage is append-only. Duplicates (same key+type) are resolved at read time
9 * by gstack-learnings-search ("latest winner" per key+type).
10 *
11 * Cross-project discovery is opt-in. The resolver asks the user once via
12 * AskUserQuestion and persists the preference via gstack-config.
13 */
14import type { TemplateContext } from './types';
15import { getHostConfig } from '../../hosts/index';
17// Whitelist for query= macro values. Allows alphanumeric, space, hyphen, underscore.
18// Anything else (e.g. $, backticks, quotes, ;) is a shell-injection vector when the
19// emitted bash interpolates the value into `--query "${queryArg}"`. Static template
20// queries hand-written in gstack are safe, but the resolver API must defend against
21// future contributors writing dangerous values.
22const QUERY_SAFE_RE = /^[A-Za-z0-9 _-]+$/;
24/**
25 * B5 (#2790): run a learnings search with its stdout intact and its stderr and
26 * exit status kept, so a missing bun or a failed script prints why instead of
27 * reading as "nothing recorded". Two lines: the capture, then the verdict.
28 */
29export const learningsCapture = (command: string) => `{ _LE=$(${command} 2>&1 >&3 3>&-); _LR=$?; } 3>&1`;
30export const LEARNINGS_VERDICT = `[ "$_LR" = 0 ] || { _LE=\${_LE%%$'\\n'*}; echo "LEARNINGS: unavailable (\${_LE:-exit $_LR})"; }`;
32export function generateLearningsSearch(ctx: TemplateContext, args?: string[]): string {
33 // Parse query= arg. Empty value falls through to no-query (principle of least surprise:
34 // a stray {{LEARNINGS_SEARCH:query=}} placeholder gets today's behavior, not a build error).
35 const queryArg = (args || [])
36 .filter(a => a.startsWith('query='))
37 .map(a => a.slice(6))
38 .filter(Boolean)[0];
39 if (queryArg && !QUERY_SAFE_RE.test(queryArg)) {
40 throw new Error(
41 `{{LEARNINGS_SEARCH:query=...}} value must match ${QUERY_SAFE_RE} (alphanumeric, space, hyphen, underscore). Got: ${JSON.stringify(queryArg)}`
42 );
43 }
44 const queryFlag = queryArg ? ` --query "${queryArg}"` : '';
45 const findingKind = ctx.skillName === 'qa' || ctx.skillName === 'qa-only' ? 'QA' : 'review';
47 if (ctx.skillName === 'qa-only') {
48 return `## Prior Learnings
50Read this project's existing learnings.jsonl only if its directory is already known
51and the caller permits that Read. Otherwise skip this optional lookup.
52${queryArg ? `Look for notes matching "${queryArg}".\n` : ''}Do not run gstack-learnings-search here: its slug helper can update a cache.
53Do not change configuration, enable cross-project search or create a learning store.
55Treat old notes as leads, not proof. When a QA finding matches a past learning,
56cite it as "Prior learning applied: [key] (confidence N/10, from [date])" and verify
57the current behavior. Reading old notes never requires writing new ones.`;
58 }
60 if (getHostConfig(ctx.host).learningsMode === 'basic') {
61 // Basic learnings mode (host config learningsMode: 'basic' — every host
62 // except claude and factory): simpler version, no cross-project prompt,
63 // uses $GSTACK_BIN (all basic hosts are env-var hosts)
64 return `## Prior Learnings
66Search for relevant learnings from previous sessions on this project:
68\`\`\`bash
69${learningsCapture(`$GSTACK_BIN/gstack-learnings-search --limit 10${queryFlag}`)}
70${LEARNINGS_VERDICT}
71\`\`\`
73If learnings are found, incorporate them into your analysis. When a ${findingKind} finding
74matches a past learning, note it: "Prior learning applied: [key] (confidence N, from [date])"`;
75 }
77 return `## Prior Learnings
79Search for relevant learnings from previous sessions:
81\`\`\`bash
82_CROSS_PROJ=$(${ctx.paths.binDir}/gstack-config get cross_project_learnings 2>/dev/null || echo "unset")
83echo "CROSS_PROJECT: $_CROSS_PROJ"
84if [ "$_CROSS_PROJ" = "true" ]; then
85 ${learningsCapture(`${ctx.paths.binDir}/gstack-learnings-search --limit 10${queryFlag} --cross-project`)}
86else
87 ${learningsCapture(`${ctx.paths.binDir}/gstack-learnings-search --limit 10${queryFlag}`)}
88fi
89${LEARNINGS_VERDICT}
90\`\`\`
92If \`CROSS_PROJECT\` is \`unset\` (first time): ${ctx.skillName === 'plan-eng-review' ? 'Build a full decision brief from these facts and options using the preamble format, then ask and wait:' : 'Use AskUserQuestion:'}
94> gstack can search learnings from your other projects on this machine to find
95> patterns that might apply here. This stays local (no data leaves your machine).
96> Recommended for solo developers. Skip if you work on multiple client codebases
97> where cross-contamination would be a concern.
99Options:
100- A) Enable cross-project learnings (recommended)
101- B) Keep learnings project-scoped only
103If A: run \`${ctx.paths.binDir}/gstack-config set cross_project_learnings true\`
104If B: run \`${ctx.paths.binDir}/gstack-config set cross_project_learnings false\`
106Then re-run the search with the appropriate flag.
108If learnings are found, incorporate them into your analysis. When a ${findingKind} finding
109matches a past learning, display:
111**"Prior learning applied: [key] (confidence N/10, from [date])"**
113This makes the compounding visible. The user should see that gstack is getting
114smarter on their codebase over time.`;
115}
117export function generateLearningsLog(ctx: TemplateContext): string {
118 const binDir = ctx.paths.binDir; // env-var hosts already resolve to $GSTACK_BIN via types.ts
120 return `## Capture Learnings
122If you discovered a non-obvious pattern, pitfall, or architectural insight during
123this session, log it for future sessions:
125\`\`\`bash
126${binDir}/gstack-learnings-log '{"skill":"${ctx.skillName}","type":"TYPE","key":"SHORT_KEY","insight":"DESCRIPTION","confidence":N,"source":"SOURCE","files":["path/to/relevant/file"]}'
127\`\`\`
129**Types:** \`pattern\` (reusable approach), \`pitfall\` (what NOT to do), \`preference\`
130(user stated), \`architecture\` (structural decision), \`tool\` (library/framework insight),
131\`operational\` (project environment/CLI/workflow knowledge).
133**Sources:** \`observed\` (you found this in the code), \`user-stated\` (user told you),
134\`inferred\` (AI deduction), \`cross-model\` (both Claude and Codex agree).
136**Confidence:** 1-10. Be honest. An observed pattern you verified in the code is 8-9.
137An inference you're not sure about is 4-5. A user preference they explicitly stated is 10.
139**files:** Include the specific file paths this learning references. This enables
140staleness detection: if those files are later deleted, the learning can be flagged.
142**Only log genuine discoveries.** Don't log obvious things. Don't log things the user
143already knows. A good test: would this insight save time in a future session? If yes, log it.`;
144}