Skip to content

Commit 66c8fc7

Browse files
author
StackMemory Bot (CLI)
committed
feat(optimize): add trace optimizer CLI for offline harness analysis
Analyzes stored traces to detect repeated failure patterns (lint, test, build, timeout, rate-limit), verification gaps, retry loops, and context thrash. Generates actionable recommendations with confidence scores and persists reports to .stackmemory/build/.
1 parent a0950b6 commit 66c8fc7

5 files changed

Lines changed: 868 additions & 4 deletions

File tree

scripts/gepa/.before-optimize.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Standalone Node.js `.mjs` automation — outreach, content, analytics, CRM sync.
102102

103103
Scheduled via GitHub Actions cron. All scheduled workflows support `workflow_dispatch` for manual runs.
104104

105-
**GitHub Actions limit:** `workflow_dispatch` allows max 25 `inputs`. `weekly-start.yml` has 22/25 inputs. Feedback is consolidated into a single JSON `feedback` input: `{"social":"...","aeo":"...","blog":"...","snitcher":"..."}`.
105+
**GitHub Actions limit:** `workflow_dispatch` allows max 25 `inputs`. `weekly-start.yml` has 21/25 inputs. Feedback is consolidated into a single JSON `feedback` input: `{"social":"...","aeo":"...","blog":"...","snitcher":"..."}`.
106106

107107
### Slack `/run` command
108108
When adding or renaming GitHub Actions workflows that should be triggerable via Slack, update the `WORKFLOWS` hash in `api/app/jobs/trigger_github_workflow_job.rb`. When deleting a workflow, remove it from the hash. The Slack `/run` command reads this mapping to dispatch workflows.
@@ -111,7 +111,7 @@ When adding or renaming GitHub Actions workflows that should be triggerable via
111111

112112
| Workflow | Script path | Category |
113113
|---|---|---|
114-
| `weekly-start.yml` | `voyager/scripts/content-brief.mjs` + `voyager/scripts/content-audit.mjs` + `ops/fathom-social-content.mjs` + `ops/fathom-testimonial-scan.mjs` + `ops/perplexity-citation-audit.mjs` + `commit/profound-aeo-pulse.mjs` + `commit/citation-rank-tracker.mjs` + `diag/ranking-snapshot.mjs` + `voyager/scripts/generate-blog-scaffold.mjs` + `ops/ahrefs-firehose-digest.mjs` + `ops/export-dripify.mjs` + `commit/prospect-discovery.mjs` + `ops/repush-clay-leads.mjs` + `ops/snitcher-outreach.mjs` | GHA cron (Mon) |
114+
| `weekly-start.yml` | `voyager/scripts/content-brief.mjs` + `voyager/scripts/content-audit.mjs` + `ops/fathom-social-content.mjs` + `ops/fathom-testimonial-scan.mjs` + `ops/perplexity-citation-audit.mjs` + `commit/profound-aeo-pulse.mjs` + `commit/citation-rank-tracker.mjs` + `diag/ranking-snapshot.mjs` + `voyager/scripts/generate-blog-scaffold.mjs` + `ops/ahrefs-firehose-digest.mjs` + `ops/export-dripify.mjs` + `commit/prospect-discovery.mjs` + `commit/growth-signal-leads.mjs` + `ops/repush-clay-leads.mjs` + `ops/snitcher-outreach.mjs` | GHA cron (Mon) |
115115
| `weekly-end.yml` | `diag/fathom-demo-scorecard.mjs` + `commit/feedback/collect-*.mjs` + `commit/feedback/collect-ops-feedback.mjs` + `diag/weekly-retro.mjs` | GHA cron (Fri) |
116116
| `anneal-keywords.yml` | `commit/anneal-keywords.mjs` | GHA cron (Sun) |
117117
| `g2-review-monitor.yml` | `ops/g2-to-senja.mjs` | GHA cron (Daily) |
@@ -133,7 +133,7 @@ When adding or renaming GitHub Actions workflows that should be triggerable via
133133
In `actions/github-script@v7`, `github.rest.issues.createComment` posts plain issue comments on PRs (PRs are issues in GitHub's API). For inline code suggestions on specific files/lines, use `github.rest.pulls.createReview` or `github.rest.pulls.createReviewComment` instead.
134134

135135
### Scheduled (cron)
136-
- `weekly-start.yml` — Mon 9am ET (content review, social content, testimonial scan, Perplexity audit, AEO pulse → blog scaffold, Ahrefs digest, Dripify export, prospect discovery → snitcher outreach)
136+
- `weekly-start.yml` — Mon 9am ET (content review, social content, testimonial scan, Perplexity audit, AEO pulse → blog scaffold, Ahrefs digest, Dripify export, prospect discovery, growth-signal leads → snitcher outreach)
137137
- `weekly-end.yml` — Fri 9am ET (demo scorecard + pipeline health)
138138
- `anneal-keywords.yml` — Sun 11am ET (keyword annealing + kill pattern updates)
139139
- `g2-review-monitor.yml` — Daily 10am ET
@@ -169,7 +169,6 @@ Key patterns:
169169
### Commits
170170
- Plain imperative sentences, no conventional commit prefixes
171171
- Short and direct — describe what, not why
172-
- **NEVER push directly to master.** Always create a feature branch and open a PR.
173172

174173
### Code
175174
- Read before writing. Edit over rewrite. No docs unless asked.

src/cli/commands/optimize.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Command } from 'commander';
2+
import Database from 'better-sqlite3';
3+
import { existsSync } from 'fs';
4+
import { join } from 'path';
5+
import chalk from 'chalk';
6+
import { TraceStore } from '../../core/trace/trace-store.js';
7+
import { TraceOptimizer } from '../../core/optimization/trace-optimizer.js';
8+
9+
export function createOptimizeCommand(): Command {
10+
const optimize = new Command('optimize').description(
11+
'Offline optimizers for harnesses, traces, and prompts'
12+
);
13+
14+
optimize
15+
.command('traces')
16+
.description(
17+
'Analyze stored traces and generate HALO-like offline optimizer recommendations'
18+
)
19+
.option('-d, --days <n>', 'Only analyze traces from the last N days', '30')
20+
.option(
21+
'-m, --min-occurrences <n>',
22+
'Minimum repeated occurrences before surfacing a pattern',
23+
'2'
24+
)
25+
.option('--json', 'Print machine-readable JSON')
26+
.option(
27+
'--no-write',
28+
'Do not persist report files under .stackmemory/build'
29+
)
30+
.action(async (options) => {
31+
const projectRoot = process.cwd();
32+
const dbPath = join(projectRoot, '.stackmemory', 'context.db');
33+
34+
if (!existsSync(dbPath)) {
35+
console.log(
36+
chalk.red('StackMemory not initialized in this directory.')
37+
);
38+
console.log(chalk.gray('Run "stackmemory init" first.'));
39+
return;
40+
}
41+
42+
const db = new Database(dbPath);
43+
try {
44+
const traceStore = new TraceStore(db);
45+
const optimizer = new TraceOptimizer(traceStore);
46+
const report = optimizer.analyze({
47+
lookbackDays: parseInt(options.days, 10) || 30,
48+
minOccurrences: parseInt(options.minOccurrences, 10) || 2,
49+
});
50+
51+
const persisted = options.write
52+
? optimizer.persistReport(projectRoot, report)
53+
: null;
54+
55+
if (options.json) {
56+
console.log(
57+
JSON.stringify(
58+
{
59+
...report,
60+
persisted,
61+
},
62+
null,
63+
2
64+
)
65+
);
66+
return;
67+
}
68+
69+
console.log(chalk.blue('\nTrace Optimizer Report\n'));
70+
console.log(`Lookback window: ${report.lookbackDays} day(s)`);
71+
console.log(`Traces analyzed: ${report.totalTracesAnalyzed}`);
72+
console.log(`Traces with errors: ${report.tracesWithErrors}`);
73+
console.log(`Causal traces: ${report.causalTraces}`);
74+
console.log(
75+
`Average tools/trace: ${report.averageToolsPerTrace.toFixed(2)}`
76+
);
77+
console.log(
78+
`Average trace score: ${report.averageTraceScore.toFixed(2)}`
79+
);
80+
81+
if (report.recommendations.length === 0) {
82+
console.log(
83+
chalk.yellow(
84+
'\nNo repeated patterns crossed the threshold. Lower --min-occurrences or collect more traces.'
85+
)
86+
);
87+
} else {
88+
console.log(chalk.blue('\nRecommendations:\n'));
89+
for (const recommendation of report.recommendations) {
90+
const badge =
91+
recommendation.priority === 'high'
92+
? chalk.red('[high]')
93+
: chalk.yellow('[medium]');
94+
console.log(
95+
`${badge} ${chalk.white(recommendation.title)} (${recommendation.confidence.toFixed(2)} confidence)`
96+
);
97+
console.log(` ${recommendation.summary}`);
98+
console.log(` Targets: ${recommendation.targetAreas.join(', ')}`);
99+
console.log(` Actions: ${recommendation.actions.join(' | ')}`);
100+
console.log(
101+
` Validate: ${recommendation.validations.join(' | ')}`
102+
);
103+
console.log('');
104+
}
105+
}
106+
107+
if (report.clusters.length > 0) {
108+
console.log(chalk.blue('Detected clusters:\n'));
109+
for (const cluster of report.clusters) {
110+
console.log(
111+
`- ${cluster.label} (${cluster.occurrences} traces, ${cluster.kind})`
112+
);
113+
if (cluster.toolPatterns.length > 0) {
114+
console.log(` Tools: ${cluster.toolPatterns.join(', ')}`);
115+
}
116+
if (cluster.sampleSummaries.length > 0) {
117+
console.log(` Examples: ${cluster.sampleSummaries.join(' | ')}`);
118+
}
119+
}
120+
console.log('');
121+
}
122+
123+
if (persisted) {
124+
console.log(chalk.gray(`Saved JSON: ${persisted.jsonPath}`));
125+
console.log(chalk.gray(`Saved Markdown: ${persisted.markdownPath}`));
126+
}
127+
} finally {
128+
db.close();
129+
}
130+
});
131+
132+
return optimize;
133+
}

src/cli/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import { createPingCommand } from './commands/ping.js';
6666
import { createAuditCommand } from './commands/audit.js';
6767
import { createStatsCommand } from './commands/stats.js';
6868
import { createBenchCommand } from './commands/bench.js';
69+
import { createOptimizeCommand } from './commands/optimize.js';
6970
import { createStateCommand } from './commands/state.js';
7071
import { createDigestCommands } from './commands/digest.js';
7172
import { createDesiresCommands } from './commands/desires.js';
@@ -806,6 +807,7 @@ program.addCommand(createModelCommand());
806807
program.addCommand(createAuditCommand());
807808
program.addCommand(createStatsCommand());
808809
program.addCommand(createBenchCommand());
810+
program.addCommand(createOptimizeCommand());
809811
program.addCommand(createStateCommand());
810812
program.addCommand(createDigestCommands());
811813
program.addCommand(createDesiresCommands());
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { beforeEach, describe, expect, it } from 'vitest';
2+
import Database from 'better-sqlite3';
3+
import { v4 as uuidv4 } from 'uuid';
4+
import { TraceStore } from '../../trace/trace-store.js';
5+
import { TraceType, type ToolCall, type Trace } from '../../trace/types.js';
6+
import { TraceOptimizer } from '../trace-optimizer.js';
7+
8+
function makeTool(tool: string, overrides: Partial<ToolCall> = {}): ToolCall {
9+
return {
10+
id: uuidv4(),
11+
tool,
12+
timestamp: Date.now(),
13+
...overrides,
14+
};
15+
}
16+
17+
function makeTrace(overrides: Partial<Trace> = {}): Trace {
18+
const now = Date.now();
19+
return {
20+
id: uuidv4(),
21+
type: TraceType.ERROR_RECOVERY,
22+
tools: [
23+
makeTool('edit'),
24+
makeTool('lint', { error: 'ESLint failed on touched file' }),
25+
],
26+
score: 0.7,
27+
summary: 'Recovered from lint failure',
28+
metadata: {
29+
startTime: now - 5_000,
30+
endTime: now,
31+
filesModified: ['src/example.ts'],
32+
errorsEncountered: ['ESLint failed on touched file'],
33+
decisionsRecorded: [],
34+
causalChain: true,
35+
},
36+
...overrides,
37+
};
38+
}
39+
40+
describe('TraceOptimizer', () => {
41+
let db: Database.Database;
42+
let traceStore: TraceStore;
43+
44+
beforeEach(() => {
45+
db = new Database(':memory:');
46+
traceStore = new TraceStore(db);
47+
});
48+
49+
it('finds repeated lint failures and suggests earlier gating', () => {
50+
traceStore.saveTrace(makeTrace());
51+
traceStore.saveTrace(
52+
makeTrace({
53+
summary: 'Second lint failure',
54+
tools: [
55+
makeTool('write'),
56+
makeTool('lint', { error: 'Prettier lint error' }),
57+
],
58+
metadata: {
59+
startTime: Date.now() - 6_000,
60+
endTime: Date.now(),
61+
filesModified: ['src/another.ts'],
62+
errorsEncountered: ['Prettier lint error'],
63+
decisionsRecorded: [],
64+
causalChain: true,
65+
},
66+
})
67+
);
68+
69+
const report = new TraceOptimizer(traceStore).analyze({
70+
minOccurrences: 2,
71+
});
72+
73+
expect(report.clusters.some((c) => c.id === 'error:lint_failure')).toBe(
74+
true
75+
);
76+
expect(
77+
report.recommendations.some((r) =>
78+
r.title.includes('Lint failures recur')
79+
)
80+
).toBe(true);
81+
});
82+
83+
it('finds verification gaps after mutations', () => {
84+
traceStore.saveTrace(
85+
makeTrace({
86+
type: TraceType.FEATURE_IMPLEMENTATION,
87+
summary: 'Implemented change without validation',
88+
tools: [makeTool('search'), makeTool('edit')],
89+
metadata: {
90+
startTime: Date.now() - 6_000,
91+
endTime: Date.now(),
92+
filesModified: ['src/feature.ts'],
93+
errorsEncountered: [],
94+
decisionsRecorded: [],
95+
causalChain: false,
96+
},
97+
})
98+
);
99+
traceStore.saveTrace(
100+
makeTrace({
101+
type: TraceType.REFACTORING,
102+
summary: 'Refactored helper without running tests',
103+
tools: [makeTool('read'), makeTool('edit')],
104+
metadata: {
105+
startTime: Date.now() - 7_000,
106+
endTime: Date.now(),
107+
filesModified: ['src/helper.ts'],
108+
errorsEncountered: [],
109+
decisionsRecorded: [],
110+
causalChain: false,
111+
},
112+
})
113+
);
114+
115+
const report = new TraceOptimizer(traceStore).analyze({
116+
minOccurrences: 2,
117+
});
118+
119+
const cluster = report.clusters.find((c) => c.id === 'verification_gap');
120+
expect(cluster).toBeDefined();
121+
expect(cluster?.targetAreas).toContain('hooks');
122+
});
123+
124+
it('finds search-heavy context thrash patterns', () => {
125+
traceStore.saveTrace(
126+
makeTrace({
127+
type: TraceType.EXPLORATION,
128+
summary: 'Searched around repeatedly',
129+
tools: [
130+
makeTool('search'),
131+
makeTool('grep'),
132+
makeTool('read'),
133+
makeTool('search'),
134+
makeTool('read'),
135+
],
136+
metadata: {
137+
startTime: Date.now() - 8_000,
138+
endTime: Date.now(),
139+
filesModified: [],
140+
errorsEncountered: [],
141+
decisionsRecorded: [],
142+
causalChain: false,
143+
},
144+
})
145+
);
146+
traceStore.saveTrace(
147+
makeTrace({
148+
type: TraceType.EXPLORATION,
149+
summary: 'More repeated searching',
150+
tools: [
151+
makeTool('grep'),
152+
makeTool('search'),
153+
makeTool('read'),
154+
makeTool('glob'),
155+
makeTool('read'),
156+
],
157+
metadata: {
158+
startTime: Date.now() - 9_000,
159+
endTime: Date.now(),
160+
filesModified: [],
161+
errorsEncountered: [],
162+
decisionsRecorded: [],
163+
causalChain: false,
164+
},
165+
})
166+
);
167+
168+
const report = new TraceOptimizer(traceStore).analyze({
169+
minOccurrences: 2,
170+
});
171+
172+
expect(report.clusters.some((c) => c.id === 'context_thrash')).toBe(true);
173+
});
174+
});

0 commit comments

Comments
 (0)