Files
gstack/bin/gstack-learnings-search
Garry Tan ae0a9ad195 feat: GStack Learns — per-project self-learning infrastructure (v0.13.4.0) (#622)
* feat: learnings + confidence resolvers — cross-skill memory infrastructure

Three new resolvers for the self-learning system:
- LEARNINGS_SEARCH: tells skills to load prior learnings before analysis
- LEARNINGS_LOG: tells skills to capture discoveries after completing work
- CONFIDENCE_CALIBRATION: adds 1-10 confidence scoring to all review findings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: learnings bin scripts — append-only JSONL read/write

gstack-learnings-log: validates JSON, auto-injects timestamp, appends to
~/.gstack/projects/$SLUG/learnings.jsonl. Append-only (no mutation).

gstack-learnings-search: reads/filters/dedupes learnings with confidence
decay (observed/inferred lose 1pt/30d), cross-project discovery, and
"latest winner" resolution per key+type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: learnings count in preamble output

Every skill now prints "LEARNINGS: N entries loaded" during preamble,
making the compounding loop visible to the user.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: integrate learnings + confidence into 9 skill templates

Add {{LEARNINGS_SEARCH}}, {{LEARNINGS_LOG}}, and {{CONFIDENCE_CALIBRATION}}
placeholders to review, ship, plan-eng-review, plan-ceo-review, office-hours,
investigate, retro, and cso templates. Regenerated all SKILL.md files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: /learn skill — manage project learnings

New skill for reviewing, searching, pruning, and exporting what gstack
has learned across sessions. Commands: /learn, /learn search, /learn prune,
/learn export, /learn stats, /learn add.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: self-learning roadmap — 5-release design doc

Covers: R1 GStack Learns (v0.14), R2 Review Army (v0.15), R3 Smart Ceremony
(v0.16), R4 /autoship (v0.17), R5 Studio (v0.18). Inspired by Compound
Engineering, adapted to GStack's architecture.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: learnings bin script unit tests — 13 tests, free

Tests gstack-learnings-log (valid/invalid JSON, timestamp injection,
append-only) and gstack-learnings-search (dedup, type/query/limit filters,
confidence decay, user-stated no-decay, malformed JSONL skip).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.13.4.0)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: learnings resolver + bin script edge case tests — 21 new tests, free

Adds gen-skill-docs coverage for LEARNINGS_SEARCH, LEARNINGS_LOG, and
CONFIDENCE_CALIBRATION resolvers. Adds bin script edge cases: timestamp
preservation, special characters, files array, sort order, type grouping,
combined filtering, missing fields, confidence floor at 0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: sync package.json version with VERSION file (0.13.4.0)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: gitignore .factory/ — generated output, not source

Same pattern as .claude/skills/ and .agents/. These SKILL.md files are
generated from .tmpl templates by gen:skill-docs --host factory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: /learn E2E — seed 3 learnings, verify agent surfaces them

Seeds N+1 query pattern, stale cache pitfall, and rubocop preference
into learnings.jsonl, then runs /learn and checks that at least 2/3
appear in the agent's output. Gate tier, ~$0.25/run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:02:01 -06:00

132 lines
4.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# gstack-learnings-search — read and filter project learnings
# Usage: gstack-learnings-search [--type TYPE] [--query KEYWORD] [--limit N] [--cross-project]
#
# Reads ~/.gstack/projects/$SLUG/learnings.jsonl, applies confidence decay,
# resolves duplicates (latest winner per key+type), and outputs formatted text.
# Exit 0 silently if no learnings file exists.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/gstack-slug" 2>/dev/null)"
GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
TYPE=""
QUERY=""
LIMIT=10
CROSS_PROJECT=false
while [[ $# -gt 0 ]]; do
case "$1" in
--type) TYPE="$2"; shift 2 ;;
--query) QUERY="$2"; shift 2 ;;
--limit) LIMIT="$2"; shift 2 ;;
--cross-project) CROSS_PROJECT=true; shift ;;
*) shift ;;
esac
done
LEARNINGS_FILE="$GSTACK_HOME/projects/$SLUG/learnings.jsonl"
# Collect all JSONL files to search
FILES=()
[ -f "$LEARNINGS_FILE" ] && FILES+=("$LEARNINGS_FILE")
if [ "$CROSS_PROJECT" = true ]; then
# Add other projects' learnings (max 5, sorted by mtime)
for f in $(find "$GSTACK_HOME/projects" -name "learnings.jsonl" -not -path "*/$SLUG/*" 2>/dev/null | head -5); do
FILES+=("$f")
done
fi
if [ ${#FILES[@]} -eq 0 ]; then
exit 0
fi
# Process all files through bun for JSON parsing, decay, dedup, filtering
cat "${FILES[@]}" 2>/dev/null | bun -e "
const lines = (await Bun.stdin.text()).trim().split('\n').filter(Boolean);
const now = Date.now();
const type = '${TYPE}';
const query = '${QUERY}'.toLowerCase();
const limit = ${LIMIT};
const slug = '${SLUG}';
const entries = [];
for (const line of lines) {
try {
const e = JSON.parse(line);
if (!e.key || !e.type) continue;
// Apply confidence decay: observed/inferred lose 1pt per 30 days
let conf = e.confidence || 5;
if (e.source === 'observed' || e.source === 'inferred') {
const days = Math.floor((now - new Date(e.ts).getTime()) / 86400000);
conf = Math.max(0, conf - Math.floor(days / 30));
}
e._effectiveConfidence = conf;
// Determine if this is from the current project or cross-project
// Cross-project entries are tagged for display
e._crossProject = !line.includes(slug) && '${CROSS_PROJECT}' === 'true';
entries.push(e);
} catch {}
}
// Dedup: latest winner per key+type
const seen = new Map();
for (const e of entries) {
const dk = e.key + '|' + e.type;
const existing = seen.get(dk);
if (!existing || new Date(e.ts) > new Date(existing.ts)) {
seen.set(dk, e);
}
}
let results = Array.from(seen.values());
// Filter by type
if (type) results = results.filter(e => e.type === type);
// Filter by query
if (query) results = results.filter(e =>
(e.key || '').toLowerCase().includes(query) ||
(e.insight || '').toLowerCase().includes(query) ||
(e.files || []).some(f => f.toLowerCase().includes(query))
);
// Sort by effective confidence desc, then recency
results.sort((a, b) => {
if (b._effectiveConfidence !== a._effectiveConfidence) return b._effectiveConfidence - a._effectiveConfidence;
return new Date(b.ts).getTime() - new Date(a.ts).getTime();
});
// Limit
results = results.slice(0, limit);
if (results.length === 0) process.exit(0);
// Format output
const byType = {};
for (const e of results) {
const t = e.type || 'unknown';
if (!byType[t]) byType[t] = [];
byType[t].push(e);
}
// Summary line
const counts = Object.entries(byType).map(([t, arr]) => arr.length + ' ' + t + (arr.length > 1 ? 's' : ''));
console.log('LEARNINGS: ' + results.length + ' loaded (' + counts.join(', ') + ')');
console.log('');
for (const [t, arr] of Object.entries(byType)) {
console.log('## ' + t.charAt(0).toUpperCase() + t.slice(1) + 's');
for (const e of arr) {
const cross = e._crossProject ? ' [cross-project]' : '';
const files = e.files?.length ? ' (files: ' + e.files.join(', ') + ')' : '';
console.log('- [' + e.key + '] (confidence: ' + e._effectiveConfidence + '/10, ' + e.source + ', ' + (e.ts || '').split('T')[0] + ')' + cross);
console.log(' ' + e.insight + files);
}
console.log('');
}
" 2>/dev/null || exit 0