zelari-code 0.1.3 → 0.2.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/dist/cli/app.js +38 -2
- package/dist/cli/app.js.map +1 -1
- package/dist/cli/main.bundled.js +2274 -66
- package/dist/cli/main.bundled.js.map +4 -4
- package/dist/cli/main.js +1 -1
- package/dist/cli/provider/openai-compatible.js +51 -3
- package/dist/cli/provider/openai-compatible.js.map +1 -1
- package/dist/cli/toolRegistry.js +4 -0
- package/dist/cli/toolRegistry.js.map +1 -1
- package/dist/main/core/AgentHarness.js +80 -1
- package/dist/main/core/AgentHarness.js.map +1 -1
- package/dist/main/core/tools/builtin/listFiles.js +86 -0
- package/dist/main/core/tools/builtin/listFiles.js.map +1 -0
- package/package.json +1 -1
package/dist/cli/main.bundled.js
CHANGED
|
@@ -523,11 +523,11 @@ __export(updater_exports, {
|
|
|
523
523
|
});
|
|
524
524
|
import { createRequire } from "node:module";
|
|
525
525
|
import { spawn as spawn2 } from "node:child_process";
|
|
526
|
-
import
|
|
526
|
+
import path16 from "node:path";
|
|
527
527
|
import { fileURLToPath } from "node:url";
|
|
528
528
|
function getCurrentVersion() {
|
|
529
529
|
try {
|
|
530
|
-
const pkgPath =
|
|
530
|
+
const pkgPath = path16.resolve(__dirname2, "..", "..", "package.json");
|
|
531
531
|
const pkg = require2(pkgPath);
|
|
532
532
|
return pkg.version;
|
|
533
533
|
} catch {
|
|
@@ -626,7 +626,7 @@ var init_updater = __esm({
|
|
|
626
626
|
"src/cli/updater.ts"() {
|
|
627
627
|
"use strict";
|
|
628
628
|
require2 = createRequire(import.meta.url);
|
|
629
|
-
__dirname2 =
|
|
629
|
+
__dirname2 = path16.dirname(fileURLToPath(import.meta.url));
|
|
630
630
|
REGISTRY_URL = "https://registry.npmjs.org/zelari-code/latest";
|
|
631
631
|
}
|
|
632
632
|
});
|
|
@@ -1321,8 +1321,29 @@ var CODING_SKILL_CATALOG = [];
|
|
|
1321
1321
|
function listCodingSkills() {
|
|
1322
1322
|
return [...CODING_SKILL_CATALOG];
|
|
1323
1323
|
}
|
|
1324
|
+
function validateCodingSkillRequires(skill) {
|
|
1325
|
+
const errors = [];
|
|
1326
|
+
for (const reqId of skill.requires) {
|
|
1327
|
+
if (!CODING_SKILL_CATALOG.some((s) => s.id === reqId)) {
|
|
1328
|
+
errors.push(`Skill "${skill.id}" requires unknown skill "${reqId}"`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return errors;
|
|
1332
|
+
}
|
|
1333
|
+
function registerCodingSkill(skill) {
|
|
1334
|
+
const errors = validateCodingSkillRequires(skill);
|
|
1335
|
+
if (errors.length > 0) {
|
|
1336
|
+
throw new Error(`Cannot register coding skill: ${errors.join("; ")}`);
|
|
1337
|
+
}
|
|
1338
|
+
const idx = CODING_SKILL_CATALOG.findIndex((s) => s.id === skill.id);
|
|
1339
|
+
if (idx >= 0) {
|
|
1340
|
+
CODING_SKILL_CATALOG[idx] = skill;
|
|
1341
|
+
} else {
|
|
1342
|
+
CODING_SKILL_CATALOG.push(skill);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1324
1345
|
|
|
1325
|
-
// src/agents/
|
|
1346
|
+
// src/agents/skills/builtin/debugging.ts
|
|
1326
1347
|
var CLARIFICATION_PROTOCOL = `
|
|
1327
1348
|
|
|
1328
1349
|
WHEN TO ASK THE USER (clarification):
|
|
@@ -1332,6 +1353,1991 @@ If a single missing fact would materially change your output (target platform, s
|
|
|
1332
1353
|
{ "question": "One focused question", "choices": ["Option A", "Option B", "Option C"], "context": "Why this matters" }
|
|
1333
1354
|
---END---
|
|
1334
1355
|
|
|
1356
|
+
Rules for clarifications:
|
|
1357
|
+
- Ask AT MOST ONE question per turn, and only when genuinely blocked.
|
|
1358
|
+
- Prefer a small set of concrete "choices" (2-4). The user can still type a wave-in response.
|
|
1359
|
+
- Do NOT ask for information that could be reasonably assumed or already in shared context.
|
|
1360
|
+
- If you can proceed with a sound documented assumption, DO SO instead of asking.`;
|
|
1361
|
+
var reproduceBug = {
|
|
1362
|
+
id: "reproduce-bug",
|
|
1363
|
+
version: "1.0.0",
|
|
1364
|
+
name: "Reproduce Bug",
|
|
1365
|
+
description: "Convert a bug report into a MINIMAL failing test. The test must fail without any code changes, then pass after the fix.",
|
|
1366
|
+
category: "debug",
|
|
1367
|
+
requiredRoles: ["oracle", "atlas"],
|
|
1368
|
+
requiredTools: ["read_file", "write_file"],
|
|
1369
|
+
estimatedCost: "medium",
|
|
1370
|
+
enabledByDefault: true,
|
|
1371
|
+
builtin: true,
|
|
1372
|
+
triggers: [
|
|
1373
|
+
"Bug report without a minimal reproduction",
|
|
1374
|
+
"Bug that only happens in production (not in tests)",
|
|
1375
|
+
'Bug that happens "sometimes" (intermittent)',
|
|
1376
|
+
"Bug reported via screenshot or video, not repro steps",
|
|
1377
|
+
"A complex multi-agent state race condition that is hard to verify without a test"
|
|
1378
|
+
],
|
|
1379
|
+
antiPatterns: [
|
|
1380
|
+
"Bug is in production AND has minimal repro steps already \u2014 skip to fix",
|
|
1381
|
+
"Bug is in test code itself \u2014 fix the test, not the production code",
|
|
1382
|
+
"Bug requires special hardware/network state \u2014 use a mock instead"
|
|
1383
|
+
],
|
|
1384
|
+
requires: [],
|
|
1385
|
+
relatedSkills: ["debug-with-rag", "root-cause-five-whys"],
|
|
1386
|
+
tags: ["debug", "reproduction", "test", "minimal-example"],
|
|
1387
|
+
examples: [
|
|
1388
|
+
{
|
|
1389
|
+
input: 'Bug report: "Council sometimes shows duplicate messages at the start of a session"',
|
|
1390
|
+
output: {
|
|
1391
|
+
minimalRepro: `// tests/regression/duplicate-messages.test.ts
|
|
1392
|
+
import { runCouncilPure } from '../agents/councilApi';
|
|
1393
|
+
|
|
1394
|
+
describe('council duplicate message bug', () => {
|
|
1395
|
+
it('does not emit duplicate user message when session resumes', async () => {
|
|
1396
|
+
const mockProvider = async function* () {
|
|
1397
|
+
yield { kind: 'text', delta: 'first' };
|
|
1398
|
+
yield { kind: 'finish', reason: 'stop' };
|
|
1399
|
+
};
|
|
1400
|
+
const harness = new AgentHarness({
|
|
1401
|
+
model: 'test', provider: 'test',
|
|
1402
|
+
messages: [
|
|
1403
|
+
{ role: 'user', content: 'hi' },
|
|
1404
|
+
{ role: 'user', content: 'hi' }, // duplicate from session resume
|
|
1405
|
+
],
|
|
1406
|
+
tools: [],
|
|
1407
|
+
providerStream: mockProvider,
|
|
1408
|
+
});
|
|
1409
|
+
const events = [];
|
|
1410
|
+
for await (const e of harness.run()) events.push(e);
|
|
1411
|
+
const userMessages = events.filter(e => e.type === 'message_delta').length;
|
|
1412
|
+
expect(userMessages).toBeGreaterThan(0); // sanity
|
|
1413
|
+
// The actual bug: duplicate is rendered twice
|
|
1414
|
+
// After fix, expect exactly 1 user message rendered
|
|
1415
|
+
});
|
|
1416
|
+
});`,
|
|
1417
|
+
reproSteps: [
|
|
1418
|
+
"1. Set up session with duplicate user message in transcript",
|
|
1419
|
+
"2. Run runCouncilPure",
|
|
1420
|
+
"3. Observe duplicate message in output"
|
|
1421
|
+
],
|
|
1422
|
+
whyMinimal: "Removed all agents except the first specialist (was: sisyphus+prometheus+hephaestus+oracle+chairman). Removed RAG context, workspace context. Kept only the duplication pattern."
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
],
|
|
1426
|
+
outputSchema: "{ minimalRepro: string; reproSteps: string[]; whyMinimal: string }",
|
|
1427
|
+
systemPromptFragment: `You are converting a bug report into a minimal failing test.
|
|
1428
|
+
|
|
1429
|
+
## Methodology
|
|
1430
|
+
1. **Read the bug report** carefully: what's the exact symptom? When does it happen?
|
|
1431
|
+
2. **Strip to essentials**: remove every agent, tool, env var, user state that isn't strictly required.
|
|
1432
|
+
3. **Identify the trigger**: what ONE input causes the bug?
|
|
1433
|
+
4. **Write the failing test**: it must FAIL on the current (buggy) code.
|
|
1434
|
+
5. **Verify it fails**: the test must fail without any code changes.
|
|
1435
|
+
6. **After the fix lands**, the test must PASS.
|
|
1436
|
+
|
|
1437
|
+
## Output format (JSON-typed)
|
|
1438
|
+
- minimalRepro: string (the test code, runnable as-is)
|
|
1439
|
+
- reproSteps: string[] (3-7 numbered steps to reproduce manually)
|
|
1440
|
+
- whyMinimal: string (what you stripped + why)
|
|
1441
|
+
|
|
1442
|
+
## Minimal-repro principles
|
|
1443
|
+
- **One assertion per test** \u2014 easier to debug when the test fails
|
|
1444
|
+
- **Deterministic** \u2014 no flaky timing, no random data
|
|
1445
|
+
- **No external dependencies** \u2014 use mocks for network/DB
|
|
1446
|
+
- **Fast** \u2014 under 100ms if possible
|
|
1447
|
+
- **Independent** \u2014 doesn't depend on other tests' state
|
|
1448
|
+
|
|
1449
|
+
Stay under 300 words.${CLARIFICATION_PROTOCOL}`
|
|
1450
|
+
};
|
|
1451
|
+
var debugWithRag = {
|
|
1452
|
+
id: "debug-with-rag",
|
|
1453
|
+
version: "1.0.0",
|
|
1454
|
+
name: "Debug With RAG",
|
|
1455
|
+
description: "Search the knowledge base for similar past bugs, read the stack trace, propose the most likely root cause and a concrete fix.",
|
|
1456
|
+
category: "debug",
|
|
1457
|
+
requiredRoles: ["oracle", "atlas"],
|
|
1458
|
+
requiredTools: ["searchRAG", "grep_content", "read_file"],
|
|
1459
|
+
estimatedCost: "medium",
|
|
1460
|
+
enabledByDefault: true,
|
|
1461
|
+
builtin: true,
|
|
1462
|
+
triggers: [
|
|
1463
|
+
"A bug report with a stack trace or error message",
|
|
1464
|
+
"A regression: feature worked before, now broken",
|
|
1465
|
+
"A flaky test that fails intermittently",
|
|
1466
|
+
"Performance regression: response time > 5s where it used to be < 500ms",
|
|
1467
|
+
"A memory leak causing out-of-memory crashes"
|
|
1468
|
+
],
|
|
1469
|
+
antiPatterns: [
|
|
1470
|
+
"Bug is obvious from the error message (e.g. TypeError: cannot read property of undefined \u2014 fix directly)",
|
|
1471
|
+
"No stack trace or repro steps \u2014 first ask the user for a minimal repro",
|
|
1472
|
+
"A test is failing because the test itself is wrong \u2014 debug the test, not the code"
|
|
1473
|
+
],
|
|
1474
|
+
requires: ["reproduce-bug"],
|
|
1475
|
+
relatedSkills: ["root-cause-five-whys", "reproduce-bug"],
|
|
1476
|
+
tags: ["debug", "rag", "stack-trace", "root-cause", "regression"],
|
|
1477
|
+
examples: [
|
|
1478
|
+
{
|
|
1479
|
+
input: 'TypeError: Cannot read properties of undefined (reading "map") at Council.tsx:847',
|
|
1480
|
+
output: {
|
|
1481
|
+
ragSearch: 'searchRAG(query="Council.tsx undefined map error") returned 2 prior incidents: similar bug fixed in commit a3f9d by adding optional chaining; another similar issue in Council.tsx:612.',
|
|
1482
|
+
stackTraceAnalysis: "Line 847 is inside `renderMessages()`. The most likely cause: `messages` is undefined when `runCouncilPure()` returns early on the first iteration before any agent has produced output.",
|
|
1483
|
+
rootCause: "race condition: messages array is set after the first agent completes, but renderMessages() is called during the loading state when messages is still undefined.",
|
|
1484
|
+
proposedFix: `function renderMessages() {
|
|
1485
|
+
const messages = session.messages ?? [];
|
|
1486
|
+
return messages.map(m => <Message key={m.id} {...m} />);
|
|
1487
|
+
}`,
|
|
1488
|
+
verification: "Add a unit test that calls renderMessages() with messages=undefined and expects [] (not crash). Manual: trigger the bug scenario, confirm no crash."
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
],
|
|
1492
|
+
outputSchema: "{ ragSearch: string; stackTraceAnalysis: string; rootCause: string; proposedFix: string; verification: string }",
|
|
1493
|
+
systemPromptFragment: `You are debugging a bug using the knowledge base.
|
|
1494
|
+
|
|
1495
|
+
## Methodology
|
|
1496
|
+
1. **Extract key terms** from the error message + stack trace (file names, function names, error type).
|
|
1497
|
+
2. **Search the knowledge base**: searchRAG(query="<key terms>")
|
|
1498
|
+
3. **Read the relevant files**: use read_file with line ranges from the stack trace.
|
|
1499
|
+
4. **Identify the root cause**: what's the actual logic error? Don't just fix the symptom.
|
|
1500
|
+
5. **Propose a minimal fix**: the SMALLEST change that addresses the root cause.
|
|
1501
|
+
6. **Specify verification**: how to confirm the fix works (test case + manual steps).
|
|
1502
|
+
|
|
1503
|
+
## Output format (JSON-typed)
|
|
1504
|
+
- ragSearch: string (what you searched + what you found)
|
|
1505
|
+
- stackTraceAnalysis: string (which line + what's likely happening)
|
|
1506
|
+
- rootCause: string (the actual bug, not the symptom)
|
|
1507
|
+
- proposedFix: string (code snippet)
|
|
1508
|
+
- verification: string (test + manual steps)
|
|
1509
|
+
|
|
1510
|
+
## Debugging principles
|
|
1511
|
+
- **Fix the cause, not the symptom** (e.g. don't add a try/catch to silence an error)
|
|
1512
|
+
- **Minimal change** \u2014 don't refactor surrounding code while debugging
|
|
1513
|
+
- **One hypothesis at a time** \u2014 don't list 5 possible causes
|
|
1514
|
+
- **Verify the fix actually fixes** \u2014 write a failing test BEFORE the fix
|
|
1515
|
+
|
|
1516
|
+
Stay under 400 words.${CLARIFICATION_PROTOCOL}`
|
|
1517
|
+
};
|
|
1518
|
+
var rootCauseFiveWhys = {
|
|
1519
|
+
id: "root-cause-five-whys",
|
|
1520
|
+
version: "1.0.0",
|
|
1521
|
+
name: "Root Cause Five Whys",
|
|
1522
|
+
description: 'Apply iterative 5-whys analysis with council consensus. Drill down through 5 levels of "why" to find the systemic root cause, not just the proximate trigger.',
|
|
1523
|
+
category: "debug",
|
|
1524
|
+
requiredRoles: ["sisyphus", "oracle", "chairman"],
|
|
1525
|
+
requiredTools: ["searchRAG", "read_file"],
|
|
1526
|
+
estimatedCost: "high",
|
|
1527
|
+
enabledByDefault: true,
|
|
1528
|
+
builtin: true,
|
|
1529
|
+
triggers: [
|
|
1530
|
+
'A bug that keeps recurring after fixes ("whack-a-mole" symptom)',
|
|
1531
|
+
"A systemic issue affecting multiple files/modules",
|
|
1532
|
+
"A post-mortem on a production incident",
|
|
1533
|
+
"Process issues (not just code) \u2014 why does the team keep making the same mistake?",
|
|
1534
|
+
"Frequent merge failures or regression patterns in a single week"
|
|
1535
|
+
],
|
|
1536
|
+
antiPatterns: [
|
|
1537
|
+
"A trivial bug with obvious cause \u2014 just fix it directly",
|
|
1538
|
+
"A user error (bad input) \u2014 the system should validate, not the user change behavior",
|
|
1539
|
+
"A flaky test \u2014 fix the test, not the underlying system"
|
|
1540
|
+
],
|
|
1541
|
+
requires: ["debug-with-rag"],
|
|
1542
|
+
relatedSkills: ["debug-with-rag", "reproduce-bug"],
|
|
1543
|
+
tags: ["debug", "root-cause", "systemic", "postmortem", "five-whys"],
|
|
1544
|
+
examples: [
|
|
1545
|
+
{
|
|
1546
|
+
input: "Production incident: 3 outages in 2 weeks, all caused by the same API rate limit hit",
|
|
1547
|
+
output: {
|
|
1548
|
+
whys: [
|
|
1549
|
+
{ level: 1, question: "Why did the service go down?", answer: "The MiniMax API returned 429 Too Many Requests at peak load." },
|
|
1550
|
+
{ level: 2, question: "Why did we hit the rate limit?", answer: "We made 200 requests/minute; the limit is 100/minute." },
|
|
1551
|
+
{ level: 3, question: "Why did we exceed the rate limit?", answer: "A single user request triggered 200 sub-requests for parallel tool calls." },
|
|
1552
|
+
{ level: 4, question: "Why did one request trigger 200 sub-requests?", answer: "The agent loop calls all tools in parallel without batching." },
|
|
1553
|
+
{ level: 5, question: "Why don't we batch tool calls?", answer: "No rate limiter in the agent loop. The team assumed the API limit was high enough." }
|
|
1554
|
+
],
|
|
1555
|
+
rootCause: "No client-side rate limiting. The agent loop fires parallel tool calls without backoff or batching.",
|
|
1556
|
+
systemicFix: "Add a token-bucket rate limiter (100 req/min) before the API client. Implement batching for independent tool calls. Add a circuit breaker that pauses on 429 responses.",
|
|
1557
|
+
processFix: "Add a pre-deploy load test that runs 1000 concurrent requests and verifies rate-limit handling.",
|
|
1558
|
+
councilConsensus: 'Chairman: "All three council members agree on the systemic root cause. The process fix is as important as the code fix \u2014 load tests should be CI-required."'
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
],
|
|
1562
|
+
outputSchema: "{ whys: Array<{ level: number; question: string; answer: string }>; rootCause: string; systemicFix: string; processFix: string; councilConsensus: string }",
|
|
1563
|
+
systemPromptFragment: `You are applying the 5-whys technique with council consensus.
|
|
1564
|
+
|
|
1565
|
+
## Methodology
|
|
1566
|
+
1. **State the symptom** as the starting point for "why?"
|
|
1567
|
+
2. **Ask "why?" 5 times**, drilling down through layers of cause
|
|
1568
|
+
3. **Each answer must be FACTUAL**, not speculative
|
|
1569
|
+
4. **At the 5th why**, you've reached the systemic root cause
|
|
1570
|
+
5. **Propose TWO fixes**: systemic (code/architecture) + process (team/workflow)
|
|
1571
|
+
6. **Get council consensus**: chairman summarizes agreement from sisyphus (orchestrator) and oracle (analyst)
|
|
1572
|
+
|
|
1573
|
+
## Output format (JSON-typed)
|
|
1574
|
+
- whys: Array<{ level: number; question: string; answer: string }>
|
|
1575
|
+
- rootCause: string (the deepest why)
|
|
1576
|
+
- systemicFix: string (code/architecture change)
|
|
1577
|
+
- processFix: string (team/workflow change)
|
|
1578
|
+
- councilConsensus: string (chairman's summary of agreement)
|
|
1579
|
+
|
|
1580
|
+
## Anti-patterns to avoid
|
|
1581
|
+
- **"Human error"** is NEVER the root cause \u2014 the system should make the error impossible
|
|
1582
|
+
- **"Lack of training"** is NEVER the root cause \u2014 automation + checklists > training
|
|
1583
|
+
- **"Not enough time"** is NEVER the root cause \u2014 find the constraint, not the excuse
|
|
1584
|
+
- Stop at the FIRST level if it's a trivial bug \u2014 don't overthink simple cases
|
|
1585
|
+
|
|
1586
|
+
Stay under 500 words.${CLARIFICATION_PROTOCOL}`
|
|
1587
|
+
};
|
|
1588
|
+
var registerCodingSkill2 = registerCodingSkill;
|
|
1589
|
+
registerCodingSkill2(reproduceBug);
|
|
1590
|
+
registerCodingSkill2(debugWithRag);
|
|
1591
|
+
registerCodingSkill2(rootCauseFiveWhys);
|
|
1592
|
+
|
|
1593
|
+
// src/agents/skills/builtin/docs.ts
|
|
1594
|
+
var CLARIFICATION_PROTOCOL2 = `
|
|
1595
|
+
|
|
1596
|
+
WHEN TO ASK THE USER (clarification):
|
|
1597
|
+
If a single missing fact would materially change your output (target platform, scope, a binary design choice with significant trade-offs, a constraint you cannot safely assume), pause and ask the user by appending EXACTLY this block at the end of your message:
|
|
1598
|
+
|
|
1599
|
+
---QUESTION---
|
|
1600
|
+
{ "question": "One focused question", "choices": ["Option A", "Option B", "Option C"], "context": "Why this matters" }
|
|
1601
|
+
---END---
|
|
1602
|
+
|
|
1603
|
+
Rules for clarifications:
|
|
1604
|
+
- Ask AT MOST ONE question per turn, and only when genuinely blocked.
|
|
1605
|
+
- Prefer a small set of concrete "choices" (2-4). The user can still type a custom answer.
|
|
1606
|
+
- Do NOT ask for information that could be reasonably assumed or already in shared context.
|
|
1607
|
+
- If you can proceed with a sound documented assumption, DO SO instead of asking.`;
|
|
1608
|
+
var writeReadme = {
|
|
1609
|
+
id: "write-readme",
|
|
1610
|
+
version: "1.0.0",
|
|
1611
|
+
name: "Write README",
|
|
1612
|
+
description: "Generate a project README from the codebase. Sections: Title, Badges, Description, Quick Start, Architecture, Development, Testing, Deployment, Contributing, License.",
|
|
1613
|
+
category: "docs",
|
|
1614
|
+
requiredRoles: ["prometheus", "atlas"],
|
|
1615
|
+
requiredTools: ["read_file", "grep_content"],
|
|
1616
|
+
estimatedCost: "medium",
|
|
1617
|
+
enabledByDefault: true,
|
|
1618
|
+
builtin: true,
|
|
1619
|
+
triggers: [
|
|
1620
|
+
"A new project is being created",
|
|
1621
|
+
"Existing README is missing or stale",
|
|
1622
|
+
"A project is being open-sourced",
|
|
1623
|
+
"Onboarding new team members (they read the README first)"
|
|
1624
|
+
],
|
|
1625
|
+
antiPatterns: [
|
|
1626
|
+
"README exists and is recent \u2014 update instead of regenerate",
|
|
1627
|
+
"Personal/internal project (README rarely matters)",
|
|
1628
|
+
"Library with auto-generated docs from source code comments"
|
|
1629
|
+
],
|
|
1630
|
+
requires: [],
|
|
1631
|
+
relatedSkills: ["write-tsdoc", "write-changelog"],
|
|
1632
|
+
tags: ["docs", "readme", "onboarding", "open-source"],
|
|
1633
|
+
examples: [
|
|
1634
|
+
{
|
|
1635
|
+
input: "Generate README for AnathemaBrain (Electron + React + TypeScript AI agent platform)",
|
|
1636
|
+
output: {
|
|
1637
|
+
readme: `# AnathemaBrain
|
|
1638
|
+
|
|
1639
|
+
> An educational Python project for learning how coding agents are built.
|
|
1640
|
+
|
|
1641
|
+
AnathemaBrain is an Electron desktop app... (truncated)
|
|
1642
|
+
|
|
1643
|
+
## Quick Start
|
|
1644
|
+
|
|
1645
|
+
\`\`\`bash
|
|
1646
|
+
npm install
|
|
1647
|
+
npm run dev
|
|
1648
|
+
\`\`\`
|
|
1649
|
+
|
|
1650
|
+
## Architecture
|
|
1651
|
+
|
|
1652
|
+
- **Main process** (electron/main/): Node.js, IPC handlers, LLM streaming
|
|
1653
|
+
- **Renderer** (src/): React 19 UI, Zustand stores
|
|
1654
|
+
- **Agent system** (src/agents/): Council multi-agent orchestration
|
|
1655
|
+
|
|
1656
|
+
## Development
|
|
1657
|
+
|
|
1658
|
+
\`\`\`bash
|
|
1659
|
+
npm run typecheck
|
|
1660
|
+
npm run test
|
|
1661
|
+
npm run lint
|
|
1662
|
+
\`\`\`
|
|
1663
|
+
|
|
1664
|
+
## Testing
|
|
1665
|
+
|
|
1666
|
+
96+ tests via Vitest + jsdom.
|
|
1667
|
+
|
|
1668
|
+
## Deployment
|
|
1669
|
+
|
|
1670
|
+
\`\`\`bash
|
|
1671
|
+
npm run build
|
|
1672
|
+
npm run electron:build
|
|
1673
|
+
\`\`\`
|
|
1674
|
+
|
|
1675
|
+
## Contributing
|
|
1676
|
+
|
|
1677
|
+
See docs/plans/ for active roadmaps. Pull requests welcome.
|
|
1678
|
+
|
|
1679
|
+
## License
|
|
1680
|
+
|
|
1681
|
+
Apache 2.0
|
|
1682
|
+
`,
|
|
1683
|
+
sections: ["Title + tagline", "Badges", "Description", "Quick Start", "Architecture diagram", "Development commands", "Testing", "Deployment", "Contributing link", "License"]
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
],
|
|
1687
|
+
outputSchema: "{ readme: string; sections: string[] }",
|
|
1688
|
+
systemPromptFragment: `You are writing a project README.md.
|
|
1689
|
+
|
|
1690
|
+
## Required sections (in order)
|
|
1691
|
+
1. **Title + tagline** (1 sentence): what is this project?
|
|
1692
|
+
2. **Badges** (optional): CI status, npm version, license
|
|
1693
|
+
3. **Description** (2-3 paragraphs): what does it do, who is it for
|
|
1694
|
+
4. **Quick Start** (5-10 lines): install + run + see something working
|
|
1695
|
+
5. **Architecture** (1-2 paragraphs + diagram): high-level structure
|
|
1696
|
+
6. **Development** (commands): how to run tests, typecheck, lint
|
|
1697
|
+
7. **Testing**: what kind of tests exist, how to run them
|
|
1698
|
+
8. **Deployment** (if applicable): how to build + ship
|
|
1699
|
+
9. **Contributing**: link to CONTRIBUTING.md or describe PR process
|
|
1700
|
+
10. **License**: license type
|
|
1701
|
+
|
|
1702
|
+
## Output format (JSON-typed)
|
|
1703
|
+
- readme: string (the full README content in markdown)
|
|
1704
|
+
- sections: string[] (the sections you included)
|
|
1705
|
+
|
|
1706
|
+
## README principles
|
|
1707
|
+
- **Show, don't tell**: code examples for non-trivial concepts
|
|
1708
|
+
- **Up-to-date**: don't include commands that don't work
|
|
1709
|
+
- **Scannable**: use headers, bullets, code blocks
|
|
1710
|
+
- **One page max**: link to deeper docs rather than nesting everything
|
|
1711
|
+
|
|
1712
|
+
Stay under 600 words.${CLARIFICATION_PROTOCOL2}`
|
|
1713
|
+
};
|
|
1714
|
+
var writeTsdoc = {
|
|
1715
|
+
id: "write-tsdoc",
|
|
1716
|
+
version: "1.0.0",
|
|
1717
|
+
name: "Write TSDoc",
|
|
1718
|
+
description: "Generate TSDoc/JSDoc comments for undocumented exports. Output: docstrings with @param, @returns, @throws, @example tags following the TSDoc standard.",
|
|
1719
|
+
category: "docs",
|
|
1720
|
+
requiredRoles: ["atlas"],
|
|
1721
|
+
requiredTools: ["read_file"],
|
|
1722
|
+
estimatedCost: "low",
|
|
1723
|
+
enabledByDefault: true,
|
|
1724
|
+
builtin: true,
|
|
1725
|
+
triggers: [
|
|
1726
|
+
"A function or class lacks documentation",
|
|
1727
|
+
"A public API is exported without TSDoc",
|
|
1728
|
+
'IDE intellisense shows "any" or missing info for a function',
|
|
1729
|
+
"A new utility is added to a shared library"
|
|
1730
|
+
],
|
|
1731
|
+
antiPatterns: [
|
|
1732
|
+
"The function is private/internal \u2014 no need for TSDoc",
|
|
1733
|
+
"Auto-generated docs already exist (typedoc, etc.)",
|
|
1734
|
+
"The function is self-explanatory (getName \u2192 returns name)"
|
|
1735
|
+
],
|
|
1736
|
+
requires: [],
|
|
1737
|
+
relatedSkills: ["write-readme"],
|
|
1738
|
+
tags: ["docs", "tsdoc", "jsdoc", "intellisense", "api-docs"],
|
|
1739
|
+
examples: [
|
|
1740
|
+
{
|
|
1741
|
+
input: "Add TSDoc to src/lib/formatDuration.ts",
|
|
1742
|
+
output: {
|
|
1743
|
+
documented: `/**
|
|
1744
|
+
* Formats a duration in milliseconds as a human-readable string.
|
|
1745
|
+
*
|
|
1746
|
+
* @param ms - Duration in milliseconds. Must be a non-negative finite number.
|
|
1747
|
+
* @param opts - Formatting options.
|
|
1748
|
+
* @param opts.style - 'compact' (1h 23m), 'full' (1:23:45), or 'minimal' (23m).
|
|
1749
|
+
* Defaults to 'compact'.
|
|
1750
|
+
* @returns The formatted duration string.
|
|
1751
|
+
* @throws {RangeError} If ms is negative.
|
|
1752
|
+
* @throws {TypeError} If ms is NaN or Infinity.
|
|
1753
|
+
*
|
|
1754
|
+
* @example
|
|
1755
|
+
* formatDuration(4980000); // '1h 23m'
|
|
1756
|
+
* formatDuration(4980000, { style: 'full' }); // '1:23:45'
|
|
1757
|
+
* formatDuration(60000, { style: 'minimal' }); // '1m'
|
|
1758
|
+
*/
|
|
1759
|
+
export function formatDuration(ms: number, opts?: { style?: 'compact' | 'full' | 'minimal' }): string {
|
|
1760
|
+
// ... existing implementation
|
|
1761
|
+
}`,
|
|
1762
|
+
tagsAdded: ["@param", "@param (nested)", "@returns", "@throws x2", "@example"]
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
],
|
|
1766
|
+
outputSchema: "{ documented: string; tagsAdded: string[] }",
|
|
1767
|
+
systemPromptFragment: `You are writing TSDoc comments for a TypeScript function or class.
|
|
1768
|
+
|
|
1769
|
+
## TSDoc tags (use these)
|
|
1770
|
+
- \`@param {type} name - description\` \u2014 for each parameter
|
|
1771
|
+
- \`@returns description\` (or \`@return\` legacy alias)
|
|
1772
|
+
- \`@throws {ErrorType} condition\` \u2014 for each throw path
|
|
1773
|
+
- \`@example\` followed by code block (for non-trivial usage)
|
|
1774
|
+
- \`@deprecated reason\` \u2014 if the function is being phased out
|
|
1775
|
+
- \`@see FunctionName\` \u2014 for cross-references
|
|
1776
|
+
- \`@remarks\` \u2014 additional context
|
|
1777
|
+
- \`@internal\` \u2014 for internal helpers (suppresses from public docs)
|
|
1778
|
+
|
|
1779
|
+
## Output format (JSON-typed)
|
|
1780
|
+
- documented: string (the full code block with TSDoc added)
|
|
1781
|
+
- tagsAdded: string[] (which tags you included)
|
|
1782
|
+
|
|
1783
|
+
## TSDoc principles
|
|
1784
|
+
- **First sentence is the summary**: ends with a period, capital letter
|
|
1785
|
+
- **One @param per parameter**: don't omit even if obvious
|
|
1786
|
+
- **@throws for EVERY throw path**: callers need to know
|
|
1787
|
+
- **@example for non-trivial usage**: best way to communicate intent
|
|
1788
|
+
- **No marketing language**: just describe what the function does
|
|
1789
|
+
- **No implementation details**: TSDoc is for the API contract, not internals
|
|
1790
|
+
|
|
1791
|
+
Stay under 400 words.${CLARIFICATION_PROTOCOL2}`
|
|
1792
|
+
};
|
|
1793
|
+
var writeChangelog = {
|
|
1794
|
+
id: "write-changelog",
|
|
1795
|
+
version: "1.0.0",
|
|
1796
|
+
name: "Write Changelog",
|
|
1797
|
+
description: "Generate a CHANGELOG.md entry from git history. Follows Keep a Changelog format with sections: Added, Changed, Deprecated, Removed, Fixed, Security. Groups commits by category, deduplicates, writes user-facing descriptions.",
|
|
1798
|
+
category: "docs",
|
|
1799
|
+
requiredRoles: ["atlas"],
|
|
1800
|
+
requiredTools: [],
|
|
1801
|
+
estimatedCost: "low",
|
|
1802
|
+
enabledByDefault: true,
|
|
1803
|
+
builtin: true,
|
|
1804
|
+
triggers: [
|
|
1805
|
+
"A release is being prepared",
|
|
1806
|
+
"A sprint/iteration ends",
|
|
1807
|
+
"Significant changes have accumulated since the last entry",
|
|
1808
|
+
"A user-visible breaking change happened"
|
|
1809
|
+
],
|
|
1810
|
+
antiPatterns: [
|
|
1811
|
+
"No commits since the last changelog entry \u2014 skip",
|
|
1812
|
+
"Internal refactors with no user impact \u2014 exclude",
|
|
1813
|
+
'Doc-only changes \u2014 mention briefly in "Changed" or skip'
|
|
1814
|
+
],
|
|
1815
|
+
requires: [],
|
|
1816
|
+
relatedSkills: ["write-readme", "write-tsdoc"],
|
|
1817
|
+
tags: ["docs", "changelog", "release-notes", "keep-a-changelog"],
|
|
1818
|
+
examples: [
|
|
1819
|
+
{
|
|
1820
|
+
input: "Generate changelog for v0.2.0 from commits since v0.1.0 (last 30 commits)",
|
|
1821
|
+
output: {
|
|
1822
|
+
changelog: `# Changelog
|
|
1823
|
+
|
|
1824
|
+
All notable changes to AnathemaBrain are documented in this file.
|
|
1825
|
+
|
|
1826
|
+
## [0.2.0] - 2026-06-28
|
|
1827
|
+
|
|
1828
|
+
### Added
|
|
1829
|
+
- **BrainEvent provider-neutral types** (Phase 11): 12 discriminated union events for agent lifecycle, message streaming, tool execution, errors
|
|
1830
|
+
- **EventBus**: typed in-memory pub/sub with error isolation
|
|
1831
|
+
- **AgentHarness**: pure agent loop yielding AsyncIterable<BrainEvent>
|
|
1832
|
+
- **runCouncilPure()**: council orchestration without window.electronAPI dependency
|
|
1833
|
+
- **5 built-in coding tools**: read_file, write_file, edit_file, bash, grep_content
|
|
1834
|
+
- **14 senior-grade coding skills** across 4 categories (Planning, Refactoring, Debugging, Review)
|
|
1835
|
+
|
|
1836
|
+
### Changed
|
|
1837
|
+
- LLM streaming now emits BrainEvents alongside the IPC bridge (renderer unchanged)
|
|
1838
|
+
- Agent system refactored to use AgentHarness as the loop driver
|
|
1839
|
+
|
|
1840
|
+
### Fixed
|
|
1841
|
+
- Council event timing: agent_start/agent_end now emit at correct boundaries
|
|
1842
|
+
- Tool execution start events fire when provider yields tool_call deltas
|
|
1843
|
+
|
|
1844
|
+
### Security
|
|
1845
|
+
- Grok OAuth token refresh now respects rate limits (fixed race condition)
|
|
1846
|
+
|
|
1847
|
+
[0.1.0] - 2026-06-01
|
|
1848
|
+
... (previous entries)
|
|
1849
|
+
`,
|
|
1850
|
+
sectionsUsed: ["Added", "Changed", "Fixed", "Security"],
|
|
1851
|
+
commitsProcessed: 30
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
],
|
|
1855
|
+
outputSchema: "{ changelog: string; sectionsUsed: string[]; commitsProcessed: number }",
|
|
1856
|
+
systemPromptFragment: `You are writing a CHANGELOG.md entry from git history.
|
|
1857
|
+
|
|
1858
|
+
## Keep a Changelog format
|
|
1859
|
+
Sections (use only those with content):
|
|
1860
|
+
- **Added** \u2014 new features
|
|
1861
|
+
- **Changed** \u2014 changes in existing functionality
|
|
1862
|
+
- **Deprecated** \u2014 soon-to-be removed features
|
|
1863
|
+
- **Removed** \u2014 now removed features
|
|
1864
|
+
- **Fixed** \u2014 bug fixes
|
|
1865
|
+
- **Security** \u2014 vulnerability fixes
|
|
1866
|
+
|
|
1867
|
+
## Methodology
|
|
1868
|
+
1. **List commits since the last release** (use git log).
|
|
1869
|
+
2. **Categorize each commit** into the right section.
|
|
1870
|
+
3. **Translate to user-facing language**: "feat: add event bus" \u2192 "EventBus: typed in-memory pub/sub".
|
|
1871
|
+
4. **Group related commits**: if 5 commits all touch the event system, write ONE entry.
|
|
1872
|
+
5. **Skip internal-only changes**: refactors that don't change behavior.
|
|
1873
|
+
6. **Add version + date**: top of the entry.
|
|
1874
|
+
|
|
1875
|
+
## Output format (JSON-typed)
|
|
1876
|
+
- changelog: string (the full CHANGELOG.md content)
|
|
1877
|
+
- sectionsUsed: string[] (which sections you wrote)
|
|
1878
|
+
- commitsProcessed: number
|
|
1879
|
+
|
|
1880
|
+
## Changelog principles
|
|
1881
|
+
- **User-facing language**: write for end-users, not developers
|
|
1882
|
+
- **Group, don't list**: 1 entry per feature, not 1 per commit
|
|
1883
|
+
- **Highlight breaking changes**: bold or callout
|
|
1884
|
+
- **Link to issues/PRs**: reference #123, fixes #456
|
|
1885
|
+
- **Date format**: ISO 8601 (YYYY-MM-DD)
|
|
1886
|
+
|
|
1887
|
+
Stay under 500 words.${CLARIFICATION_PROTOCOL2}`
|
|
1888
|
+
};
|
|
1889
|
+
registerCodingSkill(writeReadme);
|
|
1890
|
+
registerCodingSkill(writeTsdoc);
|
|
1891
|
+
registerCodingSkill(writeChangelog);
|
|
1892
|
+
|
|
1893
|
+
// src/agents/skills/builtin/git-ops.ts
|
|
1894
|
+
var CLARIFICATION_PROTOCOL3 = `
|
|
1895
|
+
|
|
1896
|
+
WHEN TO ASK THE USER (clarification):
|
|
1897
|
+
If a single missing fact would materially change your output (target platform, scope, a binary design choice with significant trade-offs, a constraint you cannot safely assume), pause and ask the user by appending EXACTLY this block at the end of your message:
|
|
1898
|
+
|
|
1899
|
+
---QUESTION---
|
|
1900
|
+
{ "question": "One focused question", "choices": ["Option A", "Option B", "Option C"], "context": "Why this matters" }
|
|
1901
|
+
---END---
|
|
1902
|
+
|
|
1903
|
+
Rules for clarifications:
|
|
1904
|
+
- Ask AT MOST ONE question per turn, and only when genuinely blocked.
|
|
1905
|
+
- Prefer a small set of concrete "choices" (2-4). The user can still type a custom answer.
|
|
1906
|
+
- Do NOT ask for information that could be reasonably assumed or already in shared context.
|
|
1907
|
+
- If you can proceed with a sound documented assumption, DO SO instead of asking.`;
|
|
1908
|
+
var commitMessage = {
|
|
1909
|
+
id: "commit-message",
|
|
1910
|
+
version: "1.0.0",
|
|
1911
|
+
name: "Commit Message",
|
|
1912
|
+
description: "Generate a Conventional Commits message from a staged git diff. Format: <type>(<scope>): <subject>. Body explains WHY. Footer references issues.",
|
|
1913
|
+
category: "ops",
|
|
1914
|
+
requiredRoles: ["atlas"],
|
|
1915
|
+
requiredTools: [],
|
|
1916
|
+
estimatedCost: "low",
|
|
1917
|
+
enabledByDefault: true,
|
|
1918
|
+
builtin: true,
|
|
1919
|
+
triggers: [
|
|
1920
|
+
"A developer has staged changes and needs a commit message",
|
|
1921
|
+
"A PR description requires a list of conventional commits",
|
|
1922
|
+
"A CHANGELOG is generated from commit history"
|
|
1923
|
+
],
|
|
1924
|
+
antiPatterns: [
|
|
1925
|
+
"Empty diff (nothing staged) \u2014 fail fast",
|
|
1926
|
+
"Merge commits or reverts \u2014 different format",
|
|
1927
|
+
"Multi-purpose commits (refactor + feat + fix in one) \u2014 split first"
|
|
1928
|
+
],
|
|
1929
|
+
requires: [],
|
|
1930
|
+
relatedSkills: ["pr-description", "write-changelog"],
|
|
1931
|
+
tags: ["git", "commit", "conventional-commits", "changelog"],
|
|
1932
|
+
examples: [
|
|
1933
|
+
{
|
|
1934
|
+
input: "Staged diff: 3 files changed, +45/-12. Adds brain event types and EventBus class.",
|
|
1935
|
+
output: {
|
|
1936
|
+
subject: "feat(events): add BrainEvent types + EventBus (Phase 11)",
|
|
1937
|
+
body: `Adds the provider-neutral event layer:
|
|
1938
|
+
- 12 BrainEvent discriminated union types
|
|
1939
|
+
- EventBus class with typed subscribers + error isolation
|
|
1940
|
+
- emit/subscribe/subscribeAll API
|
|
1941
|
+
|
|
1942
|
+
Foundation for Phase 12 (AgentHarness extraction).`,
|
|
1943
|
+
footer: "Refs: docs/plans/2026-06-28-zelari-code.md",
|
|
1944
|
+
type: "feat",
|
|
1945
|
+
scope: "events"
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
],
|
|
1949
|
+
outputSchema: '{ subject: string; body: string; footer: string; type: "feat" | "fix" | "refactor" | "docs" | "test" | "chore" | "perf" | "style"; scope: string }',
|
|
1950
|
+
systemPromptFragment: `You are writing a Conventional Commits message.
|
|
1951
|
+
|
|
1952
|
+
## Format
|
|
1953
|
+
<type>(<scope>): <subject>
|
|
1954
|
+
|
|
1955
|
+
<body \u2014 explain WHY, not WHAT>
|
|
1956
|
+
|
|
1957
|
+
<footer \u2014 issue refs, breaking changes>
|
|
1958
|
+
|
|
1959
|
+
## Types
|
|
1960
|
+
- feat: new feature
|
|
1961
|
+
- fix: bug fix
|
|
1962
|
+
- refactor: code change that neither fixes a bug nor adds a feature
|
|
1963
|
+
- docs: documentation only
|
|
1964
|
+
- test: adding/correcting tests
|
|
1965
|
+
- chore: build/tooling changes (deps, CI, config)
|
|
1966
|
+
- perf: performance improvement
|
|
1967
|
+
- style: formatting, missing semicolons, etc. (no logic change)
|
|
1968
|
+
|
|
1969
|
+
## Subject rules
|
|
1970
|
+
- Imperative mood ("add" not "added")
|
|
1971
|
+
- Lowercase (except proper nouns)
|
|
1972
|
+
- Max 72 characters
|
|
1973
|
+
- No trailing period
|
|
1974
|
+
|
|
1975
|
+
## Body rules
|
|
1976
|
+
- Wrap at 72 characters
|
|
1977
|
+
- Explain WHY the change was made
|
|
1978
|
+
- Bullet points for multiple distinct changes
|
|
1979
|
+
|
|
1980
|
+
## Footer rules
|
|
1981
|
+
- \`Refs: #123\` for related issues
|
|
1982
|
+
- \`BREAKING CHANGE: <description>\` for breaking changes
|
|
1983
|
+
- \`Co-authored-by: Name <email>\` for pair work
|
|
1984
|
+
|
|
1985
|
+
## Output format (JSON-typed)
|
|
1986
|
+
- subject: string
|
|
1987
|
+
- body: string
|
|
1988
|
+
- footer: string
|
|
1989
|
+
- type: ConventionalCommitType
|
|
1990
|
+
- scope: string (optional, lowercase module name)
|
|
1991
|
+
|
|
1992
|
+
Stay under 300 words.${CLARIFICATION_PROTOCOL3}`
|
|
1993
|
+
};
|
|
1994
|
+
var prDescription = {
|
|
1995
|
+
id: "pr-description",
|
|
1996
|
+
version: "1.0.0",
|
|
1997
|
+
name: "PR Description",
|
|
1998
|
+
description: "Generate a pull request description with sections: Summary, Motivation, Changes, Testing, Screenshots, Risks. Synthesizes from commits + diff + related issues.",
|
|
1999
|
+
category: "ops",
|
|
2000
|
+
requiredRoles: ["prometheus", "atlas"],
|
|
2001
|
+
requiredTools: [],
|
|
2002
|
+
estimatedCost: "medium",
|
|
2003
|
+
enabledByDefault: true,
|
|
2004
|
+
builtin: true,
|
|
2005
|
+
triggers: [
|
|
2006
|
+
"A PR is ready for review and needs a description",
|
|
2007
|
+
"A PR template requires filling in structured sections",
|
|
2008
|
+
"A large change (>200 LOC) needs careful framing for reviewers"
|
|
2009
|
+
],
|
|
2010
|
+
antiPatterns: [
|
|
2011
|
+
"A 1-line typo fix \u2014 title only, no body needed",
|
|
2012
|
+
"A WIP / draft PR \u2014 defer until ready",
|
|
2013
|
+
"Generated code (codegen output) \u2014 link to the generator instead"
|
|
2014
|
+
],
|
|
2015
|
+
requires: ["commit-message"],
|
|
2016
|
+
relatedSkills: ["commit-message", "write-changelog"],
|
|
2017
|
+
tags: ["git", "pr", "pull-request", "review", "documentation"],
|
|
2018
|
+
examples: [
|
|
2019
|
+
{
|
|
2020
|
+
input: "PR for Phase 11 of AnathemaCoder: BrainEvent types + EventBus + LLM streaming wiring",
|
|
2021
|
+
output: {
|
|
2022
|
+
title: "Phase 11: Provider-neutral events foundation",
|
|
2023
|
+
summary: "Introduces the BrainEvent discriminated union and EventBus \u2014 the provider-neutral contract between LLM providers and any frontend (Electron renderer today, Ink CLI in Phase 14).",
|
|
2024
|
+
motivation: "The current IPC bridge (`emitChunk`/`emitDone`) couples every frontend to provider-specific chunks. Any new frontend (CLI, mobile, web) would need to duplicate SSE parsing per provider. This is the foundation that lets Phase 12 extract an AgentHarness usable from any context.",
|
|
2025
|
+
changes: [
|
|
2026
|
+
"Added `electron/shared/events.ts` \u2014 12 discriminated union types (agent_start, message_delta, tool_execution_start, etc.)",
|
|
2027
|
+
"Added `electron/shared/eventBus.ts` \u2014 typed pub/sub with error isolation",
|
|
2028
|
+
"Wired 5 local emit helpers in `electron/main/ipc/llm.ts` (emitAgentStart, emitMessageStart, emitMessageDelta, emitMessageEnd, emitBrainError)",
|
|
2029
|
+
"Tool execution emits `BrainToolExecutionStartEvent` when provider yields tool_call",
|
|
2030
|
+
"Council emits `agent_start` / `agent_end` at run boundaries",
|
|
2031
|
+
"6 new vitest tests for EventBus (typed delivery, wildcard, error isolation)"
|
|
2032
|
+
],
|
|
2033
|
+
testing: "96/96 tests passing. New: 6 EventBus tests in tests/unit/eventBus.test.ts. Manual: existing Electron renderer unchanged (verified Council chat still streams correctly).",
|
|
2034
|
+
risks: [
|
|
2035
|
+
"Adding event emission alongside IPC is purely additive \u2014 no migration needed for existing renderer code.",
|
|
2036
|
+
"Council event timing verified manually: agent_start fires before any message events, agent_end fires after."
|
|
2037
|
+
]
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
],
|
|
2041
|
+
outputSchema: "{ title: string; summary: string; motivation: string; changes: string[]; testing: string; risks: string[] }",
|
|
2042
|
+
systemPromptFragment: `You are writing a Pull Request description.
|
|
2043
|
+
|
|
2044
|
+
## Sections (use all)
|
|
2045
|
+
1. **Title** (max 72 chars, imperative mood)
|
|
2046
|
+
2. **Summary** (1 paragraph): what does this PR do?
|
|
2047
|
+
3. **Motivation** (1-2 paragraphs): WHY? What problem does it solve?
|
|
2048
|
+
4. **Changes** (bullet list, 5-15 items): specific files/concepts changed
|
|
2049
|
+
5. **Testing** (1 paragraph): how was it verified? Test counts, manual steps
|
|
2050
|
+
6. **Risks** (bullet list, 2-5 items): what could go wrong? Migration needed?
|
|
2051
|
+
|
|
2052
|
+
## Output format (JSON-typed)
|
|
2053
|
+
- title: string
|
|
2054
|
+
- summary: string
|
|
2055
|
+
- motivation: string
|
|
2056
|
+
- changes: string[]
|
|
2057
|
+
- testing: string
|
|
2058
|
+
- risks: string[]
|
|
2059
|
+
|
|
2060
|
+
## PR description principles
|
|
2061
|
+
- **Reviewer-first**: optimize for someone who has NEVER seen this code
|
|
2062
|
+
- **Concrete not abstract**: cite file paths, line numbers, function names
|
|
2063
|
+
- **Risk-honest**: don't hide breaking changes or migration steps
|
|
2064
|
+
- **Self-contained**: PR description should make sense without clicking through to issues
|
|
2065
|
+
|
|
2066
|
+
Stay under 500 words.${CLARIFICATION_PROTOCOL3}`
|
|
2067
|
+
};
|
|
2068
|
+
var ciPipeline = {
|
|
2069
|
+
id: "ci-pipeline",
|
|
2070
|
+
version: "1.0.0",
|
|
2071
|
+
name: "CI Pipeline",
|
|
2072
|
+
description: "Generate a GitHub Actions workflow YAML that runs typecheck + lint + test + build on every push and PR. Configures Node version, caching, and failure annotations.",
|
|
2073
|
+
category: "ops",
|
|
2074
|
+
requiredRoles: ["atlas"],
|
|
2075
|
+
requiredTools: [],
|
|
2076
|
+
estimatedCost: "medium",
|
|
2077
|
+
enabledByDefault: true,
|
|
2078
|
+
builtin: true,
|
|
2079
|
+
triggers: [
|
|
2080
|
+
"A project is being open-sourced and needs CI",
|
|
2081
|
+
"Existing CI is missing a step (e.g. no typecheck)",
|
|
2082
|
+
"A new test framework is added",
|
|
2083
|
+
"A release pipeline is being automated"
|
|
2084
|
+
],
|
|
2085
|
+
antiPatterns: [
|
|
2086
|
+
"The project has no tests yet \u2014 write tests first, then CI",
|
|
2087
|
+
"A different CI system is in use (CircleCI, GitLab CI) \u2014 use that one",
|
|
2088
|
+
"The project is a personal/internal tool (CI rarely matters)"
|
|
2089
|
+
],
|
|
2090
|
+
requires: [],
|
|
2091
|
+
relatedSkills: ["write-changelog"],
|
|
2092
|
+
tags: ["ci", "github-actions", "automation", "pipeline"],
|
|
2093
|
+
examples: [
|
|
2094
|
+
{
|
|
2095
|
+
input: "Generate GitHub Actions for AnathemaBrain (Electron + React + TS + Vitest)",
|
|
2096
|
+
output: {
|
|
2097
|
+
workflowFile: ".github/workflows/ci.yml",
|
|
2098
|
+
content: `name: CI
|
|
2099
|
+
|
|
2100
|
+
on:
|
|
2101
|
+
push:
|
|
2102
|
+
branches: [main]
|
|
2103
|
+
pull_request:
|
|
2104
|
+
branches: [main]
|
|
2105
|
+
|
|
2106
|
+
jobs:
|
|
2107
|
+
test:
|
|
2108
|
+
runs-on: ubuntu-latest
|
|
2109
|
+
timeout-minutes: 15
|
|
2110
|
+
strategy:
|
|
2111
|
+
matrix:
|
|
2112
|
+
node-version: [20.x]
|
|
2113
|
+
|
|
2114
|
+
steps:
|
|
2115
|
+
- uses: actions/checkout@v4
|
|
2116
|
+
|
|
2117
|
+
- name: Setup Node.js \${{ matrix.node-version }}
|
|
2118
|
+
uses: actions/setup-node@v4
|
|
2119
|
+
with:
|
|
2120
|
+
node-version: \${{ matrix.node-version }}
|
|
2121
|
+
cache: 'npm'
|
|
2122
|
+
|
|
2123
|
+
- name: Install dependencies
|
|
2124
|
+
run: npm ci
|
|
2125
|
+
|
|
2126
|
+
- name: Typecheck (node config)
|
|
2127
|
+
run: npm run typecheck:node
|
|
2128
|
+
|
|
2129
|
+
- name: Typecheck (web config)
|
|
2130
|
+
run: npm run typecheck:web
|
|
2131
|
+
|
|
2132
|
+
- name: Lint
|
|
2133
|
+
run: npm run lint
|
|
2134
|
+
|
|
2135
|
+
- name: Test
|
|
2136
|
+
run: npm run test
|
|
2137
|
+
|
|
2138
|
+
- name: Build (Electron renderer + main)
|
|
2139
|
+
run: npm run build
|
|
2140
|
+
|
|
2141
|
+
- name: Upload coverage
|
|
2142
|
+
if: success()
|
|
2143
|
+
uses: actions/upload-artifact@v4
|
|
2144
|
+
with:
|
|
2145
|
+
name: coverage-\${{ matrix.node-version }}
|
|
2146
|
+
path: coverage/
|
|
2147
|
+
`,
|
|
2148
|
+
jobs: ["test"],
|
|
2149
|
+
triggers: ["push to main", "pull_request to main"],
|
|
2150
|
+
nodeVersion: "20.x"
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
],
|
|
2154
|
+
outputSchema: "{ workflowFile: string; content: string; jobs: string[]; triggers: string[]; nodeVersion: string }",
|
|
2155
|
+
systemPromptFragment: `You are writing a GitHub Actions workflow YAML.
|
|
2156
|
+
|
|
2157
|
+
## Required structure
|
|
2158
|
+
1. **name**: workflow display name
|
|
2159
|
+
2. **on**: triggers (push to main, pull_request, manual workflow_dispatch)
|
|
2160
|
+
3. **jobs**: at least one job with steps
|
|
2161
|
+
4. **runs-on**: ubuntu-latest (default)
|
|
2162
|
+
5. **timeout-minutes**: 15 (default, increase for slower builds)
|
|
2163
|
+
6. **matrix.node-version**: pin to a specific major version (e.g. 20.x)
|
|
2164
|
+
|
|
2165
|
+
## Standard steps (in order)
|
|
2166
|
+
1. actions/checkout@v4
|
|
2167
|
+
2. actions/setup-node@v4 with cache: 'npm'
|
|
2168
|
+
3. npm ci (NOT npm install \u2014 faster in CI)
|
|
2169
|
+
4. typecheck (run the project's typecheck script)
|
|
2170
|
+
5. lint (run the project's lint script)
|
|
2171
|
+
6. test (run the project's test script)
|
|
2172
|
+
7. build (run the project's build script, if applicable)
|
|
2173
|
+
|
|
2174
|
+
## Output format (JSON-typed)
|
|
2175
|
+
- workflowFile: string (path like .github/workflows/ci.yml)
|
|
2176
|
+
- content: string (the full YAML)
|
|
2177
|
+
- jobs: string[] (job names)
|
|
2178
|
+
- triggers: string[] (trigger events)
|
|
2179
|
+
- nodeVersion: string (pinned Node version)
|
|
2180
|
+
|
|
2181
|
+
## CI principles
|
|
2182
|
+
- **Fail fast**: order steps from fastest \u2192 slowest (typecheck before build)
|
|
2183
|
+
- **Cache dependencies**: cache: 'npm' saves 30s+ on cold builds
|
|
2184
|
+
- **Pin versions**: use @v4 not @latest for reproducibility
|
|
2185
|
+
- **Don't cache build artifacts**: cache only deps
|
|
2186
|
+
- **Separate jobs for parallel**: typecheck, lint, test can be parallel jobs
|
|
2187
|
+
|
|
2188
|
+
Stay under 500 words.${CLARIFICATION_PROTOCOL3}`
|
|
2189
|
+
};
|
|
2190
|
+
registerCodingSkill(commitMessage);
|
|
2191
|
+
registerCodingSkill(prDescription);
|
|
2192
|
+
registerCodingSkill(ciPipeline);
|
|
2193
|
+
|
|
2194
|
+
// src/agents/skills/builtin/planning.ts
|
|
2195
|
+
var CLARIFICATION_PROTOCOL4 = `
|
|
2196
|
+
|
|
2197
|
+
WHEN TO ASK THE USER (clarification):
|
|
2198
|
+
If a single missing fact would materially change your output (target platform, scope, a binary design choice with significant trade-offs, a constraint you cannot safely assume), pause and ask the user by appending EXACTLY this block at the end of your message:
|
|
2199
|
+
|
|
2200
|
+
---QUESTION---
|
|
2201
|
+
{ "question": "One focused question", "choices": ["Option A", "Option B", "Option C"], "context": "Why this matters" }
|
|
2202
|
+
---END---
|
|
2203
|
+
|
|
2204
|
+
Rules for clarifications:
|
|
2205
|
+
- Ask AT MOST ONE question per turn, and only when genuinely blocked.
|
|
2206
|
+
- Prefer a small set of concrete "choices" (2-4). The user can still type a custom answer.
|
|
2207
|
+
- Do NOT ask for information that could be reasonably assumed or already in shared context.
|
|
2208
|
+
- If you can proceed with a sound documented assumption, DO SO instead of asking.`;
|
|
2209
|
+
var architectFeature = {
|
|
2210
|
+
id: "architect-feature",
|
|
2211
|
+
version: "1.0.0",
|
|
2212
|
+
name: "Architect Feature",
|
|
2213
|
+
description: "Multi-perspective feature design: explore 3-5 alternatives, pick the strongest, generate an ADR, list concrete risks and mitigations.",
|
|
2214
|
+
category: "planning",
|
|
2215
|
+
requiredRoles: ["sisyphus", "prometheus", "oracle"],
|
|
2216
|
+
requiredTools: ["createTask", "searchRAG"],
|
|
2217
|
+
estimatedCost: "high",
|
|
2218
|
+
enabledByDefault: true,
|
|
2219
|
+
builtin: true,
|
|
2220
|
+
triggers: [
|
|
2221
|
+
"New feature spanning multiple modules",
|
|
2222
|
+
"Architectural decision with long-term impact",
|
|
2223
|
+
"Public API design (libraries, services, CLI commands)",
|
|
2224
|
+
"Schema or data model changes affecting persistence",
|
|
2225
|
+
"Cross-cutting concern (auth, logging, caching) added to existing system"
|
|
2226
|
+
],
|
|
2227
|
+
antiPatterns: [
|
|
2228
|
+
"Simple bug fix \u2014 use debug-with-rag instead",
|
|
2229
|
+
"Single-file refactor under 500 LOC \u2014 use refactor-monolith only if > 1000 LOC",
|
|
2230
|
+
"Documentation update \u2014 use write-tsdoc or write-readme",
|
|
2231
|
+
"Adding a one-line config or env var \u2014 direct edit is faster",
|
|
2232
|
+
"Pure refactor with no design choice \u2014 use refactor-monolith or extract-reusable"
|
|
2233
|
+
],
|
|
2234
|
+
requires: ["architect-decision-record"],
|
|
2235
|
+
relatedSkills: ["scope-check", "migrate-stack", "code-review"],
|
|
2236
|
+
tags: ["planning", "architecture", "design", "multi-role", "adr"],
|
|
2237
|
+
examples: [
|
|
2238
|
+
{
|
|
2239
|
+
input: "Add OAuth login via Google to the Electron desktop app",
|
|
2240
|
+
output: {
|
|
2241
|
+
goal: "Enable users to sign in to AnathemaBrain with their Google account, replacing the current email+password flow.",
|
|
2242
|
+
alternatives: [
|
|
2243
|
+
{ approach: "OAuth library (Passport.js)", score: "F:4 R:3 M:5", rationale: "Battle-tested, large ecosystem, easy revocation, ~50KB bundle." },
|
|
2244
|
+
{ approach: "Custom PKCE flow with raw HTTP", score: "F:3 R:4 M:2", rationale: "No deps, but high surface area (token refresh, CSRF, error states)." },
|
|
2245
|
+
{ approach: "Use Electron built-in OAuth via deep link", score: "F:4 R:2 M:4", rationale: "Native to Electron, no redirect_uri issues, but desktop-specific." }
|
|
2246
|
+
],
|
|
2247
|
+
chosen: { approach: "Passport.js with Google strategy", rationale: "Lowest maintenance burden given the OAuth library ecosystem; revocation handled by the library." },
|
|
2248
|
+
adrPath: "docs/adr/0014-google-oauth.md",
|
|
2249
|
+
risks: [
|
|
2250
|
+
{ risk: "Token refresh edge cases when offline", mitigation: "Persist refresh token in keychain (already implemented for Grok OAuth)." },
|
|
2251
|
+
{ risk: "CSRF on deep-link redirect", mitigation: "Use OAuth state parameter with cryptographic random nonce." }
|
|
2252
|
+
]
|
|
2253
|
+
}
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
input: "Add a /skill command to the CLI that expands a skill template",
|
|
2257
|
+
output: {
|
|
2258
|
+
goal: "Allow the CLI to invoke a skill by name, expanding the template and dispatching to the council.",
|
|
2259
|
+
alternatives: [
|
|
2260
|
+
{ approach: "Slash command handler in cli/main.ts", score: "F:5 R:1 M:5", rationale: "Direct, simple." },
|
|
2261
|
+
{ approach: "Plugin system with skill loader", score: "F:2 R:4 M:2", rationale: "Over-engineered for v1." }
|
|
2262
|
+
],
|
|
2263
|
+
chosen: { approach: "Slash command handler", rationale: "YAGNI \u2014 defer plugin system to v2 when users request it." },
|
|
2264
|
+
adrPath: "docs/adr/0015-cli-skill-commands.md",
|
|
2265
|
+
risks: [
|
|
2266
|
+
{ risk: "Skill name collisions across categories", mitigation: "Namespace as `<category>:<name>` (e.g. `planning:architect-feature`)." }
|
|
2267
|
+
]
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
],
|
|
2271
|
+
outputSchema: "{ goal: string; alternatives: Array<{ approach: string; score: string; rationale: string }>; chosen: { approach: string; rationale: string }; adrPath: string; risks: Array<{ risk: string; mitigation: string }> }",
|
|
2272
|
+
systemPromptFragment: `You are designing a feature with multi-perspective analysis.
|
|
2273
|
+
|
|
2274
|
+
## Methodology
|
|
2275
|
+
1. Restate the goal in ONE sentence: what user need does this serve?
|
|
2276
|
+
2. Search the knowledge base for prior decisions: searchRAG(query="<feature-keyword>")
|
|
2277
|
+
3. Generate 3-5 DISTINCT approaches (vary the axis: library vs custom, sync vs async, desktop vs server, etc.).
|
|
2278
|
+
4. Score each approach on three dimensions (1-5 each):
|
|
2279
|
+
- Feasibility (F): how easy to implement given the current stack
|
|
2280
|
+
- Risk (R, INVERTED \u2014 lower is better): likelihood of bugs, security holes, scope creep
|
|
2281
|
+
- Maintenance (M): long-term cost (bus factor, doc quality, ecosystem health)
|
|
2282
|
+
5. Pick the top approach and justify in 1-2 sentences.
|
|
2283
|
+
6. Generate an ADR (link to architect-decision-record skill).
|
|
2284
|
+
7. List 2-5 concrete risks + mitigations.
|
|
2285
|
+
|
|
2286
|
+
## Output format (JSON-typed)
|
|
2287
|
+
- goal: string
|
|
2288
|
+
- alternatives: Array<{ approach: string; score: string; rationale: string }>
|
|
2289
|
+
- chosen: { approach: string; rationale: string }
|
|
2290
|
+
- adrPath: string
|
|
2291
|
+
- risks: Array<{ risk: string; mitigation: string }>
|
|
2292
|
+
|
|
2293
|
+
Stay under 400 words. Be decisive: pick ONE approach, do not list 3 equally good options.${CLARIFICATION_PROTOCOL4}`
|
|
2294
|
+
};
|
|
2295
|
+
var architectDecisionRecord = {
|
|
2296
|
+
id: "architect-decision-record",
|
|
2297
|
+
version: "1.0.0",
|
|
2298
|
+
name: "Architect Decision Record",
|
|
2299
|
+
description: "Generate an ADR (Architecture Decision Record) from a decision rationale. Format: Context, Decision, Consequences, Alternatives Considered.",
|
|
2300
|
+
category: "planning",
|
|
2301
|
+
requiredRoles: ["prometheus", "atlas"],
|
|
2302
|
+
requiredTools: ["createDocument"],
|
|
2303
|
+
estimatedCost: "low",
|
|
2304
|
+
enabledByDefault: true,
|
|
2305
|
+
builtin: true,
|
|
2306
|
+
triggers: [
|
|
2307
|
+
"A non-trivial architectural decision has been made",
|
|
2308
|
+
"Reviewing a past decision and want to capture WHY it was made",
|
|
2309
|
+
"Onboarding a new team member to the rationale behind existing choices",
|
|
2310
|
+
"After running architect-feature, to formalize the chosen approach"
|
|
2311
|
+
],
|
|
2312
|
+
antiPatterns: [
|
|
2313
|
+
"No real decision has been made yet \u2014 first run architect-feature or scope-check",
|
|
2314
|
+
"Trivial choice (file naming, color scheme) \u2014 use a code comment",
|
|
2315
|
+
"Decision is reversible in under 1 hour \u2014 defer the ADR"
|
|
2316
|
+
],
|
|
2317
|
+
requires: [],
|
|
2318
|
+
relatedSkills: ["architect-feature", "scope-check"],
|
|
2319
|
+
tags: ["planning", "documentation", "adr", "decision-log"],
|
|
2320
|
+
examples: [
|
|
2321
|
+
{
|
|
2322
|
+
input: "Document why we use SQLite over Postgres for local storage",
|
|
2323
|
+
output: {
|
|
2324
|
+
path: "docs/adr/0007-sqlite-over-postgres.md",
|
|
2325
|
+
content: `# 7. SQLite over Postgres for local storage
|
|
2326
|
+
|
|
2327
|
+
## Context
|
|
2328
|
+
AnathemaBrain is an Electron desktop app that runs entirely on the user's machine. We need persistent storage for vault documents, RAG vectors, settings, and council sessions.
|
|
2329
|
+
|
|
2330
|
+
## Decision
|
|
2331
|
+
Use SQLite (via better-sqlite3) for all local storage.
|
|
2332
|
+
|
|
2333
|
+
## Consequences
|
|
2334
|
+
- Zero-config: works out of the box, no separate DB server to manage
|
|
2335
|
+
- Single-file backups: copy ~/.anathemabrain/db.sqlite
|
|
2336
|
+
- FTS5 + JSON1 support built-in (used by RAG and vault search)
|
|
2337
|
+
- Cross-platform: tested on macOS, Linux, Windows
|
|
2338
|
+
|
|
2339
|
+
### Positive
|
|
2340
|
+
- No network round-trip \u2014 sub-millisecond reads
|
|
2341
|
+
- Embedded = no auth/connection-string management
|
|
2342
|
+
- Mature ecosystem (better-sqlite3, sqlite-vss)
|
|
2343
|
+
|
|
2344
|
+
### Negative
|
|
2345
|
+
- Single-writer: concurrent writes are serialized (acceptable for desktop)
|
|
2346
|
+
- No horizontal scaling (irrelevant for desktop)
|
|
2347
|
+
- Migration tooling requires custom scripts (we have db/migrations.ts)
|
|
2348
|
+
|
|
2349
|
+
## Alternatives Considered
|
|
2350
|
+
- **Postgres**: rejected \u2014 requires a server process, overkill for desktop
|
|
2351
|
+
- **LevelDB / RocksDB**: rejected \u2014 no SQL, harder to query vector data
|
|
2352
|
+
- **JSON files**: rejected \u2014 no concurrent access, no FTS
|
|
2353
|
+
`
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
],
|
|
2357
|
+
outputSchema: "{ path: string; content: string }",
|
|
2358
|
+
systemPromptFragment: `You are writing an Architecture Decision Record (ADR).
|
|
2359
|
+
|
|
2360
|
+
## Required sections
|
|
2361
|
+
1. **# N. <Title>** \u2014 short title in present tense ("Use X for Y")
|
|
2362
|
+
2. **## Context** \u2014 what is the situation? What forces are at play? (2-3 paragraphs)
|
|
2363
|
+
3. **## Decision** \u2014 what did we decide? State it clearly in 1-2 sentences.
|
|
2364
|
+
4. **## Consequences** \u2014 what becomes easier (+) and harder (-) as a result?
|
|
2365
|
+
5. **## Alternatives Considered** \u2014 2-4 alternatives with one-line rejection reasons.
|
|
2366
|
+
|
|
2367
|
+
## Numbering
|
|
2368
|
+
- Check docs/adr/ for the next available number (NNN-title.md format)
|
|
2369
|
+
- ADR numbers are permanent \u2014 never reuse, even if an ADR is superseded (mark superseded in the body instead)
|
|
2370
|
+
|
|
2371
|
+
## Tone
|
|
2372
|
+
- Factual, not promotional
|
|
2373
|
+
- Acknowledge trade-offs honestly
|
|
2374
|
+
- Link to relevant code/PRs/files using relative paths
|
|
2375
|
+
|
|
2376
|
+
Stay under 800 words.${CLARIFICATION_PROTOCOL4}`
|
|
2377
|
+
};
|
|
2378
|
+
var migrateStack = {
|
|
2379
|
+
id: "migrate-stack",
|
|
2380
|
+
version: "1.0.0",
|
|
2381
|
+
name: "Migrate Stack",
|
|
2382
|
+
description: "Plan a migration (framework, language, database, build tool) with phased rollout and a concrete rollback strategy. Identifies big-bang risks and proposes incremental steps.",
|
|
2383
|
+
category: "planning",
|
|
2384
|
+
requiredRoles: ["sisyphus", "prometheus", "hephaestus"],
|
|
2385
|
+
requiredTools: ["createTask", "searchRAG"],
|
|
2386
|
+
estimatedCost: "high",
|
|
2387
|
+
enabledByDefault: true,
|
|
2388
|
+
builtin: true,
|
|
2389
|
+
triggers: [
|
|
2390
|
+
"Replacing a framework (e.g. CRA \u2192 Vite, Express \u2192 Fastify)",
|
|
2391
|
+
"Upgrading a major version with breaking changes (React 17 \u2192 18)",
|
|
2392
|
+
"Switching databases (Postgres \u2192 SQLite, JSON files \u2192 Postgres)",
|
|
2393
|
+
"Migrating build tooling (webpack \u2192 esbuild, npm \u2192 pnpm)",
|
|
2394
|
+
"Replacing a key library with a competitor (moment \u2192 date-fns)"
|
|
2395
|
+
],
|
|
2396
|
+
antiPatterns: [
|
|
2397
|
+
"Greenfield project \u2014 use architect-feature instead",
|
|
2398
|
+
"A/B test of two stacks \u2014 use a spike/benchmark first",
|
|
2399
|
+
"Migration can be done by automated codemods \u2014 skip the planning skill"
|
|
2400
|
+
],
|
|
2401
|
+
requires: ["architect-decision-record", "scope-check"],
|
|
2402
|
+
relatedSkills: ["architect-feature", "refactor-monolith"],
|
|
2403
|
+
tags: ["planning", "migration", "rollout", "rollback", "risk-management"],
|
|
2404
|
+
examples: [
|
|
2405
|
+
{
|
|
2406
|
+
input: "Migrate AnathemaBrain from Create React App (CRA) to Vite",
|
|
2407
|
+
output: {
|
|
2408
|
+
goal: "Reduce dev server startup from 30s to <2s, reduce prod build from 60s to <10s.",
|
|
2409
|
+
phases: [
|
|
2410
|
+
{ name: "Phase 1: Setup parallel build", exitCriterion: "npm run dev:vite works for a hello-world", durationDays: 2 },
|
|
2411
|
+
{ name: "Phase 2: Migrate entry points", exitCriterion: "Both Webpack and Vite builds succeed; renderer loads in both", durationDays: 5 },
|
|
2412
|
+
{ name: "Phase 3: Migrate loaders (CSS, assets)", exitCriterion: "All static assets load correctly under Vite", durationDays: 3 },
|
|
2413
|
+
{ name: "Phase 4: Cut over", exitCriterion: "Webpack config deleted; CI runs only Vite", durationDays: 2 }
|
|
2414
|
+
],
|
|
2415
|
+
rollbackStrategy: "Keep Webpack config in git history (tag: pre-vite-migration). Revert the single commit that switches the default dev script.",
|
|
2416
|
+
risks: [
|
|
2417
|
+
{ risk: "Asset paths differ between Webpack and Vite (~/ aliases, ?url imports)", mitigation: "Phase 3 includes a path-mapping smoke test." },
|
|
2418
|
+
{ risk: "electron-vite specific electron features not yet stable", mitigation: "Phase 1 spike verifies electron-vite can compile the existing Electron main process." }
|
|
2419
|
+
]
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
],
|
|
2423
|
+
outputSchema: "{ goal: string; phases: Array<{ name: string; exitCriterion: string; durationDays: number }>; rollbackStrategy: string; risks: Array<{ risk: string; mitigation: string }> }",
|
|
2424
|
+
systemPromptFragment: `You are planning a stack migration.
|
|
2425
|
+
|
|
2426
|
+
## Methodology
|
|
2427
|
+
1. State the GOAL in measurable terms (e.g. "reduce build time from 60s to 10s").
|
|
2428
|
+
2. Decompose into 3-6 phases. Each phase has a CLEAR exit criterion (testable condition).
|
|
2429
|
+
3. Each phase should be DEPLOYABLE on its own (no big-bang steps).
|
|
2430
|
+
4. Plan a ROLLBACK strategy: which single commit reverts to the previous state?
|
|
2431
|
+
5. List 3-7 concrete risks + mitigations per phase.
|
|
2432
|
+
|
|
2433
|
+
## Migration principles
|
|
2434
|
+
- **Strangler Fig**: new system grows alongside old, traffic migrates incrementally
|
|
2435
|
+
- **No big-bang rewrites**: every phase must be independently shippable
|
|
2436
|
+
- **Reversibility first**: every phase has a tested rollback path
|
|
2437
|
+
- **Data migration last**: schema changes follow code changes (dual-write during transition)
|
|
2438
|
+
|
|
2439
|
+
## Output format (JSON-typed)
|
|
2440
|
+
- goal: string (measurable)
|
|
2441
|
+
- phases: Array<{ name: string; exitCriterion: string; durationDays: number }>
|
|
2442
|
+
- rollbackStrategy: string (the single command or commit that reverts)
|
|
2443
|
+
- risks: Array<{ risk: string; mitigation: string }>
|
|
2444
|
+
|
|
2445
|
+
Stay under 500 words.${CLARIFICATION_PROTOCOL4}`
|
|
2446
|
+
};
|
|
2447
|
+
var scopeCheck = {
|
|
2448
|
+
id: "scope-check",
|
|
2449
|
+
version: "1.0.0",
|
|
2450
|
+
name: "Scope Check",
|
|
2451
|
+
description: "Validate feasibility of a proposed feature or change. Identify hidden costs, dependencies, risks. Propose a reduced scope that fits the available budget.",
|
|
2452
|
+
category: "planning",
|
|
2453
|
+
requiredRoles: ["sisyphus", "oracle", "chairman"],
|
|
2454
|
+
requiredTools: ["searchRAG"],
|
|
2455
|
+
estimatedCost: "medium",
|
|
2456
|
+
enabledByDefault: true,
|
|
2457
|
+
builtin: true,
|
|
2458
|
+
triggers: [
|
|
2459
|
+
"User proposes an ambitious feature without a clear MVP boundary",
|
|
2460
|
+
"Estimates are unclear or contested",
|
|
2461
|
+
"Hidden dependencies suspected (auth, billing, third-party APIs)",
|
|
2462
|
+
"Before committing to a multi-week project",
|
|
2463
|
+
'When a stakeholder says "this should be easy" and you suspect it is not'
|
|
2464
|
+
],
|
|
2465
|
+
antiPatterns: [
|
|
2466
|
+
"The task is already small and well-scoped \u2014 just do it",
|
|
2467
|
+
"Pure research / exploration \u2014 no commitment to implement",
|
|
2468
|
+
"Bug fix \u2014 use debug-with-rag instead"
|
|
2469
|
+
],
|
|
2470
|
+
requires: [],
|
|
2471
|
+
relatedSkills: ["architect-feature", "migrate-stack"],
|
|
2472
|
+
tags: ["planning", "feasibility", "risk-assessment", "mvp", "scoping"],
|
|
2473
|
+
examples: [
|
|
2474
|
+
{
|
|
2475
|
+
input: 'Build a "smart inbox" that categorizes emails using the local LLM council',
|
|
2476
|
+
output: {
|
|
2477
|
+
feasibility: "partial",
|
|
2478
|
+
estimatedEffortDays: 14,
|
|
2479
|
+
hiddenCosts: [
|
|
2480
|
+
{ cost: "IMAP polling + auth setup (3-5 days)", mitigation: "Use existing Gmail API OAuth2 flow" },
|
|
2481
|
+
{ cost: "Email parsing (HTML, attachments) - 2 days", mitigation: "Use mailparser library" },
|
|
2482
|
+
{ cost: "Local LLM inference at scale - 1 day setup + per-email ~3s", mitigation: "Batch categorization, use smaller model (gemini-2.5-flash)" },
|
|
2483
|
+
{ cost: "Council orchestration - already have it", mitigation: "Reuse runCouncilPure" }
|
|
2484
|
+
],
|
|
2485
|
+
reducedScope: {
|
|
2486
|
+
phase1: "Single Gmail account, read-only, top-20 inbox categorization (3 days)",
|
|
2487
|
+
phase2: "Multi-account + auto-archive (5 days)",
|
|
2488
|
+
phase3: "Reply drafting suggestions (6 days)"
|
|
2489
|
+
},
|
|
2490
|
+
risks: [
|
|
2491
|
+
"Council API costs: 5 emails \xD7 3 council members \xD7 4 providers = 60 LLM calls per batch",
|
|
2492
|
+
"User privacy: emails sent to cloud providers unless restricted to local-only model"
|
|
2493
|
+
]
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
],
|
|
2497
|
+
outputSchema: '{ feasibility: "yes" | "partial" | "no"; estimatedEffortDays: number; hiddenCosts: Array<{ cost: string; mitigation: string }>; reducedScope: { phase1: string; phase2: string; phase3: string }; risks: string[] }',
|
|
2498
|
+
systemPromptFragment: `You are validating the scope of a proposed feature or change.
|
|
2499
|
+
|
|
2500
|
+
## Methodology
|
|
2501
|
+
1. Restate the proposal in ONE sentence.
|
|
2502
|
+
2. Search the knowledge base: searchRAG(query="<proposed-keyword>")
|
|
2503
|
+
3. Estimate effort realistically (in person-days, with calibration from prior projects).
|
|
2504
|
+
4. List HIDDEN costs: auth flows, data migration, third-party APIs, deployment, monitoring.
|
|
2505
|
+
5. Propose a REDUCED SCOPE that delivers 80% of the value in 20% of the time.
|
|
2506
|
+
6. Flag risks that could block the project.
|
|
2507
|
+
|
|
2508
|
+
## Output format (JSON-typed)
|
|
2509
|
+
- feasibility: 'yes' | 'partial' | 'no'
|
|
2510
|
+
- estimatedEffortDays: number
|
|
2511
|
+
- hiddenCosts: Array<{ cost: string; mitigation: string }>
|
|
2512
|
+
- reducedScope: { phase1: string; phase2: string; phase3: string } (3 phases, each independently shippable)
|
|
2513
|
+
- risks: string[] (3-5 risks)
|
|
2514
|
+
|
|
2515
|
+
## When to be skeptical
|
|
2516
|
+
- "Just add X" usually hides auth + persistence + UI work (3x multiplier)
|
|
2517
|
+
- "Use the existing system" usually means new integration code
|
|
2518
|
+
- Estimates from optimistic people \u2014 multiply by 2-3x for calendar time
|
|
2519
|
+
|
|
2520
|
+
Stay under 400 words. Be honest about feasibility \u2014 say NO when the answer is NO.${CLARIFICATION_PROTOCOL4}`
|
|
2521
|
+
};
|
|
2522
|
+
registerCodingSkill(architectDecisionRecord);
|
|
2523
|
+
registerCodingSkill(scopeCheck);
|
|
2524
|
+
registerCodingSkill(architectFeature);
|
|
2525
|
+
registerCodingSkill(migrateStack);
|
|
2526
|
+
|
|
2527
|
+
// src/agents/skills/builtin/refactoring.ts
|
|
2528
|
+
var CLARIFICATION_PROTOCOL5 = `
|
|
2529
|
+
|
|
2530
|
+
WHEN TO ASK THE USER (clarification):
|
|
2531
|
+
If a single missing fact would materially change your output (target platform, scope, a binary design choice with significant trade-offs, a constraint you cannot safely assume), pause and ask the user by appending EXACTLY this block at the end of your message:
|
|
2532
|
+
|
|
2533
|
+
---QUESTION---
|
|
2534
|
+
{ "question": "One focused question", "choices": ["Option A", "Option B", "Option C"], "context": "Why this matters" }
|
|
2535
|
+
---END---
|
|
2536
|
+
|
|
2537
|
+
Rules for clarifications:
|
|
2538
|
+
- Ask AT MOST ONE question per turn, and only when genuinely blocked.
|
|
2539
|
+
- Prefer a small set of concrete "choices" (2-4). The user can still type a custom answer.
|
|
2540
|
+
- Do NOT ask for information that could be reasonably assumed or already in shared context.
|
|
2541
|
+
- If you can proceed with a sound documented assumption, DO SO instead of asking.`;
|
|
2542
|
+
var extractReusable = {
|
|
2543
|
+
id: "extract-reusable",
|
|
2544
|
+
version: "1.0.0",
|
|
2545
|
+
name: "Extract Reusable Module",
|
|
2546
|
+
description: "Find duplication across files via semantic search. Identify shared logic, propose a new reusable module with a clean API, plan the migration of all call sites.",
|
|
2547
|
+
category: "refactor",
|
|
2548
|
+
requiredRoles: ["atlas", "oracle"],
|
|
2549
|
+
requiredTools: ["searchRAG"],
|
|
2550
|
+
estimatedCost: "medium",
|
|
2551
|
+
enabledByDefault: true,
|
|
2552
|
+
builtin: true,
|
|
2553
|
+
triggers: [
|
|
2554
|
+
"Same logic copy-pasted across 3+ files",
|
|
2555
|
+
"Two near-identical utility functions (e.g. formatDate in 2 different files)",
|
|
2556
|
+
"A bug fix needs to be applied in 5 places",
|
|
2557
|
+
"New project copies code from old project (opportunity to extract)"
|
|
2558
|
+
],
|
|
2559
|
+
antiPatterns: [
|
|
2560
|
+
"Duplication is INTENTIONAL (different domains, different evolution paths)",
|
|
2561
|
+
"Shared logic is so trivial (< 5 lines) that a function call adds more noise than it removes",
|
|
2562
|
+
"API is unstable \u2014 extracting now would force awkward premature abstractions"
|
|
2563
|
+
],
|
|
2564
|
+
requires: [],
|
|
2565
|
+
relatedSkills: ["refactor-monolith", "simplify-conditionals"],
|
|
2566
|
+
tags: ["refactor", "deduplication", "reuse", "shared-module"],
|
|
2567
|
+
examples: [
|
|
2568
|
+
{
|
|
2569
|
+
input: 'There are 3 different "formatDuration" implementations in src/ \u2014 slightly different in each',
|
|
2570
|
+
output: {
|
|
2571
|
+
duplication: {
|
|
2572
|
+
callSites: [
|
|
2573
|
+
{ file: "src/components/Timer.tsx", function: "formatDuration(ms: number): string", line: 23 },
|
|
2574
|
+
{ file: "src/lib/utils.ts", function: "formatDuration(ms: number): string", line: 47 },
|
|
2575
|
+
{ file: "src/pages/Settings.tsx", function: "formatDurationText(ms: number): string", line: 156 }
|
|
2576
|
+
],
|
|
2577
|
+
differences: [
|
|
2578
|
+
'Timer.tsx: shows "1h 23m" for >1h, "23m 45s" for >1m, else "45s"',
|
|
2579
|
+
'utils.ts: shows "1:23:45" always (HH:MM:SS)',
|
|
2580
|
+
'Settings.tsx: shows "23m" for >1m, "45s" else'
|
|
2581
|
+
]
|
|
2582
|
+
},
|
|
2583
|
+
proposedModule: {
|
|
2584
|
+
path: "src/lib/format/duration.ts",
|
|
2585
|
+
api: `export function formatDuration(ms: number, opts?: { style?: 'compact' | 'full' | 'minimal' }): string`,
|
|
2586
|
+
variants: [
|
|
2587
|
+
{ style: "compact", example: "1h 23m" },
|
|
2588
|
+
{ style: "full", example: "1:23:45" },
|
|
2589
|
+
{ style: "minimal", example: "23m" }
|
|
2590
|
+
]
|
|
2591
|
+
},
|
|
2592
|
+
migration: [
|
|
2593
|
+
{ step: "Create src/lib/format/duration.ts with the 3-style API", linesToAdd: 30 },
|
|
2594
|
+
{ step: 'Update Timer.tsx to call formatDuration(ms, {style:"compact"})', linesToChange: 2 },
|
|
2595
|
+
{ step: 'Update utils.ts to call formatDuration(ms, {style:"full"})', linesToChange: 2 },
|
|
2596
|
+
{ step: 'Update Settings.tsx to call formatDuration(ms, {style:"minimal"})', linesToChange: 2 }
|
|
2597
|
+
],
|
|
2598
|
+
benefits: ["Single source of truth for duration formatting", "Easy to add new styles (e.g. ISO 8601)", "Tests in one place"]
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
],
|
|
2602
|
+
outputSchema: "{ duplication: { callSites: Array<{ file: string; function: string; line: number }>; differences: string[] }; proposedModule: { path: string; api: string; variants: Array<{ style: string; example: string }> }; migration: Array<{ step: string; linesToChange: number }>; benefits: string[] }",
|
|
2603
|
+
systemPromptFragment: `You are finding duplicated logic and extracting a reusable module.
|
|
2604
|
+
|
|
2605
|
+
## Methodology
|
|
2606
|
+
1. Use grep/semantic search to find candidate duplication (3+ similar implementations).
|
|
2607
|
+
2. Read each call site to understand the VARIATIONS (not just the common pattern).
|
|
2608
|
+
3. Design an API that accommodates ALL variations (use options pattern, not multiple functions).
|
|
2609
|
+
4. Specify the new module path + function signature + behavior table.
|
|
2610
|
+
5. List migration steps: which file + which lines change to use the new module.
|
|
2611
|
+
|
|
2612
|
+
## Extraction principles
|
|
2613
|
+
- **Variations via options, not forks**: prefer formatDuration(ms, opts) over formatDurationCompact()/Full()
|
|
2614
|
+
- **Pure functions**: extracted modules should have no hidden state, no I/O
|
|
2615
|
+
- **Tests first**: write the test suite for the new module BEFORE migrating call sites
|
|
2616
|
+
- **Single responsibility**: extracted module does ONE thing well
|
|
2617
|
+
|
|
2618
|
+
## Output format (JSON-typed)
|
|
2619
|
+
- duplication: { callSites: [{file, function, line}], differences[] }
|
|
2620
|
+
- proposedModule: { path, api (TS signature), variants: [{style, example}] }
|
|
2621
|
+
- migration: [{step, linesToChange}]
|
|
2622
|
+
- benefits: string[] (3-5 bullet points)
|
|
2623
|
+
|
|
2624
|
+
Stay under 500 words.${CLARIFICATION_PROTOCOL5}`
|
|
2625
|
+
};
|
|
2626
|
+
var simplifyConditionals = {
|
|
2627
|
+
id: "simplify-conditionals",
|
|
2628
|
+
version: "1.0.0",
|
|
2629
|
+
name: "Simplify Conditionals",
|
|
2630
|
+
description: "Reduce cyclomatic complexity of boolean/conditional code. Extract guard clauses, replace nested if-else with lookup tables, simplify boolean expressions using De Morgan's laws.",
|
|
2631
|
+
category: "refactor",
|
|
2632
|
+
requiredRoles: ["atlas"],
|
|
2633
|
+
requiredTools: [],
|
|
2634
|
+
estimatedCost: "low",
|
|
2635
|
+
enabledByDefault: true,
|
|
2636
|
+
builtin: true,
|
|
2637
|
+
triggers: [
|
|
2638
|
+
"Cyclomatic complexity > 10 in a single function",
|
|
2639
|
+
"Nested if-else > 3 levels deep",
|
|
2640
|
+
"Boolean expressions with > 4 terms",
|
|
2641
|
+
"Multiple early returns that could be guard clauses",
|
|
2642
|
+
"Switch statements with > 8 cases that share structure"
|
|
2643
|
+
],
|
|
2644
|
+
antiPatterns: [
|
|
2645
|
+
"Performance-critical branches (compiler will optimize nested ifs better than lookups)",
|
|
2646
|
+
"Complex domain logic that IS the conditional structure (e.g. tax brackets)",
|
|
2647
|
+
"Code under active development where the structure is still in flux"
|
|
2648
|
+
],
|
|
2649
|
+
requires: [],
|
|
2650
|
+
relatedSkills: ["extract-reusable", "refactor-monolith"],
|
|
2651
|
+
tags: ["refactor", "simplification", "cyclomatic-complexity", "clean-code"],
|
|
2652
|
+
examples: [
|
|
2653
|
+
{
|
|
2654
|
+
input: "function canUserEdit(user, doc) { if (user.isAdmin) { return true; } else { if (doc.ownerId === user.id) { return true; } else { if (doc.collaborators.includes(user.id) && !doc.locked) { return true; } else { return false; } } } }",
|
|
2655
|
+
output: {
|
|
2656
|
+
before: { lines: 9, cyclomaticComplexity: 5 },
|
|
2657
|
+
after: { lines: 6, cyclomaticComplexity: 1 },
|
|
2658
|
+
refactored: `function canUserEdit(user, doc) {
|
|
2659
|
+
if (user.isAdmin) return true;
|
|
2660
|
+
if (doc.ownerId === user.id) return true;
|
|
2661
|
+
if (doc.collaborators.includes(user.id) && !doc.locked) return true;
|
|
2662
|
+
return false;
|
|
2663
|
+
}`,
|
|
2664
|
+
explanation: "Extracted guard clauses (early returns). Eliminated 3 levels of nesting. Cyclomatic complexity dropped from 5 to 1. Logic preserved exactly."
|
|
2665
|
+
}
|
|
2666
|
+
},
|
|
2667
|
+
{
|
|
2668
|
+
input: 'function getDiscount(tier) { if (tier === "bronze") return 0.05; else if (tier === "silver") return 0.10; else if (tier === "gold") return 0.15; else if (tier === "platinum") return 0.20; else return 0; }',
|
|
2669
|
+
output: {
|
|
2670
|
+
before: { lines: 7, cyclomaticComplexity: 5 },
|
|
2671
|
+
after: { lines: 7, cyclomaticComplexity: 1 },
|
|
2672
|
+
refactored: `const DISCOUNT_BY_TIER = { bronze: 0.05, silver: 0.10, gold: 0.15, platinum: 0.20 } as const;
|
|
2673
|
+
function getDiscount(tier: DiscountTier): number {
|
|
2674
|
+
return DISCOUNT_BY_TIER[tier] ?? 0;
|
|
2675
|
+
}`,
|
|
2676
|
+
explanation: "Replaced if-else chain with const lookup table. Adding a new tier = one line in the table, no function edit. Type-safe via `as const`. Default 0 via nullish coalescing."
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
],
|
|
2680
|
+
outputSchema: "{ before: { lines: number; cyclomaticComplexity: number }; after: { lines: number; cyclomaticComplexity: number }; refactored: string; explanation: string }",
|
|
2681
|
+
systemPromptFragment: `You are reducing the complexity of conditional code.
|
|
2682
|
+
|
|
2683
|
+
## Common patterns to apply
|
|
2684
|
+
1. **Guard clauses**: replace \`if (cond) { ...big block... } else { return false; }\` with \`if (!cond) return false; ...big block...\`
|
|
2685
|
+
2. **Lookup tables**: replace \`if/else if/else if/...\` chains with \`const TABLE = { ... } as const; return TABLE[key] ?? default\`
|
|
2686
|
+
3. **Boolean simplification**: apply De Morgan's laws (\`!(A && B)\` \u2192 \`!A || !B\`), extract complex predicates into named booleans
|
|
2687
|
+
4. **Polymorphism**: replace type-checking conditionals (\`if (type === 'A') ... else if (type === 'B') ...\`) with method dispatch
|
|
2688
|
+
|
|
2689
|
+
## Output format (JSON-typed)
|
|
2690
|
+
- before: { lines, cyclomaticComplexity }
|
|
2691
|
+
- after: { lines, cyclomaticComplexity }
|
|
2692
|
+
- refactored: string (the new code)
|
|
2693
|
+
- explanation: string (1-2 sentences on what changed and why)
|
|
2694
|
+
|
|
2695
|
+
## What NOT to do
|
|
2696
|
+
- Don't change behavior \u2014 only structure
|
|
2697
|
+
- Don't introduce new abstractions for one-off conditionals
|
|
2698
|
+
- Don't sacrifice readability for fewer lines (e.g. overly clever ternary chains)
|
|
2699
|
+
|
|
2700
|
+
Stay under 400 words.${CLARIFICATION_PROTOCOL5}`
|
|
2701
|
+
};
|
|
2702
|
+
var refactorMonolith = {
|
|
2703
|
+
id: "refactor-monolith",
|
|
2704
|
+
version: "1.0.0",
|
|
2705
|
+
name: "Refactor Monolith",
|
|
2706
|
+
description: "Multi-perspective decomposition of large files (>1000 LOC) into focused modules. Identifies natural responsibility boundaries, proposes a module map with line numbers, validates no circular deps, generates a per-phase migration plan.",
|
|
2707
|
+
category: "refactor",
|
|
2708
|
+
requiredRoles: ["hephaestus", "atlas", "oracle"],
|
|
2709
|
+
requiredTools: ["searchRAG", "createTask"],
|
|
2710
|
+
estimatedCost: "high",
|
|
2711
|
+
enabledByDefault: true,
|
|
2712
|
+
builtin: true,
|
|
2713
|
+
triggers: [
|
|
2714
|
+
"Single file > 1000 LOC with mixed responsibilities",
|
|
2715
|
+
"Cyclomatic complexity > 50 in a single function",
|
|
2716
|
+
"Multiple unrelated imports (>15 distinct packages) in one file",
|
|
2717
|
+
"Onboarding new devs is hard because no one file fits in their head",
|
|
2718
|
+
"Build/test times are slow because one big file recompiles on every change"
|
|
2719
|
+
],
|
|
2720
|
+
antiPatterns: [
|
|
2721
|
+
"File under 500 LOC \u2014 defer; not worth the migration cost",
|
|
2722
|
+
"Truly cohesive file even if large (e.g. a state machine FSM)",
|
|
2723
|
+
"Quick fix needed urgently \u2014 refactor first, fix second",
|
|
2724
|
+
"No tests exist \u2014 write characterization tests first, refactor second"
|
|
2725
|
+
],
|
|
2726
|
+
requires: ["extract-reusable"],
|
|
2727
|
+
relatedSkills: ["simplify-conditionals", "architect-feature"],
|
|
2728
|
+
tags: ["refactor", "modularization", "god-module", "split", "architecture"],
|
|
2729
|
+
examples: [
|
|
2730
|
+
{
|
|
2731
|
+
input: "src/Council.tsx is 1822 LOC with mixed concerns: chat rendering, tool dispatch, voice input, slash commands, file attachments",
|
|
2732
|
+
output: {
|
|
2733
|
+
currentState: {
|
|
2734
|
+
file: "src/Council.tsx",
|
|
2735
|
+
totalLines: 1822,
|
|
2736
|
+
responsibilities: ["chat render", "tool dispatch", "voice input", "slash commands", "file attachments", "settings panel", "token counter"]
|
|
2737
|
+
},
|
|
2738
|
+
proposedModules: [
|
|
2739
|
+
{ name: "ChatStream.tsx", responsibility: "render assistant/user messages", estimatedLines: 350, dependsOn: ["useChatStream hook"] },
|
|
2740
|
+
{ name: "ToolDispatch.tsx", responsibility: "show tool execution + results", estimatedLines: 280, dependsOn: ["brain/tool types"] },
|
|
2741
|
+
{ name: "VoiceInput.tsx", responsibility: "microphone + transcription UI", estimatedLines: 150, dependsOn: ["useVoiceRecorder hook"] },
|
|
2742
|
+
{ name: "SlashCommands.tsx", responsibility: "command palette + autocomplete", estimatedLines: 200, dependsOn: ["useSlashCommands hook"] },
|
|
2743
|
+
{ name: "useCouncil.ts (hook)", responsibility: "state + council orchestration logic", estimatedLines: 400, dependsOn: [] }
|
|
2744
|
+
],
|
|
2745
|
+
migrationPhases: [
|
|
2746
|
+
{ phase: 1, name: "Extract useCouncil hook", exitCriterion: "Council.tsx < 1500 LOC, all behavior unchanged, all tests pass", durationDays: 2 },
|
|
2747
|
+
{ phase: 2, name: "Extract ChatStream + ToolDispatch", exitCriterion: "Council.tsx < 1000 LOC", durationDays: 3 },
|
|
2748
|
+
{ phase: 3, name: "Extract VoiceInput + SlashCommands", exitCriterion: "Council.tsx < 500 LOC OR deleted entirely", durationDays: 2 }
|
|
2749
|
+
],
|
|
2750
|
+
risks: [
|
|
2751
|
+
{ risk: "Hidden circular imports between new modules", mitigation: "Run `madge --circular src/` after each phase; CI fails on circular deps." },
|
|
2752
|
+
{ risk: "Lost re-render optimization (inline JSX vs memoized components)", mitigation: "Phase 1 establishes React.memo + useMemo conventions before extracting." }
|
|
2753
|
+
]
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
],
|
|
2757
|
+
outputSchema: "{ currentState: { file: string; totalLines: number; responsibilities: string[] }; proposedModules: Array<{ name: string; responsibility: string; estimatedLines: number; dependsOn: string[] }>; migrationPhases: Array<{ phase: number; name: string; exitCriterion: string; durationDays: number }>; risks: Array<{ risk: string; mitigation: string }> }",
|
|
2758
|
+
systemPromptFragment: `You are planning a multi-perspective decomposition of a large file.
|
|
2759
|
+
|
|
2760
|
+
## Methodology
|
|
2761
|
+
1. Identify the file's CURRENT responsibilities (read the source if needed).
|
|
2762
|
+
2. Search the KB for prior split decisions: searchRAG(query="<file-name-keyword>")
|
|
2763
|
+
3. Propose 4-8 NEW modules, each with ONE clear responsibility.
|
|
2764
|
+
4. Map existing functions/sections to new modules (with line numbers).
|
|
2765
|
+
5. Validate NO circular dependencies between proposed modules.
|
|
2766
|
+
6. Plan 3-5 migration phases, each independently shippable with all tests passing.
|
|
2767
|
+
|
|
2768
|
+
## Module split principles
|
|
2769
|
+
- **One job per module**: if you can't describe a module's purpose in ONE sentence, split further
|
|
2770
|
+
- **No circular deps**: A imports B, B imports C is fine; A imports B, B imports A is not
|
|
2771
|
+
- **Stable abstractions**: the new module boundaries should match NATURAL responsibility seams (state vs UI vs I/O), not arbitrary line counts
|
|
2772
|
+
- **Testability first**: each module should be unit-testable in isolation
|
|
2773
|
+
|
|
2774
|
+
## Output format (JSON-typed)
|
|
2775
|
+
- currentState: { file, totalLines, responsibilities[] }
|
|
2776
|
+
- proposedModules: Array<{ name, responsibility, estimatedLines, dependsOn[] }>
|
|
2777
|
+
- migrationPhases: Array<{ phase, name, exitCriterion, durationDays }>
|
|
2778
|
+
- risks: Array<{ risk, mitigation }>
|
|
2779
|
+
|
|
2780
|
+
Stay under 600 words. Be decisive about module boundaries \u2014 pick ONE natural split, don't present 3 equally valid options.${CLARIFICATION_PROTOCOL5}`
|
|
2781
|
+
};
|
|
2782
|
+
registerCodingSkill(extractReusable);
|
|
2783
|
+
registerCodingSkill(simplifyConditionals);
|
|
2784
|
+
registerCodingSkill(refactorMonolith);
|
|
2785
|
+
|
|
2786
|
+
// src/agents/skills/builtin/review.ts
|
|
2787
|
+
var CLARIFICATION_PROTOCOL6 = `
|
|
2788
|
+
|
|
2789
|
+
WHEN TO ASK THE USER (clarification):
|
|
2790
|
+
If a single missing fact would materially change your output (target platform, scope, a binary design choice with significant trade-offs, a constraint you cannot safely assume), pause and ask the user by appending EXACTLY this block at the end of your message:
|
|
2791
|
+
|
|
2792
|
+
---QUESTION---
|
|
2793
|
+
{ "question": "One focused question", "choices": ["Option A", "Option B", "Option C"], "context": "Why this matters" }
|
|
2794
|
+
---END---
|
|
2795
|
+
|
|
2796
|
+
Rules for clarifications:
|
|
2797
|
+
- Ask AT MOST ONE question per turn, and only when genuinely blocked.
|
|
2798
|
+
- Prefer a small set of concrete "choices" (2-4). The user can still type a wave-in response.
|
|
2799
|
+
- Do NOT ask for information that could be reasonably assumed or already in shared context.
|
|
2800
|
+
- If you can proceed with a sound documented assumption, DO SO instead of asking.`;
|
|
2801
|
+
var securityAudit = {
|
|
2802
|
+
id: "security-audit",
|
|
2803
|
+
version: "1.0.0",
|
|
2804
|
+
name: "Security Audit",
|
|
2805
|
+
description: "OWASP Top 10 + CVE check on dependencies + secrets scan. Identifies injection vectors, auth flaws, unsafe deserialization, hardcoded credentials.",
|
|
2806
|
+
category: "review",
|
|
2807
|
+
requiredRoles: ["oracle"],
|
|
2808
|
+
requiredTools: ["grep_content", "read_file"],
|
|
2809
|
+
estimatedCost: "medium",
|
|
2810
|
+
enabledByDefault: true,
|
|
2811
|
+
builtin: true,
|
|
2812
|
+
triggers: [
|
|
2813
|
+
"Any code touching auth, session, password, token, or credential storage",
|
|
2814
|
+
"Pre-release audit of the codebase",
|
|
2815
|
+
"After a security incident or CVE disclosure",
|
|
2816
|
+
"Quarterly compliance check",
|
|
2817
|
+
"Integrating a new third-party npm dependency that handles user data"
|
|
2818
|
+
],
|
|
2819
|
+
antiPatterns: [
|
|
2820
|
+
"Pure UI code with no I/O \u2014 defer to code-review skill",
|
|
2821
|
+
"A unit test file \u2014 no production risk",
|
|
2822
|
+
"Mock/seed data \u2014 those credentials are intentionally fake"
|
|
2823
|
+
],
|
|
2824
|
+
requires: [],
|
|
2825
|
+
relatedSkills: ["code-review"],
|
|
2826
|
+
tags: ["security", "owasp", "audit", "cve", "secrets", "compliance"],
|
|
2827
|
+
examples: [
|
|
2828
|
+
{
|
|
2829
|
+
input: "Audit src/auth/ directory for security issues",
|
|
2830
|
+
output: {
|
|
2831
|
+
owaspFindings: [
|
|
2832
|
+
{ category: "A01:2021 Broken Access Control", severity: "HIGH", file: "src/auth/middleware.ts:34", issue: "Missing role check on /admin/* routes \u2014 any authenticated user can access admin endpoints" },
|
|
2833
|
+
{ category: "A02:2021 Cryptographic Failures", severity: "CRITICAL", file: "src/auth/token.ts:12", issue: "Tokens stored with SHA-256(password) instead of bcrypt/argon2 \u2014 vulnerable to rainbow tables" },
|
|
2834
|
+
{ category: "A03:2021 Injection", severity: "MEDIUM", file: "src/auth/login.ts:45", issue: "Email field not validated; potential XSS if rendered unsafed downstream" }
|
|
2835
|
+
],
|
|
2836
|
+
cveCheck: "npm audit: 3 vulnerabilities (1 high, 2 moderate) in jsonwebtoken@8.5.1 (CVE-2022-23529) \u2014 upgrade to 9.0.0+",
|
|
2837
|
+
secretsScan: "No hardcoded API keys found in src/. .env files are gitignored.",
|
|
2838
|
+
priorityOrder: [
|
|
2839
|
+
"1. Fix SHA-256 \u2192 bcrypt (CRITICAL)",
|
|
2840
|
+
"2. Add role check middleware (HIGH)",
|
|
2841
|
+
"3. Upgrade jsonwebtoken (CVE)",
|
|
2842
|
+
"4. Add email validation (MEDIUM)"
|
|
2843
|
+
]
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
],
|
|
2847
|
+
outputSchema: "{ owaspFindings: Array<{ category: string; severity: string; file: string; line: number; issue: string }>; cveCheck: string; secretsScan: string; priorityOrder: string[] }",
|
|
2848
|
+
systemPromptFragment: `You are auditing the codebase for security vulnerabilities.
|
|
2849
|
+
|
|
2850
|
+
## OWASP Top 10 (2021) \u2014 check each
|
|
2851
|
+
1. **A01 Broken Access Control**: missing role checks, IDOR vulnerabilities
|
|
2852
|
+
2. **A02 Cryptographic Failures**: weak hashing (MD5/SHA-1/SHA-256 for passwords), missing TLS
|
|
2853
|
+
3. **A03 Injection**: SQL injection, command injection, XSS (especially via unsafe dangerouslySetInnerHTML)
|
|
2854
|
+
4. **A04 Insecure Design**: missing rate limiting on auth endpoints
|
|
2855
|
+
5. **A05 Security Misconfiguration**: default credentials, exposed debug endpoints
|
|
2856
|
+
6. **A06 Vulnerable Components**: outdated dependencies with known CVEs (run npm audit)
|
|
2857
|
+
7. **A07 Identification & Auth Failures**: missing MFA, weak password policies
|
|
2858
|
+
8. **A08 Software & Data Integrity Failures**: missing signature verification on updates
|
|
2859
|
+
9. **A09 Logging & Monitoring Failures**: no audit log on sensitive operations
|
|
2860
|
+
10. **A10 SSRF**: user-controlled URLs fetched server-side without allowlist
|
|
2861
|
+
|
|
2862
|
+
## CVE check
|
|
2863
|
+
- Run npm audit (or pip-audit, cargo audit, etc. depending on the stack)
|
|
2864
|
+
- Cross-reference with the GitHub Advisory Database
|
|
2865
|
+
|
|
2866
|
+
## Secrets scan
|
|
2867
|
+
- grep for common patterns: API_KEY, SECRET, TOKEN, PASSWORD, BEGIN PRIVATE KEY
|
|
2868
|
+
- Check .env files are gitignored
|
|
2869
|
+
|
|
2870
|
+
## Output format (JSON-typed)
|
|
2871
|
+
- owaspFindings: Array<{ category, severity, file, line, issue }>
|
|
2872
|
+
- cveCheck: string (results of dependency scan)
|
|
2873
|
+
- secretsScan: string (results of hardcoded secrets scan)
|
|
2874
|
+
- priorityOrder: string[] (numbered list, CRITICAL first)
|
|
2875
|
+
|
|
2876
|
+
Stay under 500 words.${CLARIFICATION_PROTOCOL6}`
|
|
2877
|
+
};
|
|
2878
|
+
var performanceReview = {
|
|
2879
|
+
id: "performance-review",
|
|
2880
|
+
version: "1.0.0",
|
|
2881
|
+
name: "Performance Review",
|
|
2882
|
+
description: "Identify algorithmic complexity issues (O(n\xB2), O(n\xB3)), N+1 queries, unnecessary allocations, missing caching opportunities, I/O blocking patterns.",
|
|
2883
|
+
category: "review",
|
|
2884
|
+
requiredRoles: ["atlas"],
|
|
2885
|
+
requiredTools: ["read_file", "grep_content"],
|
|
2886
|
+
estimatedCost: "medium",
|
|
2887
|
+
enabledByDefault: true,
|
|
2888
|
+
builtin: true,
|
|
2889
|
+
triggers: [
|
|
2890
|
+
"Performance regression report from users",
|
|
2891
|
+
"Response time > 1s where it used to be < 100ms",
|
|
2892
|
+
"CPU profiling shows a hot function",
|
|
2893
|
+
"Memory growth over time (potential leak)",
|
|
2894
|
+
"Database query log shows slow queries"
|
|
2895
|
+
],
|
|
2896
|
+
antiPatterns: [
|
|
2897
|
+
"UI-only code with no I/O or loops \u2014 defer",
|
|
2898
|
+
"Code that handles < 100 items (premature optimization)",
|
|
2899
|
+
"One-time scripts \u2014 performance rarely matters"
|
|
2900
|
+
],
|
|
2901
|
+
requires: [],
|
|
2902
|
+
relatedSkills: ["code-review"],
|
|
2903
|
+
tags: ["performance", "optimization", "big-o", "caching", "memory", "profiling"],
|
|
2904
|
+
examples: [
|
|
2905
|
+
{
|
|
2906
|
+
input: "Review src/lib/search.ts (filters 10,000 documents)",
|
|
2907
|
+
output: {
|
|
2908
|
+
findings: [
|
|
2909
|
+
{ type: "N+1 query", severity: "HIGH", file: "src/lib/search.ts:34", issue: "For each of 10,000 docs, fires a separate DB query to fetch tags. Total: 10,001 queries per search.", fix: "Use a single JOIN query to fetch all tags upfront. Reduces to 1 query." },
|
|
2910
|
+
{ type: "Allocation in hot loop", severity: "MEDIUM", file: "src/lib/search.ts:67", issue: "Creates a new Date object inside the filter loop (10,000 allocations per search).", fix: "Hoist the comparison date outside the loop." },
|
|
2911
|
+
{ type: "Missing index", severity: "MEDIUM", file: "src/lib/search.ts:45", issue: "SQL query filters by `created_at` but the table has no index on that column. Full table scan on every search.", fix: "CREATE INDEX idx_documents_created_at ON documents(created_at);" },
|
|
2912
|
+
{ type: "Caching opportunity", severity: "LOW", file: "src/lib/search.ts:89", issue: "Results identical for the same query within 60s window; no caching layer.", fix: "Add LRU cache keyed on query hash, TTL 60s." }
|
|
2913
|
+
],
|
|
2914
|
+
bigO: "O(n) becomes O(1) with the JOIN fix + O(1) with the cache",
|
|
2915
|
+
expectedSpeedup: "10-100x for typical queries"
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
],
|
|
2919
|
+
outputSchema: "{ findings: Array<{ type: string; severity: string; file: string; line: number; issue: string; fix: string }>; bigO: string; expectedSpeedup: string }",
|
|
2920
|
+
systemPromptFragment: `You are reviewing code for performance issues.
|
|
2921
|
+
|
|
2922
|
+
## Common patterns to detect
|
|
2923
|
+
1. **N+1 queries**: loop with a DB call inside (fetch all data upfront with a JOIN)
|
|
2924
|
+
2. **Allocation in hot loops**: \`new SomeClass()\` inside \`for (...)\` (hoist outside)
|
|
2925
|
+
3. **Missing index**: SQL filter on unindexed column (add index, EXPLAIN ANALYZE)
|
|
2926
|
+
4. **No caching**: identical computation repeated (LRU cache, memoize)
|
|
2927
|
+
5. **Blocking I/O in async**: \`await fs.readFileSync(...)\` (use async readFile)
|
|
2928
|
+
6. **Quadratic loops**: nested loops over the same array (use hash map for O(n) lookup)
|
|
2929
|
+
7. **Synchronous XHR/fetch**: blocking the main thread (use async/await or worker)
|
|
2930
|
+
|
|
2931
|
+
## Output format (JSON-typed)
|
|
2932
|
+
- findings: Array<{ type, severity, file, line, issue, fix }>
|
|
2933
|
+
- bigO: string (the complexity change after fixes)
|
|
2934
|
+
- expectedSpeedup: string (estimated speedup magnitude)
|
|
2935
|
+
|
|
2936
|
+
## Anti-patterns to avoid
|
|
2937
|
+
- **Don't propose premature optimizations** (caching for a function called once)
|
|
2938
|
+
- **Don't change behavior for performance** unless explicitly asked
|
|
2939
|
+
- **Profile first** \u2014 don't guess where the hot path is
|
|
2940
|
+
|
|
2941
|
+
Stay under 500 words.${CLARIFICATION_PROTOCOL6}`
|
|
2942
|
+
};
|
|
2943
|
+
var testCoverageAnalysis = {
|
|
2944
|
+
id: "test-coverage-analysis",
|
|
2945
|
+
version: "1.0.0",
|
|
2946
|
+
name: "Test Coverage Analysis",
|
|
2947
|
+
description: "Identify untested code branches. Suggest specific test cases that would close the coverage gap, prioritizing edge cases and error paths.",
|
|
2948
|
+
category: "review",
|
|
2949
|
+
requiredRoles: ["oracle", "atlas"],
|
|
2950
|
+
requiredTools: ["read_file"],
|
|
2951
|
+
estimatedCost: "medium",
|
|
2952
|
+
enabledByDefault: true,
|
|
2953
|
+
builtin: true,
|
|
2954
|
+
triggers: [
|
|
2955
|
+
"Coverage report shows < 80% line coverage",
|
|
2956
|
+
"A new feature was added without tests",
|
|
2957
|
+
"A bug was found that should have been caught by tests",
|
|
2958
|
+
"Quarterly coverage audit",
|
|
2959
|
+
"Refactoring a high-churn module where regression risk is high"
|
|
2960
|
+
],
|
|
2961
|
+
antiPatterns: [
|
|
2962
|
+
"Tests exist but are tautologies (assertions that always pass)",
|
|
2963
|
+
"Coverage targets > 95% (often forces meaningless tests)",
|
|
2964
|
+
"Auto-generated code (skip \u2014 the generator has its own tests)"
|
|
2965
|
+
],
|
|
2966
|
+
requires: [],
|
|
2967
|
+
relatedSkills: ["code-review"],
|
|
2968
|
+
tags: ["review", "testing", "coverage", "edge-cases"],
|
|
2969
|
+
examples: [
|
|
2970
|
+
{
|
|
2971
|
+
input: "src/lib/formatDuration.ts has 6 branches; coverage report shows only 3 are tested",
|
|
2972
|
+
output: {
|
|
2973
|
+
untestedBranches: [
|
|
2974
|
+
{ branch: "ms < 0 (negative duration)", file: "src/lib/formatDuration.ts:12", suggestedTest: "expect(formatDuration(-1000)).toThrow('Negative duration')" },
|
|
2975
|
+
{ branch: "ms === 0", file: "src/lib/formatDuration.ts:14", suggestedTest: "expect(formatDuration(0)).toBe('0s')" },
|
|
2976
|
+
{ branch: "ms > 1 day (very large)", file: "src/lib/formatDuration.ts:22", suggestedTest: "expect(formatDuration(86400000 * 2)).toBe('2d')" }
|
|
2977
|
+
],
|
|
2978
|
+
edgeCases: [
|
|
2979
|
+
"ms is NaN (e.g. from a corrupt timestamp) \u2014 should throw",
|
|
2980
|
+
"ms is Infinity (e.g. from setTimeout overflow) \u2014 should cap at a max",
|
|
2981
|
+
'style option is invalid \u2014 should default to "compact"'
|
|
2982
|
+
],
|
|
2983
|
+
priorityOrder: ["1. NaN/Infinity handling (data integrity)", "2. ms === 0 boundary (off-by-one)", "3. style validation (UX)"]
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
],
|
|
2987
|
+
outputSchema: "{ untestedBranches: Array<{ branch: string; file: string; line: number; suggestedTest: string }>; edgeCases: string[]; priorityOrder: string[] }",
|
|
2988
|
+
systemPromptFragment: `You are identifying untested code branches.
|
|
2989
|
+
|
|
2990
|
+
## Methodology
|
|
2991
|
+
1. **Read each branch** in the source file (if/else, switch cases, try/catch).
|
|
2992
|
+
2. **Identify untested branches** by reading the corresponding test file.
|
|
2993
|
+
3. **Suggest a SPECIFIC test** (not "add a test for X" \u2014 write the actual assertion).
|
|
2994
|
+
4. **Prioritize edge cases**: NaN, null, undefined, empty array, max int, negative numbers.
|
|
2995
|
+
5. **Prioritize error paths**: what happens when validation fails, network drops, etc.?
|
|
2996
|
+
|
|
2997
|
+
## Output format (JSON-typed)
|
|
2998
|
+
- untestedBranches: Array<{ branch, file, line, suggestedTest }>
|
|
2999
|
+
- edgeCases: string[] (3-7 cases)
|
|
3000
|
+
- priorityOrder: string[] (numbered list)
|
|
3001
|
+
|
|
3002
|
+
## Test quality principles
|
|
3003
|
+
- **One assertion per test** \u2014 easier to debug
|
|
3004
|
+
- **Specific assertion** \u2014 not \`expect(result).toBeTruthy()\` but \`expect(result).toBe('1h 23m')\`
|
|
3005
|
+
- **Test the boundary** \u2014 \`n=0\`, \`n=1\`, \`n=MAX_INT\`
|
|
3006
|
+
- **Test the failure path** \u2014 invalid input, network errors, partial data
|
|
3007
|
+
|
|
3008
|
+
Stay under 400 words.${CLARIFICATION_PROTOCOL6}`
|
|
3009
|
+
};
|
|
3010
|
+
var codeReview = {
|
|
3011
|
+
id: "code-review",
|
|
3012
|
+
version: "1.0.0",
|
|
3013
|
+
name: "Code Review",
|
|
3014
|
+
description: "Multi-role review covering correctness, style, performance, security, and accessibility. Each role emits findings with severity (CRITICAL/HIGH/MEDIUM/LOW). Chairman synthesizes consensus.",
|
|
3015
|
+
category: "review",
|
|
3016
|
+
requiredRoles: ["oracle", "atlas", "chairman"],
|
|
3017
|
+
requiredTools: ["read_file", "grep_content", "searchRAG"],
|
|
3018
|
+
estimatedCost: "high",
|
|
3019
|
+
enabledByDefault: true,
|
|
3020
|
+
builtin: true,
|
|
3021
|
+
triggers: [
|
|
3022
|
+
"A pull request is ready for review",
|
|
3023
|
+
"A code change affects > 100 lines",
|
|
3024
|
+
"Code touches auth, payments, or other security-critical paths",
|
|
3025
|
+
"Before merging to main",
|
|
3026
|
+
"A pre-merge sanity check on critical files"
|
|
3027
|
+
],
|
|
3028
|
+
antiPatterns: [
|
|
3029
|
+
"Single-line change (typo, comment fix) \u2014 direct merge",
|
|
3030
|
+
"Trivial style fix (whitespace, import order) \u2014 direct merge",
|
|
3031
|
+
"WIP / draft PR \u2014 review when ready",
|
|
3032
|
+
"Generated code (e.g. codegen output) \u2014 review the upstream generator instead"
|
|
3033
|
+
],
|
|
3034
|
+
requires: ["security-audit", "performance-review", "test-coverage-analysis"],
|
|
3035
|
+
relatedSkills: ["security-audit", "performance-review", "test-coverage-analysis"],
|
|
3036
|
+
tags: ["review", "multi-role", "pr-review", "correctness", "security"],
|
|
3037
|
+
examples: [
|
|
3038
|
+
{
|
|
3039
|
+
input: "Review src/components/AuthForm.tsx (180 LOC) \u2014 adds passwordless email login",
|
|
3040
|
+
output: {
|
|
3041
|
+
findings: [
|
|
3042
|
+
{ role: "oracle", severity: "CRITICAL", line: 67, issue: "No CSRF token on the form submit", file: "src/components/AuthForm.tsx" },
|
|
3043
|
+
{ role: "atlas", severity: "HIGH", line: 23, issue: "N+1 query: user lookup fires on every keystroke instead of debounced", file: "src/components/AuthForm.tsx" },
|
|
3044
|
+
{ role: "oracle", severity: "MEDIUM", line: 102, issue: "Error message leaks server response body (XSS risk)", file: "src/components/AuthForm.tsx" },
|
|
3045
|
+
{ role: "chairman", severity: "LOW", line: 45, issue: "Variable name `usrEml` should be `userEmail` (style)", file: "src/components/AuthForm.tsx" }
|
|
3046
|
+
],
|
|
3047
|
+
consensus: "2 CRITICAL/HIGH blockers must be fixed before merge. CSRF is the most critical. After CSRF fix, N+1 query, then error message.",
|
|
3048
|
+
mergeVerdict: "BLOCK"
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
],
|
|
3052
|
+
outputSchema: '{ findings: Array<{ role: string; severity: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW"; line: number; issue: string; file: string }>; consensus: string; mergeVerdict: "APPROVE" | "REQUEST_CHANGES" | "BLOCK" }',
|
|
3053
|
+
systemPromptFragment: `You are conducting a multi-role code review.
|
|
3054
|
+
|
|
3055
|
+
## Review roles
|
|
3056
|
+
- **oracle (correctness)**: bugs, race conditions, edge cases, error handling
|
|
3057
|
+
- **atlas (performance)**: O(n\xB2) algorithms, N+1 queries, unnecessary allocations, missing caching
|
|
3058
|
+
- **oracle (security)**: injection, auth bypass, secrets in code, unsafe deserialization (overlaps with security-audit skill \u2014 run that first)
|
|
3059
|
+
- **atlas (accessibility)**: ARIA labels, keyboard nav, color contrast
|
|
3060
|
+
- **chairman (synthesis)**: aggregates all findings, emits verdict (APPROVE / REQUEST_CHANGES / BLOCK)
|
|
3061
|
+
|
|
3062
|
+
## Severity levels
|
|
3063
|
+
- **CRITICAL**: must fix before merge (security, data loss, crash)
|
|
3064
|
+
- **HIGH**: should fix before merge (correctness bug, performance regression)
|
|
3065
|
+
- **MEDIUM**: nice to fix (code smell, minor perf)
|
|
3066
|
+
- **LOW**: nitpick (style, naming)
|
|
3067
|
+
|
|
3068
|
+
## Output format (JSON-typed)
|
|
3069
|
+
- findings: Array<{ role, severity, line, issue, file }>
|
|
3070
|
+
- consensus: string (chairman's synthesis)
|
|
3071
|
+
- mergeVerdict: 'APPROVE' | 'REQUEST_CHANGES' | 'BLOCK'
|
|
3072
|
+
|
|
3073
|
+
## Review principles
|
|
3074
|
+
- **Be specific**: cite file + line numbers
|
|
3075
|
+
- **Be actionable**: each finding has a concrete fix
|
|
3076
|
+
- **Don't bikeshed style** \u2014 note as LOW or skip
|
|
3077
|
+
- **Trust the author** \u2014 don't require changes that are preference, not correctness
|
|
3078
|
+
|
|
3079
|
+
Stay under 600 words.${CLARIFICATION_PROTOCOL6}`
|
|
3080
|
+
};
|
|
3081
|
+
var registerCodingSkill3 = registerCodingSkill;
|
|
3082
|
+
registerCodingSkill3(securityAudit);
|
|
3083
|
+
registerCodingSkill3(performanceReview);
|
|
3084
|
+
registerCodingSkill3(testCoverageAnalysis);
|
|
3085
|
+
registerCodingSkill3(codeReview);
|
|
3086
|
+
|
|
3087
|
+
// src/agents/skills/builtin/testing.ts
|
|
3088
|
+
var CLARIFICATION_PROTOCOL7 = `
|
|
3089
|
+
|
|
3090
|
+
WHEN TO ASK THE USER (clarification):
|
|
3091
|
+
If a single missing fact would materially change your output (target platform, scope, a binary design choice with significant trade-offs, a constraint you cannot safely assume), pause and ask the user by appending EXACTLY this block at the end of your message:
|
|
3092
|
+
|
|
3093
|
+
---QUESTION---
|
|
3094
|
+
{ "question": "One focused question", "choices": ["Option A", "Option B", "Option C"], "context": "Why this matters" }
|
|
3095
|
+
---END---
|
|
3096
|
+
|
|
3097
|
+
Rules for clarifications:
|
|
3098
|
+
- Ask AT MOST ONE question per turn, and only when genuinely blocked.
|
|
3099
|
+
- Prefer a small set of concrete "choices" (2-4). The user can still type a custom answer.
|
|
3100
|
+
- Do NOT ask for information that could be reasonably assumed or already in shared context.
|
|
3101
|
+
- If you can proceed with a sound documented assumption, DO SO instead of asking.`;
|
|
3102
|
+
var writeUnitTests = {
|
|
3103
|
+
id: "write-unit-tests",
|
|
3104
|
+
version: "1.0.0",
|
|
3105
|
+
name: "Write Unit Tests",
|
|
3106
|
+
description: "TDD-style test generation for a function or module. Output: 5-10 focused unit tests covering happy paths, edge cases (empty/null/boundary), error paths. Each test has one assertion. Uses Vitest patterns (the project standard).",
|
|
3107
|
+
category: "test",
|
|
3108
|
+
requiredRoles: ["oracle", "atlas"],
|
|
3109
|
+
requiredTools: ["read_file"],
|
|
3110
|
+
estimatedCost: "medium",
|
|
3111
|
+
enabledByDefault: true,
|
|
3112
|
+
builtin: true,
|
|
3113
|
+
triggers: [
|
|
3114
|
+
"A function or module has no tests",
|
|
3115
|
+
"Coverage report shows a specific function is untested",
|
|
3116
|
+
"A new utility is added (helper, formatter, validator)",
|
|
3117
|
+
"A bug is fixed and a regression test is needed"
|
|
3118
|
+
],
|
|
3119
|
+
antiPatterns: [
|
|
3120
|
+
"Integration scenarios (multi-component) \u2014 use write-integration-tests",
|
|
3121
|
+
"A bug report exists \u2014 use regression-test instead (more focused)",
|
|
3122
|
+
"Performance benchmarks \u2014 those need write-bench-test, not unit tests"
|
|
3123
|
+
],
|
|
3124
|
+
requires: [],
|
|
3125
|
+
relatedSkills: ["regression-test", "test-coverage-analysis"],
|
|
3126
|
+
tags: ["test", "unit-test", "tdd", "vitest", "coverage"],
|
|
3127
|
+
examples: [
|
|
3128
|
+
{
|
|
3129
|
+
input: "Write unit tests for src/lib/formatDuration.ts",
|
|
3130
|
+
output: {
|
|
3131
|
+
testFile: "tests/unit/formatDuration.test.ts",
|
|
3132
|
+
tests: [
|
|
3133
|
+
{ name: 'formats 0 ms as "0s"', input: 0, expected: "0s" },
|
|
3134
|
+
{ name: 'formats 45 seconds as "45s"', input: 45e3, expected: "45s" },
|
|
3135
|
+
{ name: 'formats 1 minute as "1m"', input: 6e4, expected: "1m" },
|
|
3136
|
+
{ name: "formats 1h 23m for long duration", input: 498e4, expected: "1h 23m" },
|
|
3137
|
+
{ name: "formats 2d for >24h", input: 1728e5, expected: "2d" },
|
|
3138
|
+
{ name: "throws on negative duration", input: -1e3, expectedThrows: "Negative duration" },
|
|
3139
|
+
{ name: "throws on NaN", input: NaN, expectedThrows: "NaN duration" },
|
|
3140
|
+
{ name: "respects compact style option", input: 6e4, options: { style: "compact" }, expected: "1m" }
|
|
3141
|
+
]
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
],
|
|
3145
|
+
outputSchema: "{ testFile: string; tests: Array<{ name: string; input: unknown; expected?: unknown; options?: Record<string, unknown>; expectedThrows?: string }> }",
|
|
3146
|
+
systemPromptFragment: `You are writing Vitest unit tests for a function or module.
|
|
3147
|
+
|
|
3148
|
+
## Methodology
|
|
3149
|
+
1. **Read the source**: understand the function signature, branches, error paths.
|
|
3150
|
+
2. **Identify happy paths**: at least 2 tests for typical inputs.
|
|
3151
|
+
3. **Identify edge cases** (boundaries): 0, 1, max, empty array, empty string.
|
|
3152
|
+
4. **Identify error paths**: invalid input, network errors, partial data.
|
|
3153
|
+
5. **Write one test per assertion** \u2014 easier to debug when a test fails.
|
|
3154
|
+
6. **Use Vitest patterns**: describe/it/expect, vi.fn() for mocks, vi.spyOn for stubs.
|
|
3155
|
+
|
|
3156
|
+
## Test naming
|
|
3157
|
+
- Use \`describe('moduleName', () => { it('does X when Y', ...) })\`.
|
|
3158
|
+
- Test name describes behavior, not implementation.
|
|
3159
|
+
- Group related tests in the same describe block.
|
|
3160
|
+
|
|
3161
|
+
## Output format (JSON-typed)
|
|
3162
|
+
- testFile: string (the file path)
|
|
3163
|
+
- tests: Array<{ name, input, expected, options?, expectedThrows? }>
|
|
3164
|
+
|
|
3165
|
+
## Anti-patterns to avoid
|
|
3166
|
+
- Tests that always pass (tautologies)
|
|
3167
|
+
- Tests with multiple assertions (split into separate tests)
|
|
3168
|
+
- Tests that depend on test execution order
|
|
3169
|
+
- Snapshot tests for non-deterministic output
|
|
3170
|
+
|
|
3171
|
+
Stay under 500 words.${CLARIFICATION_PROTOCOL7}`
|
|
3172
|
+
};
|
|
3173
|
+
var writeIntegrationTests = {
|
|
3174
|
+
id: "write-integration-tests",
|
|
3175
|
+
version: "1.0.0",
|
|
3176
|
+
name: "Write Integration Tests",
|
|
3177
|
+
description: "Generate end-to-end scenario tests that span multiple components. Tests verify the integration boundary, not individual units. Uses real subsystems (or close mocks) to catch contract mismatches.",
|
|
3178
|
+
category: "test",
|
|
3179
|
+
requiredRoles: ["oracle", "hephaestus"],
|
|
3180
|
+
requiredTools: ["read_file", "grep_content"],
|
|
3181
|
+
estimatedCost: "medium",
|
|
3182
|
+
enabledByDefault: true,
|
|
3183
|
+
builtin: true,
|
|
3184
|
+
triggers: [
|
|
3185
|
+
"A new feature spans 2+ modules",
|
|
3186
|
+
"A new API endpoint needs end-to-end coverage",
|
|
3187
|
+
"A refactor changes module boundaries \u2014 verify behavior preserved",
|
|
3188
|
+
"A bug was caused by a contract mismatch between two modules"
|
|
3189
|
+
],
|
|
3190
|
+
antiPatterns: [
|
|
3191
|
+
"Single-component logic \u2014 use write-unit-tests",
|
|
3192
|
+
"Performance benchmarks \u2014 use a dedicated perf test",
|
|
3193
|
+
"Visual regression (UI snapshot) \u2014 needs screenshot diff tooling"
|
|
3194
|
+
],
|
|
3195
|
+
requires: ["write-unit-tests"],
|
|
3196
|
+
relatedSkills: ["write-unit-tests", "regression-test"],
|
|
3197
|
+
tags: ["test", "integration", "e2e", "contract", "multi-component"],
|
|
3198
|
+
examples: [
|
|
3199
|
+
{
|
|
3200
|
+
input: 'Integration test for "user uploads file \u2192 RAG indexes \u2192 user queries"',
|
|
3201
|
+
output: {
|
|
3202
|
+
testFile: "tests/integration/upload-query.test.ts",
|
|
3203
|
+
scenarios: [
|
|
3204
|
+
{ name: "uploaded file appears in search results within 5s", steps: ["create test vault", "upload doc.md", "wait 1s for indexer", 'query "doc.md"', "expect result"] },
|
|
3205
|
+
{ name: "deleted file removed from search results", steps: ["upload + query (sanity)", "delete file", "wait 1s", "query", "expect no result"] },
|
|
3206
|
+
{ name: "concurrent uploads maintain ordering", steps: ["upload 10 files in parallel", "query each", "expect all 10 found"] },
|
|
3207
|
+
{ name: "corrupt file (binary garbage) does not crash indexer", steps: ["upload binary garbage", "wait 5s", "expect vault accessible (no crash)"] }
|
|
3208
|
+
],
|
|
3209
|
+
contracts: [
|
|
3210
|
+
{ between: "vault + indexer", contract: "indexer subscribes to vault:created events and processes new files within 5s" },
|
|
3211
|
+
{ between: "indexer + search", contract: "search returns docs only after indexer has flushed their embeddings to SQLite" }
|
|
3212
|
+
]
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
],
|
|
3216
|
+
outputSchema: "{ testFile: string; scenarios: Array<{ name: string; steps: string[] }>; contracts: Array<{ between: string; contract: string }> }",
|
|
3217
|
+
systemPromptFragment: `You are writing integration tests that span multiple components.
|
|
3218
|
+
|
|
3219
|
+
## Methodology
|
|
3220
|
+
1. **Identify the integration boundary**: which 2+ components interact?
|
|
3221
|
+
2. **Document the contract**: what does component A promise to component B?
|
|
3222
|
+
3. **Test the contract, not the internals**: verify the OBSERVABLE behavior, not the implementation.
|
|
3223
|
+
4. **Use real subsystems where possible**: in-memory SQLite instead of mocks; fake HTTP server instead of stubbed client.
|
|
3224
|
+
5. **Each scenario is a complete user story**: setup \u2192 action \u2192 verify.
|
|
3225
|
+
|
|
3226
|
+
## Output format (JSON-typed)
|
|
3227
|
+
- testFile: string
|
|
3228
|
+
- scenarios: Array<{ name, steps[] }>
|
|
3229
|
+
- contracts: Array<{ between, contract }>
|
|
3230
|
+
|
|
3231
|
+
## Integration test principles
|
|
3232
|
+
- **Minimal mocks**: mock only the OUTSIDE world (network, disk, time), not your own code
|
|
3233
|
+
- **Real data**: use realistic test data, not "foo" / "bar"
|
|
3234
|
+
- **Deterministic**: avoid timing-based assertions (use polling with timeout)
|
|
3235
|
+
- **Cleanup**: each scenario should leave the system in a clean state
|
|
3236
|
+
|
|
3237
|
+
Stay under 500 words.${CLARIFICATION_PROTOCOL7}`
|
|
3238
|
+
};
|
|
3239
|
+
var regressionTest = {
|
|
3240
|
+
id: "regression-test",
|
|
3241
|
+
version: "1.0.0",
|
|
3242
|
+
name: "Regression Test",
|
|
3243
|
+
description: "Convert a bug report into a permanent regression test. The test must fail on the BUGGY code (pre-fix), pass on the FIXED code (post-fix), and stay in the suite forever to prevent the bug from coming back.",
|
|
3244
|
+
category: "test",
|
|
3245
|
+
requiredRoles: ["oracle"],
|
|
3246
|
+
requiredTools: ["write_file"],
|
|
3247
|
+
estimatedCost: "low",
|
|
3248
|
+
enabledByDefault: true,
|
|
3249
|
+
builtin: true,
|
|
3250
|
+
triggers: [
|
|
3251
|
+
"A bug is fixed and a regression test is needed",
|
|
3252
|
+
'A user reports a recurring bug ("this used to work")',
|
|
3253
|
+
"A flaky test that intermittently fails",
|
|
3254
|
+
"A bug found in code review"
|
|
3255
|
+
],
|
|
3256
|
+
antiPatterns: [
|
|
3257
|
+
"The bug has not been fixed yet \u2014 first fix, then add regression test",
|
|
3258
|
+
"The bug is in test code itself \u2014 fix the test",
|
|
3259
|
+
"A bug that requires impossible-to-mock state (real network, real browser)"
|
|
3260
|
+
],
|
|
3261
|
+
requires: [],
|
|
3262
|
+
relatedSkills: ["write-unit-tests", "reproduce-bug"],
|
|
3263
|
+
tags: ["test", "regression", "bug-fix", "permanent-test"],
|
|
3264
|
+
examples: [
|
|
3265
|
+
{
|
|
3266
|
+
input: 'Bug: "Council shows duplicate user message at session resume" was just fixed in commit a3f9d',
|
|
3267
|
+
output: {
|
|
3268
|
+
testFile: "tests/regression/duplicate-user-message.test.ts",
|
|
3269
|
+
test: `import { describe, it, expect } from 'vitest';
|
|
3270
|
+
import { runCouncilPure } from '../agents/councilApi';
|
|
3271
|
+
|
|
3272
|
+
describe('regression: duplicate user message at session resume', () => {
|
|
3273
|
+
it('does not render duplicate user message when session resumes with duplicate input', async () => {
|
|
3274
|
+
const messages = [
|
|
3275
|
+
{ role: 'user' as const, content: 'hi' },
|
|
3276
|
+
{ role: 'user' as const, content: 'hi' }, // duplicate from session resume
|
|
3277
|
+
];
|
|
3278
|
+
const mockProvider = async function* () {
|
|
3279
|
+
yield { kind: 'text' as const, delta: 'response' };
|
|
3280
|
+
yield { kind: 'finish' as const, reason: 'stop' };
|
|
3281
|
+
};
|
|
3282
|
+
const events = [];
|
|
3283
|
+
for await (const e of runCouncilPure('hi', {
|
|
3284
|
+
model: 'test', provider: 'test',
|
|
3285
|
+
messages,
|
|
3286
|
+
tools: [],
|
|
3287
|
+
councilSize: 1,
|
|
3288
|
+
debateMode: false,
|
|
3289
|
+
ragContext: '',
|
|
3290
|
+
workspaceContext: '',
|
|
3291
|
+
providerStream: mockProvider,
|
|
3292
|
+
}, {})) {
|
|
3293
|
+
events.push(e);
|
|
3294
|
+
}
|
|
3295
|
+
const userMessageDeltas = events.filter(e => e.type === 'message_delta').length;
|
|
3296
|
+
expect(userMessageDeltas).toBe(1); // exactly 1, not 2
|
|
3297
|
+
});
|
|
3298
|
+
});`,
|
|
3299
|
+
whyRegression: "Bug was reported 3 times in the last month. The fix (dedup user messages on session resume) was in commit a3f9d. This test guards against future refactors breaking the dedup logic.",
|
|
3300
|
+
bugCommit: "a3f9d"
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
],
|
|
3304
|
+
outputSchema: "{ testFile: string; test: string; whyRegression: string; bugCommit: string }",
|
|
3305
|
+
systemPromptFragment: `You are converting a fixed bug into a permanent regression test.
|
|
3306
|
+
|
|
3307
|
+
## Methodology
|
|
3308
|
+
1. **Confirm the bug is FIXED**: check git log / recent commits for the fix.
|
|
3309
|
+
2. **Write the test**: it should FAIL if you revert the fix (verify this mentally).
|
|
3310
|
+
3. **Add a comment** in the test file linking to the bug commit.
|
|
3311
|
+
4. **Place in tests/regression/**: separate from unit tests for visibility.
|
|
3312
|
+
|
|
3313
|
+
## Output format (JSON-typed)
|
|
3314
|
+
- testFile: string
|
|
3315
|
+
- test: string (the full test code, runnable as-is)
|
|
3316
|
+
- whyRegression: string (why this test should stay forever)
|
|
3317
|
+
- bugCommit: string (the commit hash that fixed the bug)
|
|
3318
|
+
|
|
3319
|
+
## Regression test principles
|
|
3320
|
+
- **One test per bug** \u2014 easier to identify which regression fired
|
|
3321
|
+
- **Reference the bug commit** in a comment so future devs understand the history
|
|
3322
|
+
- **Minimal repro** \u2014 strip to the essential trigger, don't include unrelated setup
|
|
3323
|
+
- **Never delete a regression test** without a written justification (the bug came back once, it'll come back again)
|
|
3324
|
+
|
|
3325
|
+
Stay under 300 words.${CLARIFICATION_PROTOCOL7}`
|
|
3326
|
+
};
|
|
3327
|
+
registerCodingSkill(writeUnitTests);
|
|
3328
|
+
registerCodingSkill(writeIntegrationTests);
|
|
3329
|
+
registerCodingSkill(regressionTest);
|
|
3330
|
+
|
|
3331
|
+
// src/agents/roles.ts
|
|
3332
|
+
var CLARIFICATION_PROTOCOL8 = `
|
|
3333
|
+
|
|
3334
|
+
WHEN TO ASK THE USER (clarification):
|
|
3335
|
+
If a single missing fact would materially change your output (target platform, scope, a binary design choice with significant trade-offs, a constraint you cannot safely assume), pause and ask the user by appending EXACTLY this block at the end of your message:
|
|
3336
|
+
|
|
3337
|
+
---QUESTION---
|
|
3338
|
+
{ "question": "One focused question", "choices": ["Option A", "Option B", "Option C"], "context": "Why this matters" }
|
|
3339
|
+
---END---
|
|
3340
|
+
|
|
1335
3341
|
Rules for clarifications:
|
|
1336
3342
|
- Ask AT MOST ONE question per turn, and only when genuinely blocked.
|
|
1337
3343
|
- Prefer a small set of concrete "choices" (2-4). The user can still type a custom answer.
|
|
@@ -1361,7 +3367,7 @@ var AGENT_ROLES = [
|
|
|
1361
3367
|
- You run first and set context for everyone downstream; make it count.
|
|
1362
3368
|
|
|
1363
3369
|
## Output format
|
|
1364
|
-
A short "Analysis" section (the goal + constraints), then a "Delegation Plan" with one bullet per specialist naming what they should produce. Keep it tight \u2014 under 150 words total.${
|
|
3370
|
+
A short "Analysis" section (the goal + constraints), then a "Delegation Plan" with one bullet per specialist naming what they should produce. Keep it tight \u2014 under 150 words total.${CLARIFICATION_PROTOCOL8}`,
|
|
1365
3371
|
tools: ["createTask", "createPhase"],
|
|
1366
3372
|
skills: ["project-planner", "research-analyst"]
|
|
1367
3373
|
},
|
|
@@ -1397,7 +3403,7 @@ Example format:
|
|
|
1397
3403
|
- Make dependencies explicit (task B depends on task A).
|
|
1398
3404
|
- If the stack/platform is unknown, ask ONCE (see clarification protocol) rather than guessing.
|
|
1399
3405
|
|
|
1400
|
-
Keep plans hierarchical and practical. Stay under 250 words.${
|
|
3406
|
+
Keep plans hierarchical and practical. Stay under 250 words.${CLARIFICATION_PROTOCOL8}`,
|
|
1401
3407
|
tools: ["createTask", "createPhase"],
|
|
1402
3408
|
skills: ["project-planner", "vault-manager"]
|
|
1403
3409
|
},
|
|
@@ -1427,7 +3433,7 @@ Keep plans hierarchical and practical. Stay under 250 words.${CLARIFICATION_PROT
|
|
|
1427
3433
|
## Themes
|
|
1428
3434
|
## Top picks (with feasibility/novelty + de-risk note)
|
|
1429
3435
|
|
|
1430
|
-
Stay under 200 words.${
|
|
3436
|
+
Stay under 200 words.${CLARIFICATION_PROTOCOL8}`,
|
|
1431
3437
|
tools: ["addIdea"],
|
|
1432
3438
|
skills: ["idea-synthesizer", "mind-mapper", "document-writer"]
|
|
1433
3439
|
},
|
|
@@ -1454,7 +3460,7 @@ Stay under 200 words.${CLARIFICATION_PROTOCOL}`,
|
|
|
1454
3460
|
- Keep the graph comprehensible: prune redundant or duplicate nodes.
|
|
1455
3461
|
|
|
1456
3462
|
## Output format
|
|
1457
|
-
Describe the proposed structure (root \u2192 branches \u2192 leaves) in text and, when building via tool, emit a buildMindMap payload. Stay under 200 words.${
|
|
3463
|
+
Describe the proposed structure (root \u2192 branches \u2192 leaves) in text and, when building via tool, emit a buildMindMap payload. Stay under 200 words.${CLARIFICATION_PROTOCOL8}`,
|
|
1458
3464
|
tools: ["buildMindMap", "addNode", "linkNodes"],
|
|
1459
3465
|
skills: ["mind-mapper", "research-analyst"]
|
|
1460
3466
|
},
|
|
@@ -1485,7 +3491,7 @@ Describe the proposed structure (root \u2192 branches \u2192 leaves) in text and
|
|
|
1485
3491
|
## Contradictions
|
|
1486
3492
|
## Top improvements
|
|
1487
3493
|
|
|
1488
|
-
Stay under 200 words. (You do not create workspace artifacts, so you do not emit a tools block \u2014 but you MAY ask the user a clarifying question if a core ambiguity blocks judgment.)${
|
|
3494
|
+
Stay under 200 words. (You do not create workspace artifacts, so you do not emit a tools block \u2014 but you MAY ask the user a clarifying question if a core ambiguity blocks judgment.)${CLARIFICATION_PROTOCOL8}`,
|
|
1489
3495
|
tools: [],
|
|
1490
3496
|
skills: ["research-analyst"]
|
|
1491
3497
|
},
|
|
@@ -1519,7 +3525,7 @@ Append EXACTLY this block at the very end of your message when you must create w
|
|
|
1519
3525
|
]
|
|
1520
3526
|
---END---
|
|
1521
3527
|
Available tool names: createTask, addIdea, createPhase, buildMindMap, addNode, linkNodes, createDocument.
|
|
1522
|
-
Only emit this block at the very end and only if actions are needed. Otherwise output plain text.${
|
|
3528
|
+
Only emit this block at the very end and only if actions are needed. Otherwise output plain text.${CLARIFICATION_PROTOCOL8}`,
|
|
1523
3529
|
tools: ["createTask", "addIdea", "createPhase", "buildMindMap", "addNode", "linkNodes", "createDocument"],
|
|
1524
3530
|
skills: ["vault-manager", "project-planner", "idea-synthesizer"]
|
|
1525
3531
|
}
|
|
@@ -1849,6 +3855,41 @@ var AgentHarness = class {
|
|
|
1849
3855
|
});
|
|
1850
3856
|
this.emit(initialMsgEnd);
|
|
1851
3857
|
yield initialMsgEnd;
|
|
3858
|
+
const MAX_TOOL_LOOP_ITERATIONS = 12;
|
|
3859
|
+
let toolLoopTurns = 0;
|
|
3860
|
+
while (!this.cancelled && !hadError && toolLoopTurns < MAX_TOOL_LOOP_ITERATIONS && initialFinishRef.value === "tool_calls") {
|
|
3861
|
+
toolLoopTurns++;
|
|
3862
|
+
const turnMessageId = crypto.randomUUID();
|
|
3863
|
+
const msgStart = createBrainEvent(
|
|
3864
|
+
"message_start",
|
|
3865
|
+
this.sessionId,
|
|
3866
|
+
{ messageId: turnMessageId, role: "assistant" }
|
|
3867
|
+
);
|
|
3868
|
+
this.emit(msgStart);
|
|
3869
|
+
yield msgStart;
|
|
3870
|
+
let turnLength = 0;
|
|
3871
|
+
const turnFinishRef = { value: "stop" };
|
|
3872
|
+
const turnUsageRef = { value: null };
|
|
3873
|
+
for await (const ev of this.runSingleTurn(turnMessageId, turnFinishRef, turnUsageRef)) {
|
|
3874
|
+
if (ev.type === "message_delta") {
|
|
3875
|
+
turnLength += ev.delta.length;
|
|
3876
|
+
} else if (ev.type === "error") {
|
|
3877
|
+
if (ev.severity !== "cancelled") hadError = true;
|
|
3878
|
+
}
|
|
3879
|
+
yield ev;
|
|
3880
|
+
}
|
|
3881
|
+
totalLength += turnLength;
|
|
3882
|
+
const msgEnd = createBrainEvent("message_end", this.sessionId, {
|
|
3883
|
+
messageId: turnMessageId,
|
|
3884
|
+
totalLength: turnLength,
|
|
3885
|
+
finishReason: turnFinishRef.value,
|
|
3886
|
+
...turnUsageRef.value ? { usage: turnUsageRef.value } : {}
|
|
3887
|
+
});
|
|
3888
|
+
this.emit(msgEnd);
|
|
3889
|
+
yield msgEnd;
|
|
3890
|
+
initialFinishRef.value = turnFinishRef.value;
|
|
3891
|
+
if (hadError || this.cancelled) break;
|
|
3892
|
+
}
|
|
1852
3893
|
let turns = 0;
|
|
1853
3894
|
while (turns < this.maxQueuedIterations) {
|
|
1854
3895
|
if (this.cancelled) break;
|
|
@@ -1919,6 +3960,8 @@ var AgentHarness = class {
|
|
|
1919
3960
|
});
|
|
1920
3961
|
let toolCallsThisTurn = 0;
|
|
1921
3962
|
const maxToolCalls = this.config.maxToolCallsPerTurn;
|
|
3963
|
+
let turnText = "";
|
|
3964
|
+
const turnToolCalls = [];
|
|
1922
3965
|
for await (const delta of stream) {
|
|
1923
3966
|
if (this.cancelled) {
|
|
1924
3967
|
const cancelEvent = createBrainEvent("error", this.sessionId, {
|
|
@@ -1931,6 +3974,7 @@ var AgentHarness = class {
|
|
|
1931
3974
|
break;
|
|
1932
3975
|
}
|
|
1933
3976
|
if (delta.kind === "text") {
|
|
3977
|
+
turnText += delta.delta;
|
|
1934
3978
|
const deltaEvent = createBrainEvent(
|
|
1935
3979
|
"message_delta",
|
|
1936
3980
|
this.sessionId,
|
|
@@ -1947,6 +3991,7 @@ var AgentHarness = class {
|
|
|
1947
3991
|
yield thinkEvent;
|
|
1948
3992
|
} else if (delta.kind === "tool_call") {
|
|
1949
3993
|
toolCallsThisTurn++;
|
|
3994
|
+
turnToolCalls.push({ id: delta.toolCallId, name: delta.toolName, args: delta.args });
|
|
1950
3995
|
const toolStartEvent = createBrainEvent("tool_execution_start", this.sessionId, {
|
|
1951
3996
|
toolCallId: delta.toolCallId,
|
|
1952
3997
|
toolName: delta.toolName,
|
|
@@ -1966,18 +4011,24 @@ var AgentHarness = class {
|
|
|
1966
4011
|
signal: this.activeController?.signal
|
|
1967
4012
|
}
|
|
1968
4013
|
);
|
|
4014
|
+
const resultStr = result.ok ? String(result.value) : result.error;
|
|
1969
4015
|
const endEvent = createBrainEvent(
|
|
1970
4016
|
"tool_execution_end",
|
|
1971
4017
|
this.sessionId,
|
|
1972
4018
|
{
|
|
1973
4019
|
toolCallId: delta.toolCallId,
|
|
1974
|
-
result:
|
|
4020
|
+
result: resultStr,
|
|
1975
4021
|
isError: !result.ok,
|
|
1976
4022
|
durationMs: Date.now() - startMs
|
|
1977
4023
|
}
|
|
1978
4024
|
);
|
|
1979
4025
|
this.emit(endEvent);
|
|
1980
4026
|
yield endEvent;
|
|
4027
|
+
this.config.messages.push({
|
|
4028
|
+
role: "tool",
|
|
4029
|
+
toolCallId: delta.toolCallId,
|
|
4030
|
+
content: resultStr
|
|
4031
|
+
});
|
|
1981
4032
|
} else if (skipped) {
|
|
1982
4033
|
const endEvent = createBrainEvent(
|
|
1983
4034
|
"tool_execution_end",
|
|
@@ -1991,9 +4042,21 @@ var AgentHarness = class {
|
|
|
1991
4042
|
);
|
|
1992
4043
|
this.emit(endEvent);
|
|
1993
4044
|
yield endEvent;
|
|
4045
|
+
this.config.messages.push({
|
|
4046
|
+
role: "tool",
|
|
4047
|
+
toolCallId: delta.toolCallId,
|
|
4048
|
+
content: `[skipped] maxToolCallsPerTurn reached (limit=${maxToolCalls})`
|
|
4049
|
+
});
|
|
1994
4050
|
}
|
|
1995
4051
|
} else if (delta.kind === "finish") {
|
|
1996
4052
|
finishRef.value = delta.reason;
|
|
4053
|
+
if (turnToolCalls.length > 0 || turnText.length > 0) {
|
|
4054
|
+
this.config.messages.push({
|
|
4055
|
+
role: "assistant",
|
|
4056
|
+
content: turnText,
|
|
4057
|
+
...turnToolCalls.length > 0 ? { toolCalls: turnToolCalls } : {}
|
|
4058
|
+
});
|
|
4059
|
+
}
|
|
1997
4060
|
break;
|
|
1998
4061
|
} else if (delta.kind === "error") {
|
|
1999
4062
|
const errEvent = createBrainEvent("error", this.sessionId, {
|
|
@@ -2245,9 +4308,33 @@ function resolveBaseUrl(providerId) {
|
|
|
2245
4308
|
}
|
|
2246
4309
|
function openaiCompatibleProvider(config2) {
|
|
2247
4310
|
return async function* (params) {
|
|
4311
|
+
const messages = params.messages.map((m) => {
|
|
4312
|
+
if (m.role === "tool") {
|
|
4313
|
+
return {
|
|
4314
|
+
role: "tool",
|
|
4315
|
+
tool_call_id: m.toolCallId,
|
|
4316
|
+
content: m.content
|
|
4317
|
+
};
|
|
4318
|
+
}
|
|
4319
|
+
if (m.role === "assistant" && m.toolCalls && m.toolCalls.length > 0) {
|
|
4320
|
+
return {
|
|
4321
|
+
role: "assistant",
|
|
4322
|
+
content: m.content ?? "",
|
|
4323
|
+
tool_calls: m.toolCalls.map((tc) => ({
|
|
4324
|
+
id: tc.id,
|
|
4325
|
+
type: "function",
|
|
4326
|
+
function: {
|
|
4327
|
+
name: tc.name,
|
|
4328
|
+
arguments: JSON.stringify(tc.args ?? {})
|
|
4329
|
+
}
|
|
4330
|
+
}))
|
|
4331
|
+
};
|
|
4332
|
+
}
|
|
4333
|
+
return { role: m.role, content: m.content };
|
|
4334
|
+
});
|
|
2248
4335
|
const body = {
|
|
2249
4336
|
model: config2.model,
|
|
2250
|
-
messages
|
|
4337
|
+
messages,
|
|
2251
4338
|
stream: true,
|
|
2252
4339
|
temperature: 0.7,
|
|
2253
4340
|
// Task G.4.2 — request the provider to send real token usage in
|
|
@@ -2257,6 +4344,17 @@ function openaiCompatibleProvider(config2) {
|
|
|
2257
4344
|
// the harness will fall back to the ~4-char/token approximation.
|
|
2258
4345
|
stream_options: { include_usage: true }
|
|
2259
4346
|
};
|
|
4347
|
+
if (params.tools && params.tools.length > 0) {
|
|
4348
|
+
body.tools = params.tools.map((t) => ({
|
|
4349
|
+
type: "function",
|
|
4350
|
+
function: {
|
|
4351
|
+
name: t.name,
|
|
4352
|
+
description: t.description,
|
|
4353
|
+
parameters: t.parameters
|
|
4354
|
+
}
|
|
4355
|
+
}));
|
|
4356
|
+
body.tool_choice = "auto";
|
|
4357
|
+
}
|
|
2260
4358
|
let response;
|
|
2261
4359
|
try {
|
|
2262
4360
|
response = await fetch(`${config2.baseUrl}/chat/completions`, {
|
|
@@ -2339,6 +4437,9 @@ function openaiCompatibleProvider(config2) {
|
|
|
2339
4437
|
}
|
|
2340
4438
|
}
|
|
2341
4439
|
}
|
|
4440
|
+
if (choice?.finish_reason) {
|
|
4441
|
+
yield { kind: "finish", reason: choice.finish_reason };
|
|
4442
|
+
}
|
|
2342
4443
|
} catch {
|
|
2343
4444
|
}
|
|
2344
4445
|
}
|
|
@@ -2840,11 +4941,11 @@ var TOOL_DEFINITIONS = [
|
|
|
2840
4941
|
execute: (args, ctx) => {
|
|
2841
4942
|
if (!ctx.addDocument) return "Knowledge vault tool not available.";
|
|
2842
4943
|
const title = args["title"] || "New Document";
|
|
2843
|
-
const
|
|
4944
|
+
const path18 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
|
|
2844
4945
|
const content = args["content"] || "";
|
|
2845
4946
|
const tags = args["tags"] || [];
|
|
2846
4947
|
ctx.addDocument({
|
|
2847
|
-
path:
|
|
4948
|
+
path: path18,
|
|
2848
4949
|
title,
|
|
2849
4950
|
content,
|
|
2850
4951
|
format: "markdown",
|
|
@@ -2853,7 +4954,7 @@ var TOOL_DEFINITIONS = [
|
|
|
2853
4954
|
workspaceId: ctx.workspaceId
|
|
2854
4955
|
});
|
|
2855
4956
|
ctx.addActivity("vault", "created document", title);
|
|
2856
|
-
return `Document "${title}" created at "${
|
|
4957
|
+
return `Document "${title}" created at "${path18}".`;
|
|
2857
4958
|
}
|
|
2858
4959
|
}
|
|
2859
4960
|
];
|
|
@@ -5120,10 +7221,10 @@ function mergeDefs(...defs) {
|
|
|
5120
7221
|
function cloneDef(schema) {
|
|
5121
7222
|
return mergeDefs(schema._zod.def);
|
|
5122
7223
|
}
|
|
5123
|
-
function getElementAtPath(obj,
|
|
5124
|
-
if (!
|
|
7224
|
+
function getElementAtPath(obj, path18) {
|
|
7225
|
+
if (!path18)
|
|
5125
7226
|
return obj;
|
|
5126
|
-
return
|
|
7227
|
+
return path18.reduce((acc, key) => acc?.[key], obj);
|
|
5127
7228
|
}
|
|
5128
7229
|
function promiseAllObject(promisesObj) {
|
|
5129
7230
|
const keys = Object.keys(promisesObj);
|
|
@@ -5532,11 +7633,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
5532
7633
|
}
|
|
5533
7634
|
return false;
|
|
5534
7635
|
}
|
|
5535
|
-
function prefixIssues(
|
|
7636
|
+
function prefixIssues(path18, issues) {
|
|
5536
7637
|
return issues.map((iss) => {
|
|
5537
7638
|
var _a3;
|
|
5538
7639
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
5539
|
-
iss.path.unshift(
|
|
7640
|
+
iss.path.unshift(path18);
|
|
5540
7641
|
return iss;
|
|
5541
7642
|
});
|
|
5542
7643
|
}
|
|
@@ -5683,16 +7784,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
|
|
|
5683
7784
|
}
|
|
5684
7785
|
function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
5685
7786
|
const fieldErrors = { _errors: [] };
|
|
5686
|
-
const processError = (error52,
|
|
7787
|
+
const processError = (error52, path18 = []) => {
|
|
5687
7788
|
for (const issue2 of error52.issues) {
|
|
5688
7789
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
5689
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
7790
|
+
issue2.errors.map((issues) => processError({ issues }, [...path18, ...issue2.path]));
|
|
5690
7791
|
} else if (issue2.code === "invalid_key") {
|
|
5691
|
-
processError({ issues: issue2.issues }, [...
|
|
7792
|
+
processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
|
|
5692
7793
|
} else if (issue2.code === "invalid_element") {
|
|
5693
|
-
processError({ issues: issue2.issues }, [...
|
|
7794
|
+
processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
|
|
5694
7795
|
} else {
|
|
5695
|
-
const fullpath = [...
|
|
7796
|
+
const fullpath = [...path18, ...issue2.path];
|
|
5696
7797
|
if (fullpath.length === 0) {
|
|
5697
7798
|
fieldErrors._errors.push(mapper(issue2));
|
|
5698
7799
|
} else {
|
|
@@ -5719,17 +7820,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
|
|
|
5719
7820
|
}
|
|
5720
7821
|
function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
5721
7822
|
const result = { errors: [] };
|
|
5722
|
-
const processError = (error52,
|
|
7823
|
+
const processError = (error52, path18 = []) => {
|
|
5723
7824
|
var _a3, _b;
|
|
5724
7825
|
for (const issue2 of error52.issues) {
|
|
5725
7826
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
5726
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
7827
|
+
issue2.errors.map((issues) => processError({ issues }, [...path18, ...issue2.path]));
|
|
5727
7828
|
} else if (issue2.code === "invalid_key") {
|
|
5728
|
-
processError({ issues: issue2.issues }, [...
|
|
7829
|
+
processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
|
|
5729
7830
|
} else if (issue2.code === "invalid_element") {
|
|
5730
|
-
processError({ issues: issue2.issues }, [...
|
|
7831
|
+
processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
|
|
5731
7832
|
} else {
|
|
5732
|
-
const fullpath = [...
|
|
7833
|
+
const fullpath = [...path18, ...issue2.path];
|
|
5733
7834
|
if (fullpath.length === 0) {
|
|
5734
7835
|
result.errors.push(mapper(issue2));
|
|
5735
7836
|
continue;
|
|
@@ -5761,8 +7862,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
|
|
|
5761
7862
|
}
|
|
5762
7863
|
function toDotPath(_path) {
|
|
5763
7864
|
const segs = [];
|
|
5764
|
-
const
|
|
5765
|
-
for (const seg of
|
|
7865
|
+
const path18 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
7866
|
+
for (const seg of path18) {
|
|
5766
7867
|
if (typeof seg === "number")
|
|
5767
7868
|
segs.push(`[${seg}]`);
|
|
5768
7869
|
else if (typeof seg === "symbol")
|
|
@@ -18454,13 +20555,13 @@ function resolveRef(ref, ctx) {
|
|
|
18454
20555
|
if (!ref.startsWith("#")) {
|
|
18455
20556
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
18456
20557
|
}
|
|
18457
|
-
const
|
|
18458
|
-
if (
|
|
20558
|
+
const path18 = ref.slice(1).split("/").filter(Boolean);
|
|
20559
|
+
if (path18.length === 0) {
|
|
18459
20560
|
return ctx.rootSchema;
|
|
18460
20561
|
}
|
|
18461
20562
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
18462
|
-
if (
|
|
18463
|
-
const key =
|
|
20563
|
+
if (path18[0] === defsKey) {
|
|
20564
|
+
const key = path18[1];
|
|
18464
20565
|
if (!key || !ctx.defs[key]) {
|
|
18465
20566
|
throw new Error(`Reference not found: ${ref}`);
|
|
18466
20567
|
}
|
|
@@ -19073,8 +21174,83 @@ var grepContentTool = {
|
|
|
19073
21174
|
}
|
|
19074
21175
|
};
|
|
19075
21176
|
|
|
19076
|
-
// src/
|
|
21177
|
+
// src/main/core/tools/builtin/listFiles.ts
|
|
21178
|
+
import { promises as fs11 } from "node:fs";
|
|
19077
21179
|
import path10 from "node:path";
|
|
21180
|
+
var ListFilesArgsSchema = external_exports.object({
|
|
21181
|
+
/** Directory to list (relative to cwd or absolute). Defaults to cwd. */
|
|
21182
|
+
path: external_exports.string().optional(),
|
|
21183
|
+
/** Max traversal depth. 1 = immediate children only (default). */
|
|
21184
|
+
maxDepth: external_exports.number().int().positive().max(10).default(1),
|
|
21185
|
+
/** Glob-style patterns to exclude (matched against each entry name). */
|
|
21186
|
+
exclude: external_exports.array(external_exports.string()).default([
|
|
21187
|
+
"node_modules",
|
|
21188
|
+
".git",
|
|
21189
|
+
"dist",
|
|
21190
|
+
"build",
|
|
21191
|
+
".next",
|
|
21192
|
+
"__pycache__",
|
|
21193
|
+
".cache"
|
|
21194
|
+
])
|
|
21195
|
+
});
|
|
21196
|
+
function matchesAny(name, patterns) {
|
|
21197
|
+
return patterns.some((p3) => {
|
|
21198
|
+
if (p3.includes("*")) {
|
|
21199
|
+
const regex = new RegExp("^" + p3.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
|
|
21200
|
+
return regex.test(name);
|
|
21201
|
+
}
|
|
21202
|
+
return name === p3;
|
|
21203
|
+
});
|
|
21204
|
+
}
|
|
21205
|
+
async function walk(dir, baseRel, depth, maxDepth, exclude, entries, signal) {
|
|
21206
|
+
if (depth > maxDepth) return;
|
|
21207
|
+
let dirents;
|
|
21208
|
+
try {
|
|
21209
|
+
dirents = await fs11.readdir(dir, { withFileTypes: true });
|
|
21210
|
+
} catch {
|
|
21211
|
+
return;
|
|
21212
|
+
}
|
|
21213
|
+
if (signal?.aborted) return;
|
|
21214
|
+
for (const dirent of dirents) {
|
|
21215
|
+
if (matchesAny(dirent.name, exclude)) continue;
|
|
21216
|
+
const rel = baseRel ? `${baseRel}/${dirent.name}` : dirent.name;
|
|
21217
|
+
const isDir = dirent.isDirectory();
|
|
21218
|
+
entries.push({
|
|
21219
|
+
name: rel,
|
|
21220
|
+
type: isDir ? "directory" : dirent.isFile() ? "file" : "other"
|
|
21221
|
+
});
|
|
21222
|
+
if (isDir && depth < maxDepth) {
|
|
21223
|
+
await walk(path10.join(dir, dirent.name), rel, depth + 1, maxDepth, exclude, entries, signal);
|
|
21224
|
+
}
|
|
21225
|
+
}
|
|
21226
|
+
}
|
|
21227
|
+
var listFilesTool = {
|
|
21228
|
+
name: "list_files",
|
|
21229
|
+
description: "List files and directories in the given path (defaults to the working directory). Returns names with types (file/directory). Use this to discover the project structure before reading specific files. Supports a maxDepth for recursive listing and excludes common dependency/build directories by default.",
|
|
21230
|
+
permissions: ["read"],
|
|
21231
|
+
timeoutMs: 15e3,
|
|
21232
|
+
inputSchema: ListFilesArgsSchema,
|
|
21233
|
+
execute: async (args, ctx) => {
|
|
21234
|
+
try {
|
|
21235
|
+
const target = args.path ? path10.isAbsolute(args.path) ? args.path : path10.join(ctx.cwd, args.path) : ctx.cwd;
|
|
21236
|
+
const entries = [];
|
|
21237
|
+
await walk(target, "", 1, args.maxDepth, args.exclude, entries, ctx.signal);
|
|
21238
|
+
entries.sort((a, b) => {
|
|
21239
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
21240
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
21241
|
+
return a.name.localeCompare(b.name);
|
|
21242
|
+
});
|
|
21243
|
+
const MAX_ENTRIES = 500;
|
|
21244
|
+
const truncated = entries.length > MAX_ENTRIES;
|
|
21245
|
+
return typedOk({ dir: target, entries: truncated ? entries.slice(0, MAX_ENTRIES) : entries, truncated });
|
|
21246
|
+
} catch (err) {
|
|
21247
|
+
return typedErr(err instanceof Error ? err.message : String(err));
|
|
21248
|
+
}
|
|
21249
|
+
}
|
|
21250
|
+
};
|
|
21251
|
+
|
|
21252
|
+
// src/cli/safety/sandboxPath.ts
|
|
21253
|
+
import path11 from "node:path";
|
|
19078
21254
|
var SandboxViolationError = class extends Error {
|
|
19079
21255
|
constructor(message, attemptedPath, resolvedPath) {
|
|
19080
21256
|
super(message);
|
|
@@ -19087,9 +21263,9 @@ function resolveSandboxedPath(userPath, options = {}) {
|
|
|
19087
21263
|
if (typeof userPath !== "string" || userPath.length === 0) {
|
|
19088
21264
|
throw new SandboxViolationError("Empty path", userPath, "");
|
|
19089
21265
|
}
|
|
19090
|
-
const root =
|
|
19091
|
-
const resolved =
|
|
19092
|
-
const rootWithSep = root.endsWith(
|
|
21266
|
+
const root = path11.resolve(options.root ?? process.cwd());
|
|
21267
|
+
const resolved = path11.isAbsolute(userPath) ? path11.resolve(userPath) : path11.resolve(root, userPath);
|
|
21268
|
+
const rootWithSep = root.endsWith(path11.sep) ? root : root + path11.sep;
|
|
19093
21269
|
if (resolved !== root && !resolved.startsWith(rootWithSep)) {
|
|
19094
21270
|
throw new SandboxViolationError(
|
|
19095
21271
|
`Path escapes sandbox root: ${userPath} \u2192 ${resolved} (root: ${root})`,
|
|
@@ -19152,8 +21328,8 @@ function assertShellAllowed(command) {
|
|
|
19152
21328
|
}
|
|
19153
21329
|
|
|
19154
21330
|
// src/cli/safety/auditLogger.ts
|
|
19155
|
-
import { promises as
|
|
19156
|
-
import
|
|
21331
|
+
import { promises as fs12 } from "node:fs";
|
|
21332
|
+
import path12 from "node:path";
|
|
19157
21333
|
import os7 from "node:os";
|
|
19158
21334
|
var AuditLogger = class {
|
|
19159
21335
|
logPath;
|
|
@@ -19172,8 +21348,8 @@ var AuditLogger = class {
|
|
|
19172
21348
|
async append(entry) {
|
|
19173
21349
|
const line = JSON.stringify(entry) + "\n";
|
|
19174
21350
|
this.writeQueue = this.writeQueue.then(async () => {
|
|
19175
|
-
await
|
|
19176
|
-
await
|
|
21351
|
+
await fs12.mkdir(path12.dirname(this.logPath), { recursive: true });
|
|
21352
|
+
await fs12.appendFile(this.logPath, line, "utf-8");
|
|
19177
21353
|
});
|
|
19178
21354
|
return this.writeQueue;
|
|
19179
21355
|
}
|
|
@@ -19216,7 +21392,7 @@ var AuditLogger = class {
|
|
|
19216
21392
|
function defaultAuditPath() {
|
|
19217
21393
|
const override = process.env.ANATHEMA_AUDIT_LOG;
|
|
19218
21394
|
if (override && override.trim().length > 0) return override;
|
|
19219
|
-
return
|
|
21395
|
+
return path12.join(os7.tmpdir(), "zelari-code", "audit.jsonl");
|
|
19220
21396
|
}
|
|
19221
21397
|
function redactArgs(args) {
|
|
19222
21398
|
const redacted = {};
|
|
@@ -19248,6 +21424,7 @@ function createBuiltinToolRegistry(options = {}) {
|
|
|
19248
21424
|
const safeWriteFile = wrapWithSandbox(writeFileTool, ["path"], root, audit, sessionId);
|
|
19249
21425
|
const safeEditFile = wrapWithSandbox(editFileTool, ["path"], root, audit, sessionId);
|
|
19250
21426
|
const safeGrepContent = wrapWithSandbox(grepContentTool, ["path"], root, audit, sessionId);
|
|
21427
|
+
const safeListFiles = wrapWithSandbox(listFilesTool, ["path"], root, audit, sessionId);
|
|
19251
21428
|
const safeBash = wrapWithShellSafety(bashTool, audit, sessionId);
|
|
19252
21429
|
const registry3 = new ToolRegistry();
|
|
19253
21430
|
registry3.register(safeReadFile);
|
|
@@ -19255,12 +21432,14 @@ function createBuiltinToolRegistry(options = {}) {
|
|
|
19255
21432
|
registry3.register(safeEditFile);
|
|
19256
21433
|
registry3.register(safeBash);
|
|
19257
21434
|
registry3.register(safeGrepContent);
|
|
21435
|
+
registry3.register(safeListFiles);
|
|
19258
21436
|
const tools = [
|
|
19259
21437
|
safeReadFile,
|
|
19260
21438
|
safeWriteFile,
|
|
19261
21439
|
safeEditFile,
|
|
19262
21440
|
safeBash,
|
|
19263
|
-
safeGrepContent
|
|
21441
|
+
safeGrepContent,
|
|
21442
|
+
safeListFiles
|
|
19264
21443
|
].map((t) => ({
|
|
19265
21444
|
name: t.name,
|
|
19266
21445
|
description: t.description,
|
|
@@ -19377,7 +21556,7 @@ function redactForAudit(args) {
|
|
|
19377
21556
|
// src/cli/gitOps.ts
|
|
19378
21557
|
import { execFile } from "node:child_process";
|
|
19379
21558
|
import { promisify } from "node:util";
|
|
19380
|
-
import
|
|
21559
|
+
import path13 from "node:path";
|
|
19381
21560
|
var execFileAsync = promisify(execFile);
|
|
19382
21561
|
async function git(cwd, args) {
|
|
19383
21562
|
try {
|
|
@@ -19423,7 +21602,7 @@ async function undoWorkingChanges(opts = {}) {
|
|
|
19423
21602
|
};
|
|
19424
21603
|
}
|
|
19425
21604
|
function defaultProjectRoot() {
|
|
19426
|
-
return
|
|
21605
|
+
return path13.resolve(__dirname, "..", "..", "..");
|
|
19427
21606
|
}
|
|
19428
21607
|
|
|
19429
21608
|
// src/cli/compaction.ts
|
|
@@ -19464,16 +21643,16 @@ function formatCompactionSummary(result) {
|
|
|
19464
21643
|
}
|
|
19465
21644
|
|
|
19466
21645
|
// src/cli/metrics.ts
|
|
19467
|
-
import { promises as
|
|
19468
|
-
import
|
|
21646
|
+
import { promises as fs13, existsSync as existsSync7, statSync as statSync3, renameSync, appendFileSync, mkdirSync as mkdirSync7 } from "node:fs";
|
|
21647
|
+
import path14 from "node:path";
|
|
19469
21648
|
import os8 from "node:os";
|
|
19470
21649
|
var METRICS_ROTATE_BYTES = 10 * 1024 * 1024;
|
|
19471
21650
|
var MetricsLogger = class {
|
|
19472
21651
|
file;
|
|
19473
21652
|
writeQueue = Promise.resolve();
|
|
19474
21653
|
constructor(file2) {
|
|
19475
|
-
this.file = file2 ?? process.env.ANATHEMA_METRICS_FILE ??
|
|
19476
|
-
mkdirSync7(
|
|
21654
|
+
this.file = file2 ?? process.env.ANATHEMA_METRICS_FILE ?? path14.join(os8.homedir(), ".tmp", "zelari-code", "metrics.jsonl");
|
|
21655
|
+
mkdirSync7(path14.dirname(this.file), { recursive: true });
|
|
19477
21656
|
}
|
|
19478
21657
|
/** Fire-and-forget record append. */
|
|
19479
21658
|
record(rec) {
|
|
@@ -19524,8 +21703,8 @@ function getMetricsLogger() {
|
|
|
19524
21703
|
}
|
|
19525
21704
|
|
|
19526
21705
|
// src/cli/skillHistory.ts
|
|
19527
|
-
import { promises as
|
|
19528
|
-
import
|
|
21706
|
+
import { promises as fs14, existsSync as existsSync8, statSync as statSync4, renameSync as renameSync2, appendFileSync as appendFileSync2, mkdirSync as mkdirSync8 } from "node:fs";
|
|
21707
|
+
import path15 from "node:path";
|
|
19529
21708
|
import os9 from "node:os";
|
|
19530
21709
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
19531
21710
|
var SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024;
|
|
@@ -19534,8 +21713,8 @@ var SkillHistoryLogger = class {
|
|
|
19534
21713
|
writeQueue = Promise.resolve();
|
|
19535
21714
|
inflight = /* @__PURE__ */ new Map();
|
|
19536
21715
|
constructor(file2) {
|
|
19537
|
-
this.file = file2 ?? process.env.ANATHEMA_SKILL_HISTORY_FILE ??
|
|
19538
|
-
mkdirSync8(
|
|
21716
|
+
this.file = file2 ?? process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path15.join(os9.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
|
|
21717
|
+
mkdirSync8(path15.dirname(this.file), { recursive: true });
|
|
19539
21718
|
}
|
|
19540
21719
|
/**
|
|
19541
21720
|
* Mark the start of a skill invocation. Returns a unique invocationId
|
|
@@ -19613,7 +21792,7 @@ var SkillHistoryLogger = class {
|
|
|
19613
21792
|
async function readSkillHistory(file2) {
|
|
19614
21793
|
let raw = "";
|
|
19615
21794
|
try {
|
|
19616
|
-
raw = await
|
|
21795
|
+
raw = await fs14.readFile(file2, "utf-8");
|
|
19617
21796
|
} catch {
|
|
19618
21797
|
return [];
|
|
19619
21798
|
}
|
|
@@ -19649,7 +21828,7 @@ function getSkillStats(records, skillId, sinceTs) {
|
|
|
19649
21828
|
}
|
|
19650
21829
|
|
|
19651
21830
|
// src/cli/app.tsx
|
|
19652
|
-
import
|
|
21831
|
+
import path17 from "node:path";
|
|
19653
21832
|
import os10 from "node:os";
|
|
19654
21833
|
var MODEL = process.env.OPENAI_MODEL ?? "grok-4";
|
|
19655
21834
|
var PROVIDER = "openai-compatible";
|
|
@@ -19887,13 +22066,42 @@ function App() {
|
|
|
19887
22066
|
primary: baseProviderStream,
|
|
19888
22067
|
fallback: failoverResolution.fallback
|
|
19889
22068
|
});
|
|
22069
|
+
const cwd = process.cwd();
|
|
22070
|
+
const toolList = toolRegistry.toOpenAITools().map((t) => `- ${t.function.name}: ${t.function.description}`).join("\n");
|
|
22071
|
+
const systemPrompt = [
|
|
22072
|
+
"You are Zelari Code, an interactive AI coding agent operating directly in the user's terminal.",
|
|
22073
|
+
"",
|
|
22074
|
+
"You ARE connected to this machine and have real tools to read, modify, and explore the codebase.",
|
|
22075
|
+
"Never claim you lack filesystem or shell access \u2014 you have it. Use your tools instead of asking the user to paste file contents.",
|
|
22076
|
+
"",
|
|
22077
|
+
`# Working Directory`,
|
|
22078
|
+
`You are running in: ${cwd}`,
|
|
22079
|
+
"All relative file paths are resolved against this directory. Always work with real files here.",
|
|
22080
|
+
"",
|
|
22081
|
+
"# Available Tools",
|
|
22082
|
+
"You can call these tools. Use them to take action and gather information autonomously:",
|
|
22083
|
+
toolList,
|
|
22084
|
+
"",
|
|
22085
|
+
"# Guidelines",
|
|
22086
|
+
"- To understand a project, list files (bash: ls) and read key files (read_file), don't ask the user to paste them.",
|
|
22087
|
+
"- Be proactive: explore, read, and act. Prefer doing over asking.",
|
|
22088
|
+
"- When you finish a task, briefly summarize what you did."
|
|
22089
|
+
].join("\n");
|
|
19890
22090
|
const harness = new AgentHarness({
|
|
19891
22091
|
model: envConfig.model,
|
|
19892
22092
|
provider: PROVIDER,
|
|
19893
|
-
messages: [
|
|
19894
|
-
|
|
22093
|
+
messages: [
|
|
22094
|
+
{ role: "system", content: systemPrompt },
|
|
22095
|
+
{ role: "user", content: userText }
|
|
22096
|
+
],
|
|
22097
|
+
tools: toolRegistry.toOpenAITools().map((t) => ({
|
|
22098
|
+
name: t.function.name,
|
|
22099
|
+
description: t.function.description,
|
|
22100
|
+
parameters: t.function.parameters
|
|
22101
|
+
})),
|
|
19895
22102
|
toolRegistry,
|
|
19896
|
-
providerStream
|
|
22103
|
+
providerStream,
|
|
22104
|
+
cwd
|
|
19897
22105
|
});
|
|
19898
22106
|
harnessRef.current = harness;
|
|
19899
22107
|
setQueueCount(harness.queueLength);
|
|
@@ -20418,9 +22626,9 @@ ${lines.join("\n")}`;
|
|
|
20418
22626
|
if (result.kind === "promote_member" && result.promoteMemberId) {
|
|
20419
22627
|
try {
|
|
20420
22628
|
const { skill, markdown } = promoteMember(result.promoteMemberId);
|
|
20421
|
-
const skillDir = process.env.ANATHEMA_SKILL_DIR ??
|
|
22629
|
+
const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path17.join(os10.homedir(), ".tmp", "zelari-code", "skills");
|
|
20422
22630
|
await fs.promises.mkdir(skillDir, { recursive: true });
|
|
20423
|
-
const filePath =
|
|
22631
|
+
const filePath = path17.join(skillDir, `${skill.id}.md`);
|
|
20424
22632
|
await fs.promises.writeFile(filePath, markdown, "utf8");
|
|
20425
22633
|
setMessages((prev) => [
|
|
20426
22634
|
...prev,
|
|
@@ -21150,7 +23358,7 @@ ${lines.join("\n")}`,
|
|
|
21150
23358
|
return;
|
|
21151
23359
|
}
|
|
21152
23360
|
if (result.kind === "skill_stats") {
|
|
21153
|
-
const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ??
|
|
23361
|
+
const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path17.join(os10.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
|
|
21154
23362
|
try {
|
|
21155
23363
|
const records = await readSkillHistory(historyFile);
|
|
21156
23364
|
const stats = getSkillStats(records, result.skillStatsSkillId);
|
|
@@ -21179,7 +23387,7 @@ ${lines.join("\n")}`,
|
|
|
21179
23387
|
setInput("");
|
|
21180
23388
|
return;
|
|
21181
23389
|
}
|
|
21182
|
-
const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ??
|
|
23390
|
+
const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path17.join(os10.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
|
|
21183
23391
|
try {
|
|
21184
23392
|
const formatted = await compareSkillsFromFile(ids[0], ids[1], historyFile);
|
|
21185
23393
|
setMessages((prev) => [
|
|
@@ -21235,7 +23443,7 @@ ${lines.join("\n")}`,
|
|
|
21235
23443
|
}
|
|
21236
23444
|
|
|
21237
23445
|
// src/cli/main.ts
|
|
21238
|
-
var VERSION = "0.1
|
|
23446
|
+
var VERSION = "0.2.1";
|
|
21239
23447
|
async function backgroundUpdateCheck() {
|
|
21240
23448
|
if (process.env.ANATHEMA_DEV === "1") return;
|
|
21241
23449
|
await new Promise((resolve) => setTimeout(resolve, 3e3));
|