kairn-cli 2.4.2 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +499 -74
- package/dist/cli.js +442 -2
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1464,6 +1464,361 @@ async function callLLM(config, userMessage, options) {
|
|
|
1464
1464
|
}
|
|
1465
1465
|
}
|
|
1466
1466
|
|
|
1467
|
+
// src/intent/patterns.ts
|
|
1468
|
+
var SYNONYM_MAP = {
|
|
1469
|
+
deploy: ["ship", "push\\s+to\\s+prod", "release", "publish"],
|
|
1470
|
+
test: ["run\\s+tests", "check", "verify", "run\\s+test\\s+suite"],
|
|
1471
|
+
lint: ["format", "style\\s+check", "linting"],
|
|
1472
|
+
build: ["compile", "bundle", "make"],
|
|
1473
|
+
dev: ["develop", "start\\s+dev", "run\\s+dev"],
|
|
1474
|
+
start: ["run", "launch", "serve"],
|
|
1475
|
+
migrate: ["migration", "schema\\s+change", "db\\s+update"],
|
|
1476
|
+
seed: ["populate", "seed\\s+data", "load\\s+fixtures"],
|
|
1477
|
+
clean: ["purge", "clear", "reset\\s+cache"],
|
|
1478
|
+
docs: ["document", "documentation", "write\\s+docs"],
|
|
1479
|
+
review: ["code\\s+review", "pr\\s+review", "check\\s+code"],
|
|
1480
|
+
commit: ["save\\s+changes", "check\\s+in", "git\\s+commit"],
|
|
1481
|
+
fix: ["repair", "patch", "debug", "resolve"],
|
|
1482
|
+
refactor: ["restructure", "reorganize", "clean\\s+up"],
|
|
1483
|
+
plan: ["design", "architect", "outline"],
|
|
1484
|
+
status: ["progress", "overview", "summary"]
|
|
1485
|
+
};
|
|
1486
|
+
function extractDescription(content) {
|
|
1487
|
+
const lines = content.split("\n");
|
|
1488
|
+
for (const line of lines) {
|
|
1489
|
+
const trimmed = line.trim();
|
|
1490
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
1491
|
+
return trimmed;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
return "";
|
|
1495
|
+
}
|
|
1496
|
+
function extractVerb(commandName) {
|
|
1497
|
+
const parts = commandName.split("-");
|
|
1498
|
+
if (parts.length > 1) {
|
|
1499
|
+
return parts[parts.length - 1];
|
|
1500
|
+
}
|
|
1501
|
+
return parts[0];
|
|
1502
|
+
}
|
|
1503
|
+
function buildPatternAlternation(commandName) {
|
|
1504
|
+
const verb = extractVerb(commandName);
|
|
1505
|
+
const alternatives = [verb];
|
|
1506
|
+
const synonyms = SYNONYM_MAP[verb];
|
|
1507
|
+
if (synonyms) {
|
|
1508
|
+
alternatives.push(...synonyms);
|
|
1509
|
+
}
|
|
1510
|
+
if (commandName !== verb && !alternatives.includes(commandName)) {
|
|
1511
|
+
alternatives.push(commandName.replace(/-/g, "[\\s-]"));
|
|
1512
|
+
}
|
|
1513
|
+
return `\\b(${alternatives.join("|")})\\b`;
|
|
1514
|
+
}
|
|
1515
|
+
function generateScriptPatterns(scripts, existingCommands) {
|
|
1516
|
+
const patterns = [];
|
|
1517
|
+
for (const scriptName of Object.keys(scripts)) {
|
|
1518
|
+
const verb = extractVerb(scriptName);
|
|
1519
|
+
if (existingCommands.has(verb) || existingCommands.has(scriptName)) {
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
const parts = scriptName.split(":");
|
|
1523
|
+
if (parts.length > 1) {
|
|
1524
|
+
const suffix = parts[parts.length - 1];
|
|
1525
|
+
const alternatives = [suffix];
|
|
1526
|
+
if (suffix === "e2e") {
|
|
1527
|
+
alternatives.push("end[\\s.-]to[\\s.-]end");
|
|
1528
|
+
}
|
|
1529
|
+
patterns.push({
|
|
1530
|
+
pattern: `\\b(${alternatives.join("|")})\\b`,
|
|
1531
|
+
command: `/project:${scriptName}`,
|
|
1532
|
+
description: `Run npm script: ${scriptName}`,
|
|
1533
|
+
source: "generated"
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return patterns;
|
|
1538
|
+
}
|
|
1539
|
+
function generateIntentPatterns(commands, agents, projectProfile) {
|
|
1540
|
+
const patterns = [];
|
|
1541
|
+
const commandNames = new Set(Object.keys(commands));
|
|
1542
|
+
for (const [name, content] of Object.entries(commands)) {
|
|
1543
|
+
const description = extractDescription(content);
|
|
1544
|
+
const patternStr = buildPatternAlternation(name);
|
|
1545
|
+
patterns.push({
|
|
1546
|
+
pattern: patternStr,
|
|
1547
|
+
command: `/project:${name}`,
|
|
1548
|
+
description: description || `Run ${name} workflow`,
|
|
1549
|
+
source: "generated"
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
const scriptPatterns = generateScriptPatterns(
|
|
1553
|
+
projectProfile.scripts,
|
|
1554
|
+
commandNames
|
|
1555
|
+
);
|
|
1556
|
+
patterns.push(...scriptPatterns);
|
|
1557
|
+
patterns.sort((a, b) => b.pattern.length - a.pattern.length);
|
|
1558
|
+
return patterns;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/intent/prompt-template.ts
|
|
1562
|
+
function extractFirstLine(content) {
|
|
1563
|
+
const lines = content.split("\n");
|
|
1564
|
+
for (const line of lines) {
|
|
1565
|
+
const trimmed = line.trim();
|
|
1566
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
1567
|
+
return trimmed;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return "";
|
|
1571
|
+
}
|
|
1572
|
+
function compileIntentPrompt(commands, agents) {
|
|
1573
|
+
const workflowLines = [];
|
|
1574
|
+
for (const [name, content] of Object.entries(commands)) {
|
|
1575
|
+
const desc = extractFirstLine(content);
|
|
1576
|
+
workflowLines.push(`- /project:${name} \u2014 ${desc}`);
|
|
1577
|
+
}
|
|
1578
|
+
const workflowManifest = workflowLines.length > 0 ? workflowLines.join("\n") : "(no workflows defined)";
|
|
1579
|
+
const agentLines = [];
|
|
1580
|
+
for (const [name, content] of Object.entries(agents)) {
|
|
1581
|
+
const desc = extractFirstLine(content);
|
|
1582
|
+
agentLines.push(`- @${name} \u2014 ${desc}`);
|
|
1583
|
+
}
|
|
1584
|
+
const agentManifest = agentLines.length > 0 ? agentLines.join("\n") : "(no agents defined)";
|
|
1585
|
+
return `You are an intent classifier for a software project. The user said something that didn't match any known command keyword.
|
|
1586
|
+
|
|
1587
|
+
Available workflows:
|
|
1588
|
+
${workflowManifest}
|
|
1589
|
+
|
|
1590
|
+
Available agents:
|
|
1591
|
+
${agentManifest}
|
|
1592
|
+
|
|
1593
|
+
User input: $PROMPT
|
|
1594
|
+
|
|
1595
|
+
If this maps to one or more workflows, return JSON:
|
|
1596
|
+
{"additionalContext": "[INTENT ROUTED] Based on your request, use: /project:<command> \u2014 <description>"}
|
|
1597
|
+
|
|
1598
|
+
If the user is asking a question or making a statement that doesn't need a workflow, return:
|
|
1599
|
+
{"ok": true}
|
|
1600
|
+
|
|
1601
|
+
Do not activate workflows for questions like 'what does deploy do?' or 'how do I test?'. Only activate for action requests.`;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// src/intent/router-template.ts
|
|
1605
|
+
function renderPatternEntry(p) {
|
|
1606
|
+
const escapedPattern = p.pattern.replace(/\\/g, "\\\\");
|
|
1607
|
+
const escapedDesc = p.description.replace(/'/g, "\\'");
|
|
1608
|
+
const escapedCmd = p.command.replace(/'/g, "\\'");
|
|
1609
|
+
return ` { pattern: /${p.pattern}/i,
|
|
1610
|
+
command: '${escapedCmd}',
|
|
1611
|
+
description: '${escapedDesc}' }`;
|
|
1612
|
+
}
|
|
1613
|
+
function renderIntentRouter(patterns, generationTimestamp) {
|
|
1614
|
+
const patternEntries = patterns.map((p) => renderPatternEntry(p)).join(",\n");
|
|
1615
|
+
return `#!/usr/bin/env node
|
|
1616
|
+
/**
|
|
1617
|
+
* Kairn Intent Router \u2014 Tier 1 (Regex)
|
|
1618
|
+
* Generated by kairn describe. Project-specific keyword patterns.
|
|
1619
|
+
*
|
|
1620
|
+
* If a keyword matches, injects a system-reminder pointing Claude
|
|
1621
|
+
* to the right command/workflow. If no match, falls through to
|
|
1622
|
+
* the Tier 2 prompt hook.
|
|
1623
|
+
*
|
|
1624
|
+
* Last updated: ${generationTimestamp}
|
|
1625
|
+
*/
|
|
1626
|
+
|
|
1627
|
+
import { readFileSync } from 'fs';
|
|
1628
|
+
|
|
1629
|
+
// Read prompt from stdin (Claude Code hooks API)
|
|
1630
|
+
const input = readFileSync('/dev/stdin', 'utf-8');
|
|
1631
|
+
let prompt = '';
|
|
1632
|
+
try {
|
|
1633
|
+
const data = JSON.parse(input);
|
|
1634
|
+
prompt = data.prompt || '';
|
|
1635
|
+
} catch { process.exit(0); }
|
|
1636
|
+
|
|
1637
|
+
if (!prompt.trim()) {
|
|
1638
|
+
console.log(JSON.stringify({ "continue": true, suppressOutput: true }));
|
|
1639
|
+
process.exit(0);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// Sanitize: strip code blocks, URLs, file paths
|
|
1643
|
+
const clean = prompt
|
|
1644
|
+
.replace(/\`\`\`[\\s\\S]*?\`\`\`/g, '')
|
|
1645
|
+
.replace(/\`[^\`]+\`/g, '')
|
|
1646
|
+
.replace(/https?:\\/\\/\\S+/g, '')
|
|
1647
|
+
.replace(/(\\/[\\w.-]+){2,}/g, '')
|
|
1648
|
+
.toLowerCase();
|
|
1649
|
+
|
|
1650
|
+
// Question filter: don't trigger on informational queries
|
|
1651
|
+
const QUESTION_PATTERNS = [
|
|
1652
|
+
/\\b(?:what(?:'s|\\s+is)|how\\s+(?:to|do\\s+i)\\s+use|explain|describe|tell\\s+me\\s+about)\\b/i,
|
|
1653
|
+
];
|
|
1654
|
+
function isQuestion(text, position, length) {
|
|
1655
|
+
const start = Math.max(0, position - 80);
|
|
1656
|
+
const end = Math.min(text.length, position + length + 80);
|
|
1657
|
+
const ctx = text.slice(start, end);
|
|
1658
|
+
return QUESTION_PATTERNS.some(p => p.test(ctx));
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1662
|
+
// PROJECT-SPECIFIC KEYWORD PATTERNS
|
|
1663
|
+
// Generated from: kairn describe analysis
|
|
1664
|
+
// Last updated: ${generationTimestamp}
|
|
1665
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
1666
|
+
|
|
1667
|
+
const PATTERNS = [
|
|
1668
|
+
${patternEntries}
|
|
1669
|
+
];
|
|
1670
|
+
|
|
1671
|
+
// Match against patterns
|
|
1672
|
+
for (const { pattern, command, description } of PATTERNS) {
|
|
1673
|
+
const match = clean.match(pattern);
|
|
1674
|
+
if (match && !isQuestion(clean, match.index, match[0].length)) {
|
|
1675
|
+
const output = {
|
|
1676
|
+
"continue": true,
|
|
1677
|
+
hookSpecificOutput: {
|
|
1678
|
+
hookEventName: 'UserPromptSubmit',
|
|
1679
|
+
additionalContext: \`[INTENT ROUTED] Based on your request, use: \${command} \u2014 \${description}\\n\\nUser's original request: \${prompt}\\n\\nExecute the command above to fulfill the user's intent.\`
|
|
1680
|
+
}
|
|
1681
|
+
};
|
|
1682
|
+
console.log(JSON.stringify(output));
|
|
1683
|
+
process.exit(0);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// No match \u2014 fall through to Tier 2 (prompt hook)
|
|
1688
|
+
console.log(JSON.stringify({ "continue": true, suppressOutput: true }));
|
|
1689
|
+
`;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// src/intent/learner-template.ts
|
|
1693
|
+
function renderIntentLearner() {
|
|
1694
|
+
return `#!/usr/bin/env node
|
|
1695
|
+
/**
|
|
1696
|
+
* Kairn Intent Learner \u2014 Pattern Promotion (Tier 2 \u2192 Tier 1)
|
|
1697
|
+
* Runs on SessionStart to promote recurring Tier 2 patterns to Tier 1 regexes.
|
|
1698
|
+
* Generated by kairn describe.
|
|
1699
|
+
*/
|
|
1700
|
+
|
|
1701
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync } from 'fs';
|
|
1702
|
+
import { join, dirname } from 'path';
|
|
1703
|
+
|
|
1704
|
+
const hooksDir = join(dirname(new URL(import.meta.url).pathname));
|
|
1705
|
+
const logPath = join(hooksDir, 'intent-log.jsonl');
|
|
1706
|
+
const routerPath = join(hooksDir, 'intent-router.mjs');
|
|
1707
|
+
const promotionsPath = join(hooksDir, 'intent-promotions.jsonl');
|
|
1708
|
+
|
|
1709
|
+
// Read log entries
|
|
1710
|
+
let entries = [];
|
|
1711
|
+
try {
|
|
1712
|
+
const raw = readFileSync(logPath, 'utf-8').trim();
|
|
1713
|
+
if (!raw) {
|
|
1714
|
+
console.log(JSON.stringify({ "continue": true, suppressOutput: true }));
|
|
1715
|
+
process.exit(0);
|
|
1716
|
+
}
|
|
1717
|
+
entries = raw.split('\\n').map(line => {
|
|
1718
|
+
try { return JSON.parse(line); }
|
|
1719
|
+
catch { return null; }
|
|
1720
|
+
}).filter(Boolean);
|
|
1721
|
+
} catch {
|
|
1722
|
+
// Log file doesn't exist or is unreadable \u2014 nothing to promote
|
|
1723
|
+
console.log(JSON.stringify({ "continue": true, suppressOutput: true }));
|
|
1724
|
+
process.exit(0);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
if (entries.length === 0) {
|
|
1728
|
+
console.log(JSON.stringify({ "continue": true, suppressOutput: true }));
|
|
1729
|
+
process.exit(0);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// Group by routed_to command
|
|
1733
|
+
const groups = {};
|
|
1734
|
+
const remaining = [];
|
|
1735
|
+
for (const entry of entries) {
|
|
1736
|
+
if (!entry.routed_to) { remaining.push(entry); continue; }
|
|
1737
|
+
if (!groups[entry.routed_to]) groups[entry.routed_to] = [];
|
|
1738
|
+
groups[entry.routed_to].push(entry);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// For each command with 3+ entries, extract common words and build regex
|
|
1742
|
+
const promoted = [];
|
|
1743
|
+
for (const [command, cmdEntries] of Object.entries(groups)) {
|
|
1744
|
+
if (cmdEntries.length < 3) {
|
|
1745
|
+
remaining.push(...cmdEntries);
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// Extract unique words from all prompts
|
|
1750
|
+
const wordFreq = {};
|
|
1751
|
+
for (const entry of cmdEntries) {
|
|
1752
|
+
const words = (entry.prompt || '').toLowerCase()
|
|
1753
|
+
.replace(/[^a-z\\s]/g, '')
|
|
1754
|
+
.split(/\\s+/)
|
|
1755
|
+
.filter(w => w.length > 2);
|
|
1756
|
+
const unique = [...new Set(words)];
|
|
1757
|
+
for (const word of unique) {
|
|
1758
|
+
wordFreq[word] = (wordFreq[word] || 0) + 1;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// Find words that appear in at least half the entries
|
|
1763
|
+
const threshold = Math.ceil(cmdEntries.length / 2);
|
|
1764
|
+
const commonWords = Object.entries(wordFreq)
|
|
1765
|
+
.filter(([, count]) => count >= threshold)
|
|
1766
|
+
.map(([word]) => word)
|
|
1767
|
+
.filter(w => !['the', 'and', 'for', 'this', 'that', 'can', 'you', 'please'].includes(w));
|
|
1768
|
+
|
|
1769
|
+
if (commonWords.length === 0) {
|
|
1770
|
+
remaining.push(...cmdEntries);
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Build regex pattern from common words
|
|
1775
|
+
const patternStr = '\\\\b(' + commonWords.join('|') + ')\\\\b';
|
|
1776
|
+
|
|
1777
|
+
// Read current router and check if pattern already exists
|
|
1778
|
+
try {
|
|
1779
|
+
const routerContent = readFileSync(routerPath, 'utf-8');
|
|
1780
|
+
if (commonWords.some(w => routerContent.includes(w + '|') || routerContent.includes('|' + w))) {
|
|
1781
|
+
remaining.push(...cmdEntries);
|
|
1782
|
+
continue;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// Append new pattern to PATTERNS array
|
|
1786
|
+
const newEntry = \` { pattern: /\\\\b(\${commonWords.join('|')})/i,\\n command: '\${command}',\\n description: 'Learned from usage patterns' },\`;
|
|
1787
|
+
const insertPoint = routerContent.lastIndexOf('];');
|
|
1788
|
+
if (insertPoint === -1) {
|
|
1789
|
+
remaining.push(...cmdEntries);
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
const updatedRouter = routerContent.slice(0, insertPoint) +
|
|
1794
|
+
newEntry + '\\n' +
|
|
1795
|
+
routerContent.slice(insertPoint);
|
|
1796
|
+
writeFileSync(routerPath, updatedRouter, 'utf-8');
|
|
1797
|
+
|
|
1798
|
+
// Record promotion
|
|
1799
|
+
const promotion = {
|
|
1800
|
+
timestamp: new Date().toISOString(),
|
|
1801
|
+
command,
|
|
1802
|
+
pattern: patternStr,
|
|
1803
|
+
sourcePrompts: cmdEntries.map(e => e.prompt),
|
|
1804
|
+
commonWords,
|
|
1805
|
+
};
|
|
1806
|
+
promoted.push(promotion);
|
|
1807
|
+
appendFileSync(promotionsPath, JSON.stringify(promotion) + '\\n');
|
|
1808
|
+
|
|
1809
|
+
} catch {
|
|
1810
|
+
remaining.push(...cmdEntries);
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// Write back remaining (unprocessed) entries
|
|
1816
|
+
writeFileSync(logPath, remaining.map(e => JSON.stringify(e)).join('\\n') + (remaining.length ? '\\n' : ''), 'utf-8');
|
|
1817
|
+
|
|
1818
|
+
console.log(JSON.stringify({ "continue": true, suppressOutput: true }));
|
|
1819
|
+
`;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1467
1822
|
// src/compiler/compile.ts
|
|
1468
1823
|
function buildSkeletonMessage(intent, registry) {
|
|
1469
1824
|
const registrySummary = registry.map(
|
|
@@ -1664,6 +2019,27 @@ async function compile(intent, onProgress) {
|
|
|
1664
2019
|
onProgress?.({ phase: "pass3", status: "running", message: "Pass 3: Configuring MCP servers & settings..." });
|
|
1665
2020
|
const settings = buildSettings(skeleton, registry);
|
|
1666
2021
|
const mcpConfig = buildMcpConfig(skeleton, registry);
|
|
2022
|
+
const projectProfile = {
|
|
2023
|
+
language: skeleton.outline.tech_stack[0] ?? "unknown",
|
|
2024
|
+
framework: skeleton.outline.tech_stack[1] ?? "none",
|
|
2025
|
+
scripts: {}
|
|
2026
|
+
// scripts come from project scanning, not compilation
|
|
2027
|
+
};
|
|
2028
|
+
const intentPatterns = generateIntentPatterns(
|
|
2029
|
+
harness.commands,
|
|
2030
|
+
harness.agents ?? {},
|
|
2031
|
+
projectProfile
|
|
2032
|
+
);
|
|
2033
|
+
const intentPromptTemplate = compileIntentPrompt(
|
|
2034
|
+
harness.commands,
|
|
2035
|
+
harness.agents ?? {}
|
|
2036
|
+
);
|
|
2037
|
+
const generationTimestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2038
|
+
const intentHooks = {};
|
|
2039
|
+
if (intentPatterns.length > 0) {
|
|
2040
|
+
intentHooks["intent-router"] = renderIntentRouter(intentPatterns, generationTimestamp);
|
|
2041
|
+
intentHooks["intent-learner"] = renderIntentLearner();
|
|
2042
|
+
}
|
|
1667
2043
|
onProgress?.({ phase: "pass3", status: "success", message: "Pass 3: Configured MCP servers & settings" });
|
|
1668
2044
|
const spec = {
|
|
1669
2045
|
id: `env_${crypto.randomUUID()}`,
|
|
@@ -1681,7 +2057,10 @@ async function compile(intent, onProgress) {
|
|
|
1681
2057
|
rules: harness.rules,
|
|
1682
2058
|
skills: harness.skills ?? {},
|
|
1683
2059
|
agents: harness.agents ?? {},
|
|
1684
|
-
docs: harness.docs
|
|
2060
|
+
docs: harness.docs,
|
|
2061
|
+
hooks: intentHooks,
|
|
2062
|
+
intent_patterns: intentPatterns,
|
|
2063
|
+
intent_prompt_template: intentPromptTemplate
|
|
1685
2064
|
}
|
|
1686
2065
|
};
|
|
1687
2066
|
const warnings = validateSpec(spec);
|
|
@@ -2146,6 +2525,41 @@ function resolveSettings(spec, options) {
|
|
|
2146
2525
|
hooks.SessionStart = sessionStart;
|
|
2147
2526
|
base.hooks = hooks;
|
|
2148
2527
|
}
|
|
2528
|
+
const hasIntentHooks = spec.harness.hooks && Object.keys(spec.harness.hooks).length > 0;
|
|
2529
|
+
if (hasIntentHooks) {
|
|
2530
|
+
const hooks = base.hooks ?? {};
|
|
2531
|
+
const userPromptSubmit = hooks.UserPromptSubmit ?? [];
|
|
2532
|
+
const intentHookEntry = {
|
|
2533
|
+
matcher: "*",
|
|
2534
|
+
hooks: [
|
|
2535
|
+
{
|
|
2536
|
+
type: "command",
|
|
2537
|
+
command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/intent-router.mjs"',
|
|
2538
|
+
timeout: 5
|
|
2539
|
+
}
|
|
2540
|
+
]
|
|
2541
|
+
};
|
|
2542
|
+
if (spec.harness.intent_prompt_template) {
|
|
2543
|
+
intentHookEntry.hooks.push({
|
|
2544
|
+
type: "prompt",
|
|
2545
|
+
prompt: spec.harness.intent_prompt_template,
|
|
2546
|
+
timeout: 15
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
userPromptSubmit.push(intentHookEntry);
|
|
2550
|
+
hooks.UserPromptSubmit = userPromptSubmit;
|
|
2551
|
+
const sessionStart = hooks.SessionStart ?? [];
|
|
2552
|
+
sessionStart.push({
|
|
2553
|
+
matcher: "*",
|
|
2554
|
+
hooks: [{
|
|
2555
|
+
type: "command",
|
|
2556
|
+
command: 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/intent-learner.mjs"',
|
|
2557
|
+
timeout: 10
|
|
2558
|
+
}]
|
|
2559
|
+
});
|
|
2560
|
+
hooks.SessionStart = sessionStart;
|
|
2561
|
+
base.hooks = hooks;
|
|
2562
|
+
}
|
|
2149
2563
|
if (Object.keys(base).length === 0) return null;
|
|
2150
2564
|
return base;
|
|
2151
2565
|
}
|
|
@@ -2194,6 +2608,14 @@ function buildFileMap(spec, options) {
|
|
|
2194
2608
|
files.set(`.claude/docs/${name}.md`, content);
|
|
2195
2609
|
}
|
|
2196
2610
|
}
|
|
2611
|
+
if (spec.harness.hooks) {
|
|
2612
|
+
for (const [name, content] of Object.entries(spec.harness.hooks)) {
|
|
2613
|
+
files.set(`.claude/hooks/${name}.mjs`, content);
|
|
2614
|
+
}
|
|
2615
|
+
if (Object.keys(spec.harness.hooks).length > 0) {
|
|
2616
|
+
files.set(".claude/hooks/intent-log.jsonl", "");
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2197
2619
|
return files;
|
|
2198
2620
|
}
|
|
2199
2621
|
async function writeEnvironment(spec, targetDir, options) {
|
|
@@ -2252,6 +2674,18 @@ async function writeEnvironment(spec, targetDir, options) {
|
|
|
2252
2674
|
written.push(`.claude/docs/${name}.md`);
|
|
2253
2675
|
}
|
|
2254
2676
|
}
|
|
2677
|
+
if (spec.harness.hooks) {
|
|
2678
|
+
for (const [name, content] of Object.entries(spec.harness.hooks)) {
|
|
2679
|
+
const p = path5.join(claudeDir, "hooks", `${name}.mjs`);
|
|
2680
|
+
await writeFile(p, content);
|
|
2681
|
+
written.push(`.claude/hooks/${name}.mjs`);
|
|
2682
|
+
}
|
|
2683
|
+
if (Object.keys(spec.harness.hooks).length > 0) {
|
|
2684
|
+
const logPath = path5.join(claudeDir, "hooks", "intent-log.jsonl");
|
|
2685
|
+
await writeFile(logPath, "");
|
|
2686
|
+
written.push(".claude/hooks/intent-log.jsonl");
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2255
2689
|
return written;
|
|
2256
2690
|
}
|
|
2257
2691
|
function summarizeSpec(spec, registry) {
|
|
@@ -3943,11 +4377,17 @@ var EVAL_TEMPLATES = {
|
|
|
3943
4377
|
name: "Rule Compliance",
|
|
3944
4378
|
description: "Does the agent follow all project rules without violations?",
|
|
3945
4379
|
bestFor: ["feature-development", "backend", "maintenance", "architecture"]
|
|
4380
|
+
},
|
|
4381
|
+
"intent-routing": {
|
|
4382
|
+
id: "intent-routing",
|
|
4383
|
+
name: "Intent Routing",
|
|
4384
|
+
description: "Test that natural language prompts route to the correct workflow command via intent hooks",
|
|
4385
|
+
bestFor: ["feature-development", "full-stack", "api-building"]
|
|
3946
4386
|
}
|
|
3947
4387
|
};
|
|
3948
4388
|
function selectTemplatesForWorkflow(workflowType) {
|
|
3949
4389
|
const mapping = {
|
|
3950
|
-
"feature-development": ["add-feature", "test-writing", "convention-adherence", "workflow-compliance"],
|
|
4390
|
+
"feature-development": ["add-feature", "test-writing", "convention-adherence", "workflow-compliance", "intent-routing"],
|
|
3951
4391
|
"api-building": ["add-feature", "fix-bug", "test-writing", "convention-adherence"],
|
|
3952
4392
|
"full-stack": ["add-feature", "fix-bug", "test-writing", "convention-adherence"],
|
|
3953
4393
|
"maintenance": ["fix-bug", "refactor", "test-writing", "rule-compliance"],
|