@shipfast-ai/shipfast 1.1.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -201
- package/agents/architect.md +7 -7
- package/agents/builder.md +9 -10
- package/agents/critic.md +3 -3
- package/agents/scout.md +1 -1
- package/agents/scribe.md +9 -13
- package/bin/install.js +250 -9
- package/brain/index.cjs +38 -80
- package/brain/indexer.cjs +6 -9
- package/brain/schema.sql +4 -2
- package/commands/sf/brain.md +4 -0
- package/commands/sf/check-plan.md +3 -4
- package/commands/sf/config.md +1 -0
- package/commands/sf/cost.md +83 -0
- package/commands/sf/diff.md +53 -0
- package/commands/sf/discuss.md +115 -68
- package/commands/sf/do.md +140 -72
- package/commands/sf/help.md +10 -5
- package/commands/sf/map.md +16 -24
- package/commands/sf/plan.md +6 -9
- package/commands/sf/project.md +4 -4
- package/commands/sf/rollback.md +70 -0
- package/commands/sf/ship.md +13 -0
- package/commands/sf/status.md +1 -3
- package/commands/sf/verify.md +4 -9
- package/commands/sf/worktree.md +286 -0
- package/core/ambiguity.cjs +229 -125
- package/core/architecture.cjs +5 -8
- package/core/autopilot.cjs +1 -0
- package/core/budget.cjs +5 -11
- package/core/constants.cjs +63 -0
- package/core/context-builder.cjs +1 -58
- package/core/executor.cjs +18 -4
- package/core/guardrails.cjs +6 -5
- package/core/model-selector.cjs +5 -48
- package/core/retry.cjs +5 -1
- package/core/session.cjs +2 -2
- package/core/skip-logic.cjs +5 -1
- package/core/verify.cjs +11 -14
- package/hooks/sf-first-run.js +2 -2
- package/mcp/server.cjs +135 -4
- package/package.json +18 -4
- package/scripts/postinstall.js +1 -1
- package/commands/sf/workstream.md +0 -51
package/brain/index.cjs
CHANGED
|
@@ -8,12 +8,9 @@
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { execFileSync } = require('child_process');
|
|
11
|
+
const { DB_NAME } = require('../core/constants.cjs');
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// ============================================================
|
|
15
|
-
// Database initialization
|
|
16
|
-
// ============================================================
|
|
13
|
+
// --- Database initialization ---
|
|
17
14
|
|
|
18
15
|
function getBrainPath(cwd) {
|
|
19
16
|
return path.join(cwd || process.cwd(), DB_NAME);
|
|
@@ -41,9 +38,7 @@ function brainExists(cwd) {
|
|
|
41
38
|
return fs.existsSync(getBrainPath(cwd));
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
//
|
|
45
|
-
// Query helpers (all zero-LLM-cost)
|
|
46
|
-
// ============================================================
|
|
41
|
+
// --- Query helpers ---
|
|
47
42
|
|
|
48
43
|
function query(cwd, sql) {
|
|
49
44
|
const dbPath = getBrainPath(cwd);
|
|
@@ -55,7 +50,10 @@ function query(cwd, sql) {
|
|
|
55
50
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
56
51
|
});
|
|
57
52
|
return result.trim() ? JSON.parse(result) : [];
|
|
58
|
-
} catch {
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (process.env.SF_DEBUG) {
|
|
55
|
+
console.error('[brain] query error:', err.message, '\n SQL:', sql.slice(0, 200));
|
|
56
|
+
}
|
|
59
57
|
return [];
|
|
60
58
|
}
|
|
61
59
|
}
|
|
@@ -65,9 +63,7 @@ function run(cwd, sql) {
|
|
|
65
63
|
execFileSync('sqlite3', [dbPath, sql], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
66
64
|
}
|
|
67
65
|
|
|
68
|
-
//
|
|
69
|
-
// Codebase graph operations
|
|
70
|
-
// ============================================================
|
|
66
|
+
// --- Codebase graph ---
|
|
71
67
|
|
|
72
68
|
function upsertNode(cwd, node) {
|
|
73
69
|
const { id, kind, name, file_path, line_start, line_end, signature, hash, metadata } = node;
|
|
@@ -83,7 +79,7 @@ function addEdge(cwd, source, target, kind, weight = 1.0) {
|
|
|
83
79
|
|
|
84
80
|
function getBlastRadius(cwd, filePaths, maxDepth = 3) {
|
|
85
81
|
const fileList = filePaths.map(f => `'file:${esc(f)}'`).join(',');
|
|
86
|
-
|
|
82
|
+
const sql = `
|
|
87
83
|
WITH RECURSIVE affected(id, depth) AS (
|
|
88
84
|
SELECT id, 0 FROM nodes WHERE id IN (${fileList})
|
|
89
85
|
UNION
|
|
@@ -96,7 +92,35 @@ function getBlastRadius(cwd, filePaths, maxDepth = 3) {
|
|
|
96
92
|
WHERE n.signature IS NOT NULL AND n.signature != ''
|
|
97
93
|
ORDER BY a.depth ASC
|
|
98
94
|
LIMIT 30
|
|
99
|
-
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
const local = query(cwd, sql);
|
|
98
|
+
|
|
99
|
+
// Cross-repo blast radius: query linked repos too
|
|
100
|
+
const linkedConfig = getConfig(cwd, 'linked_repos');
|
|
101
|
+
if (linkedConfig) {
|
|
102
|
+
try {
|
|
103
|
+
const linkedPaths = JSON.parse(linkedConfig);
|
|
104
|
+
for (const repoPath of linkedPaths) {
|
|
105
|
+
const linkedDb = path.join(repoPath, '.shipfast', 'brain.db');
|
|
106
|
+
if (fs.existsSync(linkedDb)) {
|
|
107
|
+
try {
|
|
108
|
+
const result = execFileSync('sqlite3', ['-json', linkedDb, sql], {
|
|
109
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']
|
|
110
|
+
}).trim();
|
|
111
|
+
if (result) {
|
|
112
|
+
const parsed = JSON.parse(result);
|
|
113
|
+
const repoName = path.basename(repoPath);
|
|
114
|
+
parsed.forEach(row => { row._repo = repoName; });
|
|
115
|
+
local.push(...parsed);
|
|
116
|
+
}
|
|
117
|
+
} catch { /* linked repo query failed */ }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch { /* linked_repos not valid JSON */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return local.slice(0, 30);
|
|
100
124
|
}
|
|
101
125
|
|
|
102
126
|
function getSignaturesForFile(cwd, filePath) {
|
|
@@ -112,9 +136,7 @@ function getStaleNodes(cwd) {
|
|
|
112
136
|
return query(cwd, `SELECT id, file_path, hash FROM nodes WHERE kind = 'file'`);
|
|
113
137
|
}
|
|
114
138
|
|
|
115
|
-
// ============================================================
|
|
116
139
|
// Context operations (replaces STATE.md / REQUIREMENTS.md)
|
|
117
|
-
// ============================================================
|
|
118
140
|
|
|
119
141
|
function setContext(cwd, scope, key, value) {
|
|
120
142
|
const val = typeof value === 'string' ? value : JSON.stringify(value);
|
|
@@ -134,9 +156,7 @@ function getAllContext(cwd, scope) {
|
|
|
134
156
|
return query(cwd, `SELECT key, value FROM context WHERE scope = '${esc(scope)}'`);
|
|
135
157
|
}
|
|
136
158
|
|
|
137
|
-
// ============================================================
|
|
138
159
|
// Decision operations
|
|
139
|
-
// ============================================================
|
|
140
160
|
|
|
141
161
|
function addDecision(cwd, { question, decision, reasoning, phase, tags }) {
|
|
142
162
|
run(cwd, `INSERT INTO decisions (question, decision, reasoning, phase, tags)
|
|
@@ -148,9 +168,7 @@ function getDecisions(cwd, phase) {
|
|
|
148
168
|
return query(cwd, `SELECT question, decision, reasoning, phase FROM decisions ${where} ORDER BY created_at DESC LIMIT 20`);
|
|
149
169
|
}
|
|
150
170
|
|
|
151
|
-
// ============================================================
|
|
152
171
|
// Task operations
|
|
153
|
-
// ============================================================
|
|
154
172
|
|
|
155
173
|
function addTask(cwd, task) {
|
|
156
174
|
const { id, phase, description, plan_text } = task;
|
|
@@ -170,9 +188,7 @@ function getTasks(cwd, phase) {
|
|
|
170
188
|
return query(cwd, `SELECT * FROM tasks ${where} ORDER BY created_at`);
|
|
171
189
|
}
|
|
172
190
|
|
|
173
|
-
// ============================================================
|
|
174
191
|
// Learnings (self-improving memory)
|
|
175
|
-
// ============================================================
|
|
176
192
|
|
|
177
193
|
function addLearning(cwd, { pattern, problem, solution, domain, source }) {
|
|
178
194
|
run(cwd, `INSERT INTO learnings (pattern, problem, solution, domain, source)
|
|
@@ -193,9 +209,7 @@ function pruneOldLearnings(cwd, daysOld = 30) {
|
|
|
193
209
|
run(cwd, `DELETE FROM learnings WHERE confidence < 0.3 AND times_used = 0 AND created_at < strftime('%s', 'now') - ${daysOld * 86400}`);
|
|
194
210
|
}
|
|
195
211
|
|
|
196
|
-
// ============================================================
|
|
197
212
|
// Checkpoints
|
|
198
|
-
// ============================================================
|
|
199
213
|
|
|
200
214
|
function createCheckpoint(cwd, id, description) {
|
|
201
215
|
let gitRef = '';
|
|
@@ -218,9 +232,7 @@ function rollbackCheckpoint(cwd, id) {
|
|
|
218
232
|
run(cwd, `DELETE FROM checkpoints WHERE id = '${esc(id)}'`);
|
|
219
233
|
}
|
|
220
234
|
|
|
221
|
-
// ============================================================
|
|
222
235
|
// Token budget
|
|
223
|
-
// ============================================================
|
|
224
236
|
|
|
225
237
|
function getTokenBudget(cwd) {
|
|
226
238
|
const rows = query(cwd, `SELECT value FROM config WHERE key = 'token_budget'`);
|
|
@@ -241,9 +253,7 @@ function getTokensByAgent(cwd, sessionId) {
|
|
|
241
253
|
return query(cwd, `SELECT agent, SUM(input_tokens + output_tokens) as total FROM token_usage WHERE session_id = '${esc(sessionId)}' GROUP BY agent`);
|
|
242
254
|
}
|
|
243
255
|
|
|
244
|
-
// ============================================================
|
|
245
256
|
// Hot files (git-derived)
|
|
246
|
-
// ============================================================
|
|
247
257
|
|
|
248
258
|
function updateHotFiles(cwd, limit = 100) {
|
|
249
259
|
try {
|
|
@@ -264,9 +274,7 @@ function getHotFiles(cwd, limit = 20) {
|
|
|
264
274
|
return query(cwd, `SELECT file_path, change_count FROM hot_files ORDER BY change_count DESC LIMIT ${limit}`);
|
|
265
275
|
}
|
|
266
276
|
|
|
267
|
-
// ============================================================
|
|
268
277
|
// Config
|
|
269
|
-
// ============================================================
|
|
270
278
|
|
|
271
279
|
function getConfig(cwd, key) {
|
|
272
280
|
const rows = query(cwd, `SELECT value FROM config WHERE key = '${esc(key)}'`);
|
|
@@ -277,9 +285,7 @@ function setConfig(cwd, key, value) {
|
|
|
277
285
|
run(cwd, `INSERT OR REPLACE INTO config (key, value) VALUES ('${esc(key)}', '${esc(String(value))}')`);
|
|
278
286
|
}
|
|
279
287
|
|
|
280
|
-
// ============================================================
|
|
281
288
|
// Context builder for agents (the key token-saving function)
|
|
282
|
-
// ============================================================
|
|
283
289
|
|
|
284
290
|
function buildAgentContext(cwd, { agent, taskDescription, affectedFiles, phase, domain }) {
|
|
285
291
|
const parts = [];
|
|
@@ -322,18 +328,14 @@ function buildAgentContext(cwd, { agent, taskDescription, affectedFiles, phase,
|
|
|
322
328
|
return parts.join('\n\n');
|
|
323
329
|
}
|
|
324
330
|
|
|
325
|
-
// ============================================================
|
|
326
331
|
// Model Performance (feedback loop)
|
|
327
|
-
// ============================================================
|
|
328
332
|
|
|
329
333
|
function recordModelOutcome(cwd, { agent, model, domain, taskId, outcome }) {
|
|
330
334
|
run(cwd, `INSERT INTO model_performance (agent, model, domain, task_id, outcome)
|
|
331
335
|
VALUES ('${esc(agent)}', '${esc(model)}', '${esc(domain || '')}', '${esc(taskId || '')}', '${esc(outcome)}')`);
|
|
332
336
|
}
|
|
333
337
|
|
|
334
|
-
// ============================================================
|
|
335
338
|
// Seeds (forward ideas captured during work)
|
|
336
|
-
// ============================================================
|
|
337
339
|
|
|
338
340
|
function addSeed(cwd, { idea, sourceTask, domain, priority }) {
|
|
339
341
|
run(cwd, `INSERT INTO seeds (idea, source_task, domain, priority)
|
|
@@ -357,9 +359,7 @@ function dismissSeed(cwd, seedId) {
|
|
|
357
359
|
run(cwd, `UPDATE seeds SET status = 'dismissed' WHERE id = ${parseInt(seedId)}`);
|
|
358
360
|
}
|
|
359
361
|
|
|
360
|
-
// ============================================================
|
|
361
362
|
// Utils
|
|
362
|
-
// ============================================================
|
|
363
363
|
|
|
364
364
|
function esc(s) {
|
|
365
365
|
if (s == null) return '';
|
|
@@ -408,46 +408,4 @@ module.exports = {
|
|
|
408
408
|
getSeeds,
|
|
409
409
|
promoteSeed,
|
|
410
410
|
dismissSeed,
|
|
411
|
-
// Requirements
|
|
412
|
-
addRequirement,
|
|
413
|
-
getRequirements,
|
|
414
|
-
updateRequirement,
|
|
415
|
-
getRequirementCoverage
|
|
416
411
|
};
|
|
417
|
-
|
|
418
|
-
// ============================================================
|
|
419
|
-
// Requirements (REQ-ID tracing)
|
|
420
|
-
// ============================================================
|
|
421
|
-
|
|
422
|
-
function addRequirement(cwd, { id, category, description, priority, phase }) {
|
|
423
|
-
run(cwd, `INSERT OR REPLACE INTO requirements (id, category, description, priority, phase)
|
|
424
|
-
VALUES ('${esc(id)}', '${esc(category)}', '${esc(description)}', '${esc(priority || 'v1')}', '${esc(phase || '')}')`);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function getRequirements(cwd, opts = {}) {
|
|
428
|
-
const conditions = [];
|
|
429
|
-
if (opts.phase) conditions.push(`phase = '${esc(opts.phase)}'`);
|
|
430
|
-
if (opts.category) conditions.push(`category = '${esc(opts.category)}'`);
|
|
431
|
-
if (opts.status) conditions.push(`status = '${esc(opts.status)}'`);
|
|
432
|
-
if (opts.priority) conditions.push(`priority = '${esc(opts.priority)}'`);
|
|
433
|
-
const where = conditions.length ? 'WHERE ' + conditions.join(' AND ') : '';
|
|
434
|
-
return query(cwd, `SELECT * FROM requirements ${where} ORDER BY category, id`);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function updateRequirement(cwd, id, updates) {
|
|
438
|
-
const sets = Object.entries(updates).map(([k, v]) => `${k} = '${esc(String(v))}'`).join(', ');
|
|
439
|
-
run(cwd, `UPDATE requirements SET ${sets} WHERE id = '${esc(id)}'`);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function getRequirementCoverage(cwd) {
|
|
443
|
-
const total = query(cwd, "SELECT COUNT(*) as c FROM requirements WHERE priority = 'v1'");
|
|
444
|
-
const mapped = query(cwd, "SELECT COUNT(*) as c FROM requirements WHERE priority = 'v1' AND phase IS NOT NULL AND phase != ''");
|
|
445
|
-
const done = query(cwd, "SELECT COUNT(*) as c FROM requirements WHERE priority = 'v1' AND status = 'done'");
|
|
446
|
-
const verified = query(cwd, "SELECT COUNT(*) as c FROM requirements WHERE priority = 'v1' AND verified = 1");
|
|
447
|
-
return {
|
|
448
|
-
total: total[0] ? total[0].c : 0,
|
|
449
|
-
mapped: mapped[0] ? mapped[0].c : 0,
|
|
450
|
-
done: done[0] ? done[0].c : 0,
|
|
451
|
-
verified: verified[0] ? verified[0].c : 0
|
|
452
|
-
};
|
|
453
|
-
}
|
package/brain/indexer.cjs
CHANGED
|
@@ -152,13 +152,10 @@ function hashContent(content) {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
// ============================================================
|
|
155
|
-
// SQL escaping (
|
|
155
|
+
// SQL escaping — use brain.esc() as single source of truth
|
|
156
156
|
// ============================================================
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
if (s == null) return '';
|
|
160
|
-
return String(s).replace(/'/g, "''");
|
|
161
|
-
}
|
|
158
|
+
const esc = brain.esc;
|
|
162
159
|
|
|
163
160
|
// ============================================================
|
|
164
161
|
// Batch SQL collector
|
|
@@ -461,7 +458,7 @@ function indexCodebase(cwd, opts = {}) {
|
|
|
461
458
|
execFileSync('sqlite3', [dbPath], { input: sql, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
462
459
|
}
|
|
463
460
|
|
|
464
|
-
//
|
|
461
|
+
// Clean orphan nodes: remove entries for files that no longer exist on disk
|
|
465
462
|
let cleaned = 0;
|
|
466
463
|
if (!changedOnly) {
|
|
467
464
|
const discoveredPaths = new Set(files.map(f => path.relative(cwd, f).replace(/\\/g, '/')));
|
|
@@ -483,10 +480,10 @@ function indexCodebase(cwd, opts = {}) {
|
|
|
483
480
|
}
|
|
484
481
|
}
|
|
485
482
|
|
|
486
|
-
//
|
|
483
|
+
// Update hot files from git history on every index
|
|
487
484
|
brain.updateHotFiles(cwd);
|
|
488
485
|
|
|
489
|
-
//
|
|
486
|
+
// Run co-change analysis from git history to detect files that change together
|
|
490
487
|
try {
|
|
491
488
|
const gitIntelPath = path.join(__dirname, '..', 'core', 'git-intel.cjs');
|
|
492
489
|
if (fs.existsSync(gitIntelPath)) {
|
|
@@ -517,7 +514,7 @@ if (require.main === module) {
|
|
|
517
514
|
const fresh = args.includes('--fresh');
|
|
518
515
|
const cwd = args.find(a => !a.startsWith('-')) || process.cwd();
|
|
519
516
|
|
|
520
|
-
//
|
|
517
|
+
// --fresh flag: delete existing brain.db for full reindex
|
|
521
518
|
if (fresh) {
|
|
522
519
|
const dbPath = path.join(cwd, '.shipfast', 'brain.db');
|
|
523
520
|
if (fs.existsSync(dbPath)) {
|
package/brain/schema.sql
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
CREATE TABLE IF NOT EXISTS nodes (
|
|
9
9
|
id TEXT PRIMARY KEY, -- 'file:src/auth.ts', 'fn:validateToken', 'type:User'
|
|
10
|
-
kind TEXT NOT NULL
|
|
10
|
+
kind TEXT NOT NULL CHECK (kind IN ('file','function','type','component','route','class','variable','export')),
|
|
11
11
|
name TEXT NOT NULL, -- human-readable name
|
|
12
12
|
file_path TEXT, -- relative file path
|
|
13
13
|
line_start INTEGER,
|
|
@@ -25,7 +25,7 @@ CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
|
|
|
25
25
|
CREATE TABLE IF NOT EXISTS edges (
|
|
26
26
|
source TEXT NOT NULL,
|
|
27
27
|
target TEXT NOT NULL,
|
|
28
|
-
kind TEXT NOT NULL
|
|
28
|
+
kind TEXT NOT NULL CHECK (kind IN ('imports','calls','implements','depends','mutates','exports','extends','co_changes')),
|
|
29
29
|
weight REAL DEFAULT 1.0,
|
|
30
30
|
PRIMARY KEY (source, target, kind),
|
|
31
31
|
FOREIGN KEY (source) REFERENCES nodes(id) ON DELETE CASCADE,
|
|
@@ -52,6 +52,7 @@ CREATE TABLE IF NOT EXISTS context (
|
|
|
52
52
|
);
|
|
53
53
|
|
|
54
54
|
CREATE INDEX IF NOT EXISTS idx_context_scope ON context(scope);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_context_scope_key ON context(scope, key);
|
|
55
56
|
|
|
56
57
|
-- ============================================================
|
|
57
58
|
-- DECISIONS (replaces decision sections in STATE.md)
|
|
@@ -270,3 +271,4 @@ CREATE TABLE IF NOT EXISTS _migrations (
|
|
|
270
271
|
INSERT OR IGNORE INTO _migrations (version, name) VALUES (1, 'initial_schema');
|
|
271
272
|
INSERT OR IGNORE INTO _migrations (version, name) VALUES (2, 'add_seeds_table');
|
|
272
273
|
INSERT OR IGNORE INTO _migrations (version, name) VALUES (3, 'add_model_performance_table');
|
|
274
|
+
INSERT OR IGNORE INTO _migrations (version, name) VALUES (4, 'add_context_index_and_cleanup');
|
package/commands/sf/brain.md
CHANGED
|
@@ -10,6 +10,10 @@ allowed-tools:
|
|
|
10
10
|
<objective>
|
|
11
11
|
Direct interface to brain.db for querying the codebase knowledge graph,
|
|
12
12
|
decisions, learnings, and task history.
|
|
13
|
+
|
|
14
|
+
This is the CANONICAL reference for brain.db queries. Other commands that need
|
|
15
|
+
brain data should use MCP tools (brain_decisions, brain_learnings, brain_search, etc.)
|
|
16
|
+
or reference the SQL patterns below — do not invent new queries.
|
|
13
17
|
</objective>
|
|
14
18
|
|
|
15
19
|
<process>
|
|
@@ -17,10 +17,9 @@ Catches scope creep, missing consumers, broken dependencies, and uncovered must-
|
|
|
17
17
|
|
|
18
18
|
## Step 1: Load tasks and must-haves
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
```
|
|
20
|
+
Use the `brain_tasks` MCP tool with: `{ "action": "list", "status": "pending" }` — returns pending tasks ordered by created_at.
|
|
21
|
+
|
|
22
|
+
Use the `brain_context` MCP tool with: `{ "action": "get", "key_like": "must_haves:%", "limit": 1 }` — returns the most recent must-haves entry.
|
|
24
23
|
|
|
25
24
|
## Step 2: Check each task
|
|
26
25
|
|
package/commands/sf/config.md
CHANGED
|
@@ -42,6 +42,7 @@ Parse `key value` from arguments. Valid keys:
|
|
|
42
42
|
- `model-scribe` -> `model_tier_scribe`
|
|
43
43
|
- `auto-checkpoint` -> `auto_checkpoint` (true/false)
|
|
44
44
|
- `auto-learn` -> `auto_learn` (true/false)
|
|
45
|
+
- `post-ship-hook` -> `post_ship_hook` (shell command to run after /sf-ship)
|
|
45
46
|
|
|
46
47
|
Update brain.db config table. Confirm the change:
|
|
47
48
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf:cost
|
|
3
|
+
description: "Show token usage breakdown by agent, domain, and model."
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Bash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<objective>
|
|
9
|
+
Analyze token spending to identify expensive patterns and optimize model selection.
|
|
10
|
+
</objective>
|
|
11
|
+
|
|
12
|
+
<process>
|
|
13
|
+
|
|
14
|
+
## Query token_usage and model_performance tables
|
|
15
|
+
|
|
16
|
+
### By agent
|
|
17
|
+
```sql
|
|
18
|
+
SELECT agent, SUM(input_tokens + output_tokens) as total_tokens, COUNT(*) as calls
|
|
19
|
+
FROM token_usage GROUP BY agent ORDER BY total_tokens DESC;
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### By model
|
|
23
|
+
```sql
|
|
24
|
+
SELECT model, SUM(input_tokens + output_tokens) as total_tokens, COUNT(*) as calls
|
|
25
|
+
FROM token_usage WHERE model != '' GROUP BY model ORDER BY total_tokens DESC;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### By domain (from model_performance)
|
|
29
|
+
```sql
|
|
30
|
+
SELECT domain, COUNT(*) as tasks,
|
|
31
|
+
SUM(CASE WHEN outcome='success' THEN 1 ELSE 0 END) as successes,
|
|
32
|
+
SUM(CASE WHEN outcome='failure' THEN 1 ELSE 0 END) as failures
|
|
33
|
+
FROM model_performance WHERE domain != '' GROUP BY domain ORDER BY tasks DESC;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Model success rates
|
|
37
|
+
```sql
|
|
38
|
+
SELECT agent, model, outcome, COUNT(*) as count
|
|
39
|
+
FROM model_performance GROUP BY agent, model, outcome ORDER BY agent, model;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Budget status
|
|
43
|
+
```sql
|
|
44
|
+
SELECT value FROM config WHERE key = 'token_budget';
|
|
45
|
+
```
|
|
46
|
+
```sql
|
|
47
|
+
SELECT COALESCE(SUM(input_tokens + output_tokens), 0) as used FROM token_usage;
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Format report
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Token Cost Analysis
|
|
54
|
+
===================
|
|
55
|
+
|
|
56
|
+
Budget: [used]/[total] ([pct]%)
|
|
57
|
+
|
|
58
|
+
By Agent:
|
|
59
|
+
builder [tokens] tokens ([calls] calls)
|
|
60
|
+
scout [tokens] tokens ([calls] calls)
|
|
61
|
+
architect [tokens] tokens ([calls] calls)
|
|
62
|
+
critic [tokens] tokens ([calls] calls)
|
|
63
|
+
scribe [tokens] tokens ([calls] calls)
|
|
64
|
+
|
|
65
|
+
By Model:
|
|
66
|
+
sonnet [tokens] tokens ([calls] calls)
|
|
67
|
+
haiku [tokens] tokens ([calls] calls)
|
|
68
|
+
opus [tokens] tokens ([calls] calls)
|
|
69
|
+
|
|
70
|
+
By Domain:
|
|
71
|
+
auth [tasks] tasks [successes]✓ [failures]✗
|
|
72
|
+
database [tasks] tasks [successes]✓ [failures]✗
|
|
73
|
+
|
|
74
|
+
Model Success Rates:
|
|
75
|
+
builder/haiku: [success]/[total] ([pct]%)
|
|
76
|
+
builder/sonnet: [success]/[total] ([pct]%)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
</process>
|
|
80
|
+
|
|
81
|
+
<context>
|
|
82
|
+
$ARGUMENTS
|
|
83
|
+
</context>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf:diff
|
|
3
|
+
description: "Smart diff viewer — changes grouped by task with file stats."
|
|
4
|
+
allowed-tools:
|
|
5
|
+
- Bash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
<objective>
|
|
9
|
+
Show recent changes organized by task, not by commit. Maps each file change
|
|
10
|
+
to the task that caused it, making it easy to review what was done.
|
|
11
|
+
</objective>
|
|
12
|
+
|
|
13
|
+
<process>
|
|
14
|
+
|
|
15
|
+
## Step 1: Get recent passed tasks
|
|
16
|
+
|
|
17
|
+
Use the `brain_tasks` MCP tool with: `{ "action": "list", "status": "passed", "has_commit_sha": true, "limit": 10 }` — returns passed tasks with commits ordered by finished_at descending.
|
|
18
|
+
|
|
19
|
+
## Step 2: For each task, get the diff stats
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git show --stat --format='' [commit_sha]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Step 3: Format as grouped report
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Recent Changes by Task
|
|
29
|
+
======================
|
|
30
|
+
|
|
31
|
+
Task: [description]
|
|
32
|
+
Commit: [sha] ([date])
|
|
33
|
+
Files:
|
|
34
|
+
M src/auth/login.ts (+15 -3)
|
|
35
|
+
A src/auth/validate.ts (+42)
|
|
36
|
+
M src/types/user.ts (+5 -1)
|
|
37
|
+
|
|
38
|
+
Task: [description]
|
|
39
|
+
Commit: [sha] ([date])
|
|
40
|
+
Files:
|
|
41
|
+
M src/api/billing.ts (+28 -12)
|
|
42
|
+
A src/hooks/usePayment.ts (+35)
|
|
43
|
+
|
|
44
|
+
Summary: [N] tasks | [M] files changed | +[additions] -[deletions]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If $ARGUMENTS contains a task ID or description, filter to show only that task's diff in full detail (`git show [sha]`).
|
|
48
|
+
|
|
49
|
+
</process>
|
|
50
|
+
|
|
51
|
+
<context>
|
|
52
|
+
$ARGUMENTS
|
|
53
|
+
</context>
|