mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
8115951284
* refactor: remove dead contributor mode, replace with operational self-improvement slot Contributor mode never fired in 18 days of heavy use (required manual opt-in via gstack-config, gated behind _CONTRIB=true, wrote disconnected markdown). Removes: generateContributorMode(), _CONTRIB bash var, 2 E2E tests, touchfile entry, doc references. Cleans up skip-lists in plan-ceo-review, autoplan, review resolver, and document-release templates. The operational self-improvement system (next commit) replaces this slot with automatic learning capture that requires no opt-in. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: operational self-improvement — every skill learns from failures Adds universal operational learning capture to the preamble completion protocol. At the end of every skill session, the agent reflects on CLI failures, wrong approaches, and project quirks, logging them as type "operational" to the learnings JSONL. Future sessions surface these automatically. - generateCompletionStatus(ctx) now includes operational capture section - Preamble bash shows top 3 learnings inline when count > 5 - New "operational" type in generateLearningsLog alongside pattern/pitfall/etc - Updated unit tests + operational seed entry in learnings E2E Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: wire learnings into all insight-producing skills Adds LEARNINGS_SEARCH and/or LEARNINGS_LOG to 10 skill templates that produce reusable insights but were previously disconnected from the learning system: - office-hours, plan-ceo-review, plan-eng-review: add LOG (had SEARCH) - plan-design-review: add both SEARCH + LOG (had neither) - design-review, design-consultation, cso, qa, qa-only: add both - retro: add SEARCH (had LOG) 13 skills now fully participate in the learning loop (read + write). Every review, QA, investigation, and design session both consults prior learnings and contributes new ones. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add operational-learning E2E test (gate-tier) Validates the write path: agent encounters a CLI failure, logs an operational learning to JSONL via gstack-learnings-log. Replaces the removed contributor-mode E2E test. Setup: temp git repo, copy bin scripts, set GSTACK_HOME. Prompt: simulated npm test failure needing --experimental-vm-modules. Assert: learnings.jsonl exists with type=operational entry. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: learnings-show E2E slug mismatch — seed at computed slug, not hardcoded The test seeded learnings at projects/test-project/ but gstack-slug computes the slug from basename(workDir) when no git remote exists. The agent's search looked at the wrong path and found nothing. Fix: compute slug the same way gstack-slug does (basename + sanitize) and seed the learnings there. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.13.8.0) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
139 lines
5.4 KiB
TypeScript
139 lines
5.4 KiB
TypeScript
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
import { runSkillTest } from './helpers/session-runner';
|
|
import {
|
|
ROOT, runId, evalsEnabled,
|
|
describeIfSelected, testConcurrentIfSelected,
|
|
copyDirSync, logCost, recordE2E,
|
|
createEvalCollector, finalizeEvalCollector,
|
|
} from './helpers/e2e-helpers';
|
|
import { spawnSync } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
|
|
const evalCollector = createEvalCollector('e2e-learnings');
|
|
|
|
// --- Learnings E2E: seed learnings, run /learn, verify output ---
|
|
|
|
describeIfSelected('Learnings E2E', ['learnings-show'], () => {
|
|
let workDir: string;
|
|
let gstackHome: string;
|
|
|
|
beforeAll(() => {
|
|
workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-e2e-learnings-'));
|
|
gstackHome = path.join(workDir, '.gstack-home');
|
|
|
|
// Init git repo
|
|
const run = (cmd: string, args: string[]) =>
|
|
spawnSync(cmd, args, { cwd: workDir, stdio: 'pipe', timeout: 5000 });
|
|
run('git', ['init', '-b', 'main']);
|
|
run('git', ['config', 'user.email', 'test@test.com']);
|
|
run('git', ['config', 'user.name', 'Test']);
|
|
fs.writeFileSync(path.join(workDir, 'app.ts'), 'console.log("hello");\n');
|
|
run('git', ['add', '.']);
|
|
run('git', ['commit', '-m', 'initial']);
|
|
|
|
// Copy the /learn skill
|
|
copyDirSync(path.join(ROOT, 'learn'), path.join(workDir, 'learn'));
|
|
|
|
// Copy bin scripts needed by /learn
|
|
const binDir = path.join(workDir, 'bin');
|
|
fs.mkdirSync(binDir, { recursive: true });
|
|
for (const script of ['gstack-learnings-search', 'gstack-learnings-log', 'gstack-slug']) {
|
|
fs.copyFileSync(path.join(ROOT, 'bin', script), path.join(binDir, script));
|
|
fs.chmodSync(path.join(binDir, script), 0o755);
|
|
}
|
|
|
|
// Seed learnings JSONL — slug must match what gstack-slug computes.
|
|
// With no git remote, gstack-slug falls back to basename(workDir).
|
|
const slug = path.basename(workDir).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
const projectDir = path.join(gstackHome, 'projects', slug);
|
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
|
|
const learnings = [
|
|
{
|
|
skill: 'review', type: 'pattern', key: 'n-plus-one-queries',
|
|
insight: 'ActiveRecord associations in loops cause N+1 queries. Always use includes/preload.',
|
|
confidence: 9, source: 'observed', ts: new Date().toISOString(),
|
|
files: ['app/models/user.rb'],
|
|
},
|
|
{
|
|
skill: 'investigate', type: 'pitfall', key: 'stale-cache-after-deploy',
|
|
insight: 'Redis cache not invalidated on deploy causes stale data for 5 minutes.',
|
|
confidence: 7, source: 'observed', ts: new Date().toISOString(),
|
|
files: ['config/redis.yml'],
|
|
},
|
|
{
|
|
skill: 'ship', type: 'preference', key: 'always-run-rubocop',
|
|
insight: 'User wants rubocop to run before every commit, no exceptions.',
|
|
confidence: 10, source: 'user-stated', ts: new Date().toISOString(),
|
|
},
|
|
{
|
|
skill: 'qa', type: 'operational', key: 'test-timeout-flag',
|
|
insight: 'bun test requires --timeout 30000 for E2E tests in this project.',
|
|
confidence: 9, source: 'observed', ts: new Date().toISOString(),
|
|
},
|
|
];
|
|
|
|
fs.writeFileSync(
|
|
path.join(projectDir, 'learnings.jsonl'),
|
|
learnings.map(l => JSON.stringify(l)).join('\n') + '\n',
|
|
);
|
|
});
|
|
|
|
afterAll(() => {
|
|
try { fs.rmSync(workDir, { recursive: true, force: true }); } catch {}
|
|
finalizeEvalCollector(evalCollector);
|
|
});
|
|
|
|
testConcurrentIfSelected('learnings-show', async () => {
|
|
const result = await runSkillTest({
|
|
prompt: `Read the file learn/SKILL.md for the /learn skill instructions.
|
|
|
|
Run the /learn command (no arguments — show recent learnings).
|
|
|
|
IMPORTANT:
|
|
- Use GSTACK_HOME="${gstackHome}" as an environment variable when running bin scripts.
|
|
- The bin scripts are at ./bin/ (relative to this directory), not at ~/.claude/skills/gstack/bin/.
|
|
Replace any references to ~/.claude/skills/gstack/bin/ with ./bin/ when running commands.
|
|
- Replace any references to ~/.claude/skills/gstack/bin/gstack-slug with ./bin/gstack-slug.
|
|
- Do NOT use AskUserQuestion.
|
|
- Do NOT implement code changes.
|
|
- Just show the learnings and summarize what you found.`,
|
|
workingDirectory: workDir,
|
|
maxTurns: 15,
|
|
allowedTools: ['Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob'],
|
|
timeout: 120_000,
|
|
testName: 'learnings-show',
|
|
runId,
|
|
});
|
|
|
|
logCost('/learn show', result);
|
|
|
|
const output = result.output.toLowerCase();
|
|
|
|
// The agent should have found and displayed the seeded learnings
|
|
const mentionsNPlusOne = output.includes('n-plus-one') || output.includes('n+1');
|
|
const mentionsCache = output.includes('stale') || output.includes('cache');
|
|
const mentionsRubocop = output.includes('rubocop');
|
|
|
|
// At least 2 of 3 learnings should appear in the output
|
|
const foundCount = [mentionsNPlusOne, mentionsCache, mentionsRubocop].filter(Boolean).length;
|
|
|
|
const exitOk = ['success', 'error_max_turns'].includes(result.exitReason);
|
|
|
|
recordE2E(evalCollector, '/learn', 'Learnings show E2E', result, {
|
|
passed: exitOk && foundCount >= 2,
|
|
});
|
|
|
|
expect(exitOk).toBe(true);
|
|
expect(foundCount).toBeGreaterThanOrEqual(2);
|
|
|
|
if (foundCount === 3) {
|
|
console.log('All 3 seeded learnings found in output');
|
|
} else {
|
|
console.warn(`Only ${foundCount}/3 learnings found (N+1: ${mentionsNPlusOne}, cache: ${mentionsCache}, rubocop: ${mentionsRubocop})`);
|
|
}
|
|
}, 180_000);
|
|
});
|