@thispointon/kondi-chat 0.1.2

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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +556 -0
  3. package/bin/kondi-chat +56 -0
  4. package/bin/kondi-chat.js +72 -0
  5. package/package.json +55 -0
  6. package/scripts/demo.tape +49 -0
  7. package/scripts/postinstall.cjs +103 -0
  8. package/src/audit/analytics.ts +261 -0
  9. package/src/audit/ledger.ts +253 -0
  10. package/src/audit/telemetry.ts +165 -0
  11. package/src/cli/backend.ts +675 -0
  12. package/src/cli/commands.ts +419 -0
  13. package/src/cli/help.ts +182 -0
  14. package/src/cli/submit-helpers.ts +159 -0
  15. package/src/cli/submit.ts +539 -0
  16. package/src/cli/wizard.ts +121 -0
  17. package/src/context/bootstrap.ts +138 -0
  18. package/src/context/budget.ts +100 -0
  19. package/src/context/manager.ts +666 -0
  20. package/src/context/memory.ts +160 -0
  21. package/src/context/preflight.ts +176 -0
  22. package/src/context/project-brain.ts +101 -0
  23. package/src/context/receipts.ts +108 -0
  24. package/src/context/skills.ts +154 -0
  25. package/src/context/symbol-index.ts +240 -0
  26. package/src/council/profiles.ts +137 -0
  27. package/src/council/tool.ts +138 -0
  28. package/src/council-engine/cli/council-artifacts.ts +230 -0
  29. package/src/council-engine/cli/council-config.ts +178 -0
  30. package/src/council-engine/cli/council-session-export.ts +116 -0
  31. package/src/council-engine/cli/kondi.ts +98 -0
  32. package/src/council-engine/cli/llm-caller.ts +229 -0
  33. package/src/council-engine/cli/localStorage-shim.ts +119 -0
  34. package/src/council-engine/cli/node-platform.ts +68 -0
  35. package/src/council-engine/cli/run-council.ts +481 -0
  36. package/src/council-engine/cli/run-pipeline.ts +772 -0
  37. package/src/council-engine/cli/session-export.ts +153 -0
  38. package/src/council-engine/configs/councils/analysis.json +101 -0
  39. package/src/council-engine/configs/councils/code-planning.json +86 -0
  40. package/src/council-engine/configs/councils/coding.json +89 -0
  41. package/src/council-engine/configs/councils/debate.json +97 -0
  42. package/src/council-engine/configs/councils/solo-claude.json +34 -0
  43. package/src/council-engine/configs/councils/solo-gpt.json +34 -0
  44. package/src/council-engine/council/coding-orchestrator.ts +1205 -0
  45. package/src/council-engine/council/context-bootstrap.ts +147 -0
  46. package/src/council-engine/council/context-inspection.ts +42 -0
  47. package/src/council-engine/council/context-store.ts +763 -0
  48. package/src/council-engine/council/deliberation-orchestrator.ts +2762 -0
  49. package/src/council-engine/council/factory.ts +164 -0
  50. package/src/council-engine/council/index.ts +201 -0
  51. package/src/council-engine/council/ledger-store.ts +438 -0
  52. package/src/council-engine/council/prompts.ts +1689 -0
  53. package/src/council-engine/council/storage-cleanup.ts +164 -0
  54. package/src/council-engine/council/store.ts +1110 -0
  55. package/src/council-engine/council/synthesis.ts +291 -0
  56. package/src/council-engine/council/types.ts +845 -0
  57. package/src/council-engine/council/validation.ts +613 -0
  58. package/src/council-engine/pipeline/build-detect.ts +73 -0
  59. package/src/council-engine/pipeline/executor.ts +1048 -0
  60. package/src/council-engine/pipeline/index.ts +9 -0
  61. package/src/council-engine/pipeline/install-detect.ts +84 -0
  62. package/src/council-engine/pipeline/memory-store.ts +182 -0
  63. package/src/council-engine/pipeline/output-parsers.ts +146 -0
  64. package/src/council-engine/pipeline/run-output.ts +149 -0
  65. package/src/council-engine/pipeline/session-import.ts +177 -0
  66. package/src/council-engine/pipeline/store.ts +753 -0
  67. package/src/council-engine/pipeline/test-detect.ts +82 -0
  68. package/src/council-engine/pipeline/types.ts +401 -0
  69. package/src/council-engine/services/deliberationSummary.ts +114 -0
  70. package/src/council-engine/tsconfig.json +16 -0
  71. package/src/council-engine/types/mcp.ts +122 -0
  72. package/src/council-engine/utils/filterTools.ts +73 -0
  73. package/src/engine/apply.ts +238 -0
  74. package/src/engine/checkpoints.ts +237 -0
  75. package/src/engine/consultants.ts +347 -0
  76. package/src/engine/diff.ts +171 -0
  77. package/src/engine/errors.ts +102 -0
  78. package/src/engine/git-tools.ts +246 -0
  79. package/src/engine/hooks.ts +181 -0
  80. package/src/engine/loop-guard.ts +155 -0
  81. package/src/engine/permissions.ts +293 -0
  82. package/src/engine/pipeline.ts +376 -0
  83. package/src/engine/sub-agents.ts +133 -0
  84. package/src/engine/task-card.ts +185 -0
  85. package/src/engine/task-router.ts +256 -0
  86. package/src/engine/task-store.ts +86 -0
  87. package/src/engine/tools.ts +783 -0
  88. package/src/engine/verify.ts +111 -0
  89. package/src/mcp/client.ts +225 -0
  90. package/src/mcp/config.ts +120 -0
  91. package/src/mcp/tool-manager.ts +192 -0
  92. package/src/mcp/types.ts +61 -0
  93. package/src/providers/llm-caller.ts +943 -0
  94. package/src/providers/rate-limiter.ts +238 -0
  95. package/src/router/NOTES.md +28 -0
  96. package/src/router/collector.ts +474 -0
  97. package/src/router/embeddings.ts +286 -0
  98. package/src/router/index.ts +299 -0
  99. package/src/router/intent-router.ts +225 -0
  100. package/src/router/nn-router.ts +205 -0
  101. package/src/router/profiles.ts +309 -0
  102. package/src/router/registry.ts +565 -0
  103. package/src/router/rules.ts +274 -0
  104. package/src/router/train.py +408 -0
  105. package/src/session/store.ts +211 -0
  106. package/src/test-utils/mock-llm.ts +39 -0
  107. package/src/types.ts +322 -0
  108. package/src/web/manager.ts +311 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Memory System — persistent KONDI.md + AGENTS.md files injected into
3
+ * the system prompt.
4
+ *
5
+ * Hierarchy (lowest to highest priority; later overrides earlier):
6
+ * 1. user memory ~/.kondi-chat/KONDI.md (+ AGENTS.md)
7
+ * 2. project memory <workingDir>/KONDI.md (+ AGENTS.md)
8
+ * 3. subdir memory nearest-ancestor KONDI.md or AGENTS.md from the active file
9
+ *
10
+ * AGENTS.md is an open convention supported by Claude Code, Cursor,
11
+ * Copilot, Gemini CLI, Windsurf, Aider, Zed, and others. kondi-chat
12
+ * reads it at the same levels as KONDI.md. If both files exist at the
13
+ * same level, both are loaded (AGENTS.md first, then KONDI.md).
14
+ *
15
+ * No YAML frontmatter parsing, no file watcher — load() re-stats on each call
16
+ * and re-reads only files whose mtime changed.
17
+ */
18
+
19
+ import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
20
+ import { homedir } from 'node:os';
21
+ import { dirname, join, resolve, relative } from 'node:path';
22
+
23
+ /** Files to search at each level, in load order. */
24
+ const MEMORY_FILENAMES = ['AGENTS.md', 'KONDI.md'];
25
+ const MAX_FILE_BYTES = 50_000;
26
+ const MAX_SUBDIR_DEPTH = 5;
27
+
28
+ export interface MemoryEntry {
29
+ source: 'user' | 'project' | 'subdirectory';
30
+ path: string;
31
+ content: string;
32
+ }
33
+
34
+ interface Cached {
35
+ mtimeMs: number;
36
+ content: string;
37
+ }
38
+
39
+ function readCapped(path: string): string {
40
+ const buf = readFileSync(path, 'utf-8');
41
+ return buf.length > MAX_FILE_BYTES
42
+ ? buf.slice(0, MAX_FILE_BYTES) + '\n\n(truncated)'
43
+ : buf;
44
+ }
45
+
46
+ export class MemoryManager {
47
+ private workingDir: string;
48
+ private cache = new Map<string, Cached>();
49
+
50
+ constructor(workingDir: string) {
51
+ this.workingDir = resolve(workingDir);
52
+ }
53
+
54
+ /**
55
+ * Read user-level + project-level memory, plus the nearest-ancestor
56
+ * KONDI.md or AGENTS.md for activeFile. Both filenames are checked at
57
+ * every level; if both exist, both are loaded (AGENTS.md first).
58
+ */
59
+ load(activeFile?: string): MemoryEntry[] {
60
+ const entries: MemoryEntry[] = [];
61
+
62
+ // User level: ~/.kondi-chat/AGENTS.md, ~/.kondi-chat/KONDI.md
63
+ const userDir = join(homedir(), '.kondi-chat');
64
+ for (const fn of MEMORY_FILENAMES) {
65
+ const e = this.readIfPresent(join(userDir, fn), 'user');
66
+ if (e) entries.push(e);
67
+ }
68
+
69
+ // Project level: <workingDir>/AGENTS.md, <workingDir>/KONDI.md
70
+ const projectPaths = new Set<string>();
71
+ for (const fn of MEMORY_FILENAMES) {
72
+ const p = join(this.workingDir, fn);
73
+ projectPaths.add(p);
74
+ const e = this.readIfPresent(p, 'project');
75
+ if (e) entries.push(e);
76
+ }
77
+
78
+ // Subdirectory: walk up from activeFile toward workingDir, stop at project root.
79
+ if (activeFile) {
80
+ const full = resolve(this.workingDir, activeFile);
81
+ if (!relative(this.workingDir, full).startsWith('..')) {
82
+ let dir = dirname(full);
83
+ let depth = 0;
84
+ let found = false;
85
+ while (!found && depth < MAX_SUBDIR_DEPTH && dir.length >= this.workingDir.length && dir !== this.workingDir) {
86
+ for (const fn of MEMORY_FILENAMES) {
87
+ const candidate = join(dir, fn);
88
+ if (projectPaths.has(candidate)) continue;
89
+ const e = this.readIfPresent(candidate, 'subdirectory');
90
+ if (e) { entries.push(e); found = true; }
91
+ }
92
+ if (found) break;
93
+ const parent = dirname(dir);
94
+ if (parent === dir) break;
95
+ dir = parent;
96
+ depth++;
97
+ }
98
+ }
99
+ }
100
+
101
+ return entries;
102
+ }
103
+
104
+ private readIfPresent(path: string, source: MemoryEntry['source']): MemoryEntry | null {
105
+ if (!existsSync(path)) { this.cache.delete(path); return null; }
106
+ try {
107
+ const stat = statSync(path);
108
+ const cached = this.cache.get(path);
109
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
110
+ return { source, path, content: cached.content };
111
+ }
112
+ const content = readCapped(path);
113
+ this.cache.set(path, { mtimeMs: stat.mtimeMs, content });
114
+ return { source, path, content };
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ /** Render memory entries as a delimited system-prompt section. Lower-priority first. */
121
+ formatForPrompt(entries: MemoryEntry[]): string {
122
+ if (entries.length === 0) return '';
123
+ const order: MemoryEntry['source'][] = ['user', 'project', 'subdirectory'];
124
+ const sorted = [...entries].sort(
125
+ (a, b) => order.indexOf(a.source) - order.indexOf(b.source),
126
+ );
127
+ const parts: string[] = ['## Memory (advisory, not safety-critical; higher sections override lower)'];
128
+ for (const e of sorted) {
129
+ parts.push(`### Memory: ${e.source} (${e.path})\n${e.content.trim()}`);
130
+ }
131
+ return parts.join('\n\n');
132
+ }
133
+
134
+ /**
135
+ * Write a KONDI.md file. `append` concatenates to the existing file; `replace` overwrites.
136
+ * Returns the absolute path written.
137
+ */
138
+ updateMemory(
139
+ scope: 'user' | 'project',
140
+ operation: 'append' | 'replace',
141
+ content: string,
142
+ ): { path: string } {
143
+ // Writes always go to KONDI.md (the kondi-chat-specific file).
144
+ // AGENTS.md is a cross-tool convention and is typically hand-authored
145
+ // or maintained by a separate process — the agent shouldn't overwrite it.
146
+ const target = scope === 'user'
147
+ ? join(homedir(), '.kondi-chat', 'KONDI.md')
148
+ : join(this.workingDir, 'KONDI.md');
149
+ mkdirSync(dirname(target), { recursive: true });
150
+ if (operation === 'append' && existsSync(target)) {
151
+ const existing = readFileSync(target, 'utf-8');
152
+ const sep = existing.endsWith('\n') ? '' : '\n';
153
+ writeFileSync(target, existing + sep + content + '\n');
154
+ } else {
155
+ writeFileSync(target, content.endsWith('\n') ? content : content + '\n');
156
+ }
157
+ this.cache.delete(target);
158
+ return { path: target };
159
+ }
160
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Preflight Investigation — automatically reads relevant files before
3
+ * the agent loop starts so the model begins each turn already knowing
4
+ * the relevant code.
5
+ *
6
+ * Instead of the agent spending its first 3-4 tool calls on read_file,
7
+ * the preflight infers which files matter from the task text and injects
8
+ * a compact snapshot into the system prompt. Zero LLM calls — just
9
+ * regex matching against the file tree + reading the matches.
10
+ *
11
+ * The router is unaffected — this runs before router.select() and
12
+ * provides context that any model can use regardless of which one
13
+ * is selected for the phase.
14
+ */
15
+
16
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
17
+ import { join, relative, extname } from 'node:path';
18
+ import { execSync } from 'node:child_process';
19
+
20
+ const MAX_FILE_SIZE = 8_000; // chars per file
21
+ const MAX_TOTAL_SIZE = 30_000; // total preflight context chars
22
+ const MAX_FILES = 8;
23
+
24
+ const CODE_EXTENSIONS = new Set([
25
+ '.ts', '.tsx', '.js', '.jsx', '.py', '.rs', '.go', '.java',
26
+ '.cpp', '.cc', '.c', '.h', '.hpp', '.cs', '.rb', '.php',
27
+ '.swift', '.kt', '.sh', '.sql', '.md', '.json', '.yml', '.yaml', '.toml',
28
+ ]);
29
+
30
+ interface PreflightResult {
31
+ filesRead: string[];
32
+ context: string;
33
+ gitDiff?: string;
34
+ }
35
+
36
+ /**
37
+ * Scan the task text for file references, find related files, read
38
+ * them, and assemble a preflight context block.
39
+ */
40
+ export function runPreflight(
41
+ workingDir: string,
42
+ taskText: string,
43
+ ): PreflightResult {
44
+ const filesRead: string[] = [];
45
+ const sections: string[] = [];
46
+ let totalChars = 0;
47
+
48
+ // 1. Extract explicit file references from the task text.
49
+ const explicitFiles = extractFileReferences(taskText);
50
+
51
+ // 2. Infer related files from keywords in the task.
52
+ const inferredFiles = inferRelatedFiles(workingDir, taskText);
53
+
54
+ // 3. Merge, deduplicate, cap at MAX_FILES.
55
+ const candidates = [...new Set([...explicitFiles, ...inferredFiles])].slice(0, MAX_FILES);
56
+
57
+ // 4. Read each file.
58
+ for (const relPath of candidates) {
59
+ if (totalChars >= MAX_TOTAL_SIZE) break;
60
+ const absPath = join(workingDir, relPath);
61
+ if (!existsSync(absPath)) continue;
62
+ try {
63
+ const stat = statSync(absPath);
64
+ if (!stat.isFile() || stat.size > 100_000) continue;
65
+ let content = readFileSync(absPath, 'utf-8');
66
+ if (content.length > MAX_FILE_SIZE) {
67
+ content = content.slice(0, MAX_FILE_SIZE) + `\n... (${content.length - MAX_FILE_SIZE} chars truncated)`;
68
+ }
69
+ sections.push(`### ${relPath}\n\`\`\`\n${content}\n\`\`\``);
70
+ filesRead.push(relPath);
71
+ totalChars += content.length;
72
+ } catch {
73
+ // Skip unreadable files
74
+ }
75
+ }
76
+
77
+ // 5. Recent git diff (last commit or uncommitted changes).
78
+ let gitDiff: string | undefined;
79
+ try {
80
+ const diff = execSync('git diff --stat HEAD 2>/dev/null || git diff --stat 2>/dev/null', {
81
+ cwd: workingDir,
82
+ encoding: 'utf-8',
83
+ timeout: 5000,
84
+ }).trim();
85
+ if (diff && diff.length < 2000) {
86
+ gitDiff = diff;
87
+ sections.push(`### Recent changes (git diff --stat)\n${diff}`);
88
+ }
89
+ } catch {
90
+ // Not a git repo or git not available
91
+ }
92
+
93
+ const context = sections.length > 0
94
+ ? `## Preflight: relevant files (auto-loaded)\n\n${sections.join('\n\n')}`
95
+ : '';
96
+
97
+ return { filesRead, context, gitDiff };
98
+ }
99
+
100
+ /**
101
+ * Extract explicit file paths from the task text.
102
+ * Matches patterns like "src/foo.ts", "package.json", "./bar/baz.py".
103
+ */
104
+ function extractFileReferences(text: string): string[] {
105
+ const refs: string[] = [];
106
+ // Match file paths with known extensions
107
+ const pathRe = /(?:^|\s|["'`(])([.\w/-]+(?:\.\w{1,10}))(?=[\s"'`),.:;]|$)/gm;
108
+ let match;
109
+ while ((match = pathRe.exec(text)) !== null) {
110
+ const candidate = match[1].replace(/^\.\//, '');
111
+ const ext = extname(candidate).toLowerCase();
112
+ if (CODE_EXTENSIONS.has(ext) || candidate === 'package.json' || candidate === 'Cargo.toml') {
113
+ refs.push(candidate);
114
+ }
115
+ }
116
+ return refs;
117
+ }
118
+
119
+ /**
120
+ * Infer related files from keywords in the task. Scans the file tree
121
+ * (shallow, no node_modules) for filenames that match significant
122
+ * words from the task.
123
+ */
124
+ function inferRelatedFiles(workingDir: string, text: string): string[] {
125
+ // Extract significant words (3+ chars, not common stop words)
126
+ const stopWords = new Set(['the', 'this', 'that', 'with', 'from', 'have', 'been', 'will',
127
+ 'can', 'should', 'would', 'could', 'make', 'like', 'just', 'also', 'into', 'about',
128
+ 'what', 'when', 'where', 'which', 'there', 'their', 'them', 'then', 'than', 'some',
129
+ 'more', 'most', 'very', 'each', 'every', 'all', 'any', 'both', 'few', 'other',
130
+ 'such', 'only', 'same', 'how', 'does', 'did', 'has', 'had', 'not', 'but', 'for',
131
+ 'are', 'was', 'were', 'and', 'the', 'add', 'fix', 'update', 'change', 'remove',
132
+ 'write', 'read', 'create', 'delete', 'implement', 'refactor', 'test', 'run', 'build',
133
+ 'use', 'using', 'file', 'code', 'function', 'method', 'class', 'module',
134
+ ]);
135
+
136
+ const words = text.toLowerCase()
137
+ .replace(/[^a-z0-9_-]/g, ' ')
138
+ .split(/\s+/)
139
+ .filter(w => w.length >= 3 && !stopWords.has(w));
140
+
141
+ if (words.length === 0) return [];
142
+
143
+ // Scan the file tree (max 2 levels deep, skip heavy dirs)
144
+ const skipDirs = new Set(['node_modules', '.git', '.kondi-chat', '.claude', 'dist', 'target', '__pycache__', '.next', '.venv', 'venv']);
145
+ const matches: string[] = [];
146
+
147
+ function scan(dir: string, depth: number) {
148
+ if (depth > 2) return;
149
+ try {
150
+ const entries = readdirSync(dir, { withFileTypes: true });
151
+ for (const entry of entries) {
152
+ if (skipDirs.has(entry.name)) continue;
153
+ const rel = relative(workingDir, join(dir, entry.name));
154
+ if (entry.isDirectory()) {
155
+ scan(join(dir, entry.name), depth + 1);
156
+ } else if (entry.isFile()) {
157
+ const ext = extname(entry.name).toLowerCase();
158
+ if (!CODE_EXTENSIONS.has(ext)) continue;
159
+ const nameLower = entry.name.toLowerCase().replace(extname(entry.name), '');
160
+ // Check if any task word appears in the filename
161
+ for (const word of words) {
162
+ if (nameLower.includes(word) || word.includes(nameLower)) {
163
+ matches.push(rel);
164
+ break;
165
+ }
166
+ }
167
+ }
168
+ }
169
+ } catch {
170
+ // Permission error or similar
171
+ }
172
+ }
173
+
174
+ scan(workingDir, 0);
175
+ return matches;
176
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Project Brain — unified context assembler.
3
+ *
4
+ * Pulls together every context source into one coherent block that
5
+ * gets injected into the system prompt before every model call:
6
+ *
7
+ * 1. AGENTS.md / KONDI.md (project conventions)
8
+ * 2. Recent receipts (what happened in prior turns)
9
+ * 3. Relevant skills (procedures for this type of task)
10
+ * 4. Preflight files (auto-loaded relevant code)
11
+ * 5. Repo summary (from bootstrap)
12
+ *
13
+ * The brain doesn't decide which model runs — that's the router's
14
+ * job. The brain decides what context that model should see.
15
+ */
16
+
17
+ import { ReceiptStore } from './receipts.ts';
18
+ import { MemoryManager } from './memory.ts';
19
+ import { runPreflight } from './preflight.ts';
20
+ import { loadSkills, selectSkills, formatSkillsForContext, seedDefaultSkills } from './skills.ts';
21
+ import { TaskStore } from '../engine/task-store.ts';
22
+ import type { Session } from '../types.ts';
23
+
24
+ export interface BrainContext {
25
+ /** Full assembled context for injection into system prompt. */
26
+ fullContext: string;
27
+ /** Files auto-loaded by preflight. */
28
+ preflightFiles: string[];
29
+ /** Skills matched for this task. */
30
+ skillsUsed: string[];
31
+ }
32
+
33
+ /**
34
+ * Assemble all context sources for a given task.
35
+ * Called once per turn, before the agent loop starts.
36
+ */
37
+ export function assembleBrainContext(
38
+ workingDir: string,
39
+ session: Session,
40
+ task: string,
41
+ opts?: { skipPreflight?: boolean },
42
+ ): BrainContext {
43
+ const sections: string[] = [];
44
+ const preflightFiles: string[] = [];
45
+ const skillsUsed: string[] = [];
46
+
47
+ // 1. Memory (AGENTS.md, KONDI.md)
48
+ const memory = new MemoryManager(workingDir);
49
+ const entries = memory.load();
50
+ if (entries.length > 0) {
51
+ sections.push(
52
+ '## Project conventions\n\n' +
53
+ entries.map(e => `### ${e.source}: ${e.path}\n${e.content}`).join('\n\n')
54
+ );
55
+ }
56
+
57
+ // 2. Active task (persisted across sessions)
58
+ const storageDir = `${workingDir}/.kondi-chat`;
59
+ const taskStore = new TaskStore(storageDir);
60
+ const activeTask = taskStore.formatForContext();
61
+ if (activeTask) {
62
+ sections.push(activeTask);
63
+ }
64
+
65
+ // 3. Recent receipts
66
+ const receipts = new ReceiptStore(storageDir, session.id);
67
+ const recentReceipts = receipts.formatForContext(3);
68
+ if (recentReceipts) {
69
+ sections.push(recentReceipts);
70
+ }
71
+
72
+ // 3. Skills
73
+ seedDefaultSkills(workingDir);
74
+ const skills = loadSkills(workingDir);
75
+ const matched = selectSkills(task, skills, 2);
76
+ if (matched.length > 0) {
77
+ sections.push(formatSkillsForContext(matched));
78
+ skillsUsed.push(...matched.map(s => s.name));
79
+ }
80
+
81
+ // 4. Preflight (relevant files) — skipped for short/follow-up messages
82
+ if (!opts?.skipPreflight) {
83
+ const preflight = runPreflight(workingDir, task);
84
+ if (preflight.context) {
85
+ sections.push(preflight.context);
86
+ preflightFiles.push(...preflight.filesRead);
87
+ }
88
+ }
89
+
90
+ // 5. Repo summary (grounding context from bootstrap, if available)
91
+ if (session.groundingContext) {
92
+ // Already in the system prompt via cacheablePrefix — don't duplicate.
93
+ // Just note it so the model knows it's there.
94
+ }
95
+
96
+ return {
97
+ fullContext: sections.join('\n\n'),
98
+ preflightFiles,
99
+ skillsUsed,
100
+ };
101
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Context Receipts — structured per-turn summaries that persist
3
+ * across turns and sessions.
4
+ *
5
+ * After every turn, a receipt records what changed, why, which files
6
+ * were touched, and what comes next. The last N receipts are injected
7
+ * into the system prompt so the model has cross-turn continuity
8
+ * without re-reading the entire conversation history.
9
+ *
10
+ * Receipts are cheap: no LLM call needed. They're assembled from
11
+ * data already available in the turn (tool calls, model responses,
12
+ * ledger entries). Storage is a simple JSONL file under the session.
13
+ */
14
+
15
+ import { appendFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
16
+ import { join, dirname } from 'node:path';
17
+
18
+ export interface TurnReceipt {
19
+ turnNumber: number;
20
+ timestamp: string;
21
+ userGoal: string;
22
+ modelUsed: string;
23
+ filesRead: string[];
24
+ filesWritten: string[];
25
+ toolsCalled: string[];
26
+ outcome: string;
27
+ remainingWork?: string;
28
+ }
29
+
30
+ export class ReceiptStore {
31
+ private path: string;
32
+
33
+ constructor(storageDir: string, sessionId: string) {
34
+ const dir = join(storageDir, 'sessions');
35
+ mkdirSync(dir, { recursive: true });
36
+ this.path = join(dir, `${sessionId}-receipts.jsonl`);
37
+ }
38
+
39
+ /** Append a receipt after a turn completes. */
40
+ record(receipt: TurnReceipt): void {
41
+ appendFileSync(this.path, JSON.stringify(receipt) + '\n');
42
+ }
43
+
44
+ /** Get the last N receipts for injection into context. */
45
+ getRecent(count = 3): TurnReceipt[] {
46
+ if (!existsSync(this.path)) return [];
47
+ try {
48
+ const lines = readFileSync(this.path, 'utf-8').trim().split('\n').filter(Boolean);
49
+ return lines.slice(-count).map(l => JSON.parse(l));
50
+ } catch {
51
+ return [];
52
+ }
53
+ }
54
+
55
+ /** Format receipts for injection into the system prompt. */
56
+ formatForContext(count = 3): string {
57
+ const receipts = this.getRecent(count);
58
+ if (receipts.length === 0) return '';
59
+
60
+ const lines = ['## Recent turns'];
61
+ for (const r of receipts) {
62
+ lines.push(`Turn ${r.turnNumber}: ${r.userGoal.slice(0, 100)}`);
63
+ if (r.filesWritten.length > 0) lines.push(` wrote: ${r.filesWritten.join(', ')}`);
64
+ if (r.filesRead.length > 0) lines.push(` read: ${r.filesRead.join(', ')}`);
65
+ lines.push(` outcome: ${r.outcome.slice(0, 200)}`);
66
+ if (r.remainingWork) lines.push(` remaining: ${r.remainingWork}`);
67
+ lines.push('');
68
+ }
69
+ return lines.join('\n');
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Build a receipt from turn data. Called at the end of handleSubmit
75
+ * with whatever data is available — no LLM call needed.
76
+ */
77
+ export function buildReceipt(
78
+ turnNumber: number,
79
+ userGoal: string,
80
+ modelUsed: string,
81
+ toolCalls: Array<{ name: string; args: string; is_error: boolean }>,
82
+ finalContent: string,
83
+ ): TurnReceipt {
84
+ const filesRead: string[] = [];
85
+ const filesWritten: string[] = [];
86
+ const toolsCalled: string[] = [];
87
+
88
+ for (const tc of toolCalls) {
89
+ toolsCalled.push(tc.name);
90
+ if (tc.name === 'read_file') filesRead.push(tc.args);
91
+ if (tc.name === 'write_file' || tc.name === 'edit_file') filesWritten.push(tc.args);
92
+ }
93
+
94
+ // Extract a short outcome from the final content.
95
+ const outcomeLines = finalContent.split('\n').filter(l => l.trim().length > 0);
96
+ const outcome = outcomeLines.slice(0, 3).join(' ').slice(0, 300);
97
+
98
+ return {
99
+ turnNumber,
100
+ timestamp: new Date().toISOString(),
101
+ userGoal: userGoal.slice(0, 200),
102
+ modelUsed,
103
+ filesRead: [...new Set(filesRead)],
104
+ filesWritten: [...new Set(filesWritten)],
105
+ toolsCalled: [...new Set(toolsCalled)],
106
+ outcome,
107
+ };
108
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Skills — reusable task procedures loaded from .kondi-chat/skills/.
3
+ *
4
+ * Each skill is a SKILL.md file with structured instructions for how
5
+ * to approach a category of task (debugging, adding features, refactoring,
6
+ * code review, etc.). The skill router picks 1-3 relevant skills per
7
+ * task and injects them into the system prompt so the model follows
8
+ * a proven procedure instead of winging it.
9
+ *
10
+ * The router picks models. Skills teach models HOW to work.
11
+ */
12
+
13
+ import { readFileSync, existsSync, readdirSync, mkdirSync } from 'node:fs';
14
+ import { join, basename } from 'node:path';
15
+ import { homedir } from 'node:os';
16
+
17
+ export interface Skill {
18
+ name: string;
19
+ content: string;
20
+ /** First line of the file, used as a short description for matching. */
21
+ description: string;
22
+ }
23
+
24
+ const MAX_SKILL_SIZE = 5_000;
25
+
26
+ /**
27
+ * Load skills from user-level + project-level .kondi-chat/skills/.
28
+ * Project-level overrides user-level on name collision.
29
+ */
30
+ export function loadSkills(workingDir: string): Skill[] {
31
+ const skills = new Map<string, Skill>();
32
+
33
+ // User-level first
34
+ loadFromDir(join(homedir(), '.kondi-chat', 'skills'), skills);
35
+ // Project-level overrides
36
+ loadFromDir(join(workingDir, '.kondi-chat', 'skills'), skills);
37
+
38
+ return [...skills.values()];
39
+ }
40
+
41
+ function loadFromDir(dir: string, skills: Map<string, Skill>): void {
42
+ if (!existsSync(dir)) return;
43
+ try {
44
+ for (const file of readdirSync(dir).filter(f => f.endsWith('.md'))) {
45
+ try {
46
+ let content = readFileSync(join(dir, file), 'utf-8');
47
+ if (content.length > MAX_SKILL_SIZE) {
48
+ content = content.slice(0, MAX_SKILL_SIZE) + '\n(truncated)';
49
+ }
50
+ const name = basename(file, '.md').toLowerCase();
51
+ const firstLine = content.split('\n').find(l => l.trim().length > 0 && !l.startsWith('#')) || '';
52
+ skills.set(name, { name, content, description: firstLine.slice(0, 200) });
53
+ } catch { /* skip unreadable */ }
54
+ }
55
+ } catch { /* dir not readable */ }
56
+ }
57
+
58
+ /**
59
+ * Pick the most relevant skills for a given task. Simple keyword
60
+ * matching — no LLM call needed.
61
+ */
62
+ export function selectSkills(task: string, skills: Skill[], max = 2): Skill[] {
63
+ if (skills.length === 0) return [];
64
+ const taskLower = task.toLowerCase();
65
+
66
+ const scored = skills.map(skill => {
67
+ let score = 0;
68
+ // Match skill name against task words
69
+ if (taskLower.includes(skill.name)) score += 10;
70
+ // Match keywords from the skill description
71
+ const descWords = skill.description.toLowerCase().split(/\s+/);
72
+ for (const word of descWords) {
73
+ if (word.length >= 4 && taskLower.includes(word)) score += 1;
74
+ }
75
+ return { skill, score };
76
+ });
77
+
78
+ return scored
79
+ .filter(s => s.score > 0)
80
+ .sort((a, b) => b.score - a.score)
81
+ .slice(0, max)
82
+ .map(s => s.skill);
83
+ }
84
+
85
+ /**
86
+ * Format selected skills for injection into the system prompt.
87
+ */
88
+ export function formatSkillsForContext(skills: Skill[]): string {
89
+ if (skills.length === 0) return '';
90
+ const sections = skills.map(s => `### Skill: ${s.name}\n${s.content}`);
91
+ return `## Relevant procedures\n\n${sections.join('\n\n')}`;
92
+ }
93
+
94
+ /**
95
+ * Seed default skills if the skills directory doesn't exist.
96
+ */
97
+ export function seedDefaultSkills(workingDir: string): void {
98
+ const dir = join(workingDir, '.kondi-chat', 'skills');
99
+ if (existsSync(dir)) return;
100
+ mkdirSync(dir, { recursive: true });
101
+
102
+ const defaults: Record<string, string> = {
103
+ 'debug': `# Debug
104
+
105
+ When debugging an issue:
106
+ 1. Reproduce the problem — read the error message carefully
107
+ 2. Find the source: search_code for the error text, read the relevant file
108
+ 3. Understand the context: read related_files to see dependencies
109
+ 4. Form a hypothesis about the root cause
110
+ 5. Make the minimal fix — don't refactor while debugging
111
+ 6. Verify: run the tests or typecheck to confirm the fix
112
+ 7. Check for similar patterns elsewhere in the codebase`,
113
+
114
+ 'add-feature': `# Add Feature
115
+
116
+ When implementing a new feature:
117
+ 1. Read existing code in the area — understand conventions, patterns, types
118
+ 2. Check for tests — understand how existing features are tested
119
+ 3. Plan the change: which files need modification, which are new
120
+ 4. Implement incrementally — write, then verify with typecheck/tests
121
+ 5. Follow existing patterns — don't introduce new conventions
122
+ 6. Add tests if the project has them
123
+ 7. Review your own diff before reporting done`,
124
+
125
+ 'refactor': `# Refactor
126
+
127
+ When refactoring code:
128
+ 1. Understand WHY it needs refactoring — state the goal clearly
129
+ 2. Read all the code you'll touch + its tests
130
+ 3. Make sure existing tests pass BEFORE you start
131
+ 4. Refactor in small steps — one file or one concept at a time
132
+ 5. Run tests after each step
133
+ 6. Don't change behavior — only structure
134
+ 7. If you find bugs during refactoring, note them but don't fix them in the same change`,
135
+
136
+ 'review': `# Code Review
137
+
138
+ When reviewing code:
139
+ 1. Read the full diff, not just the changed lines
140
+ 2. Check: does it do what it claims? Are edge cases handled?
141
+ 3. Look for: security issues, error handling gaps, race conditions
142
+ 4. Check naming: are new names consistent with existing conventions?
143
+ 5. Check tests: are new behaviors tested? Are old tests still valid?
144
+ 6. Be specific: "line 42 misses the null case" not "needs more error handling"
145
+ 7. Distinguish blocking issues from suggestions`,
146
+ };
147
+
148
+ for (const [name, content] of Object.entries(defaults)) {
149
+ const path = join(dir, `${name}.md`);
150
+ if (!existsSync(path)) {
151
+ try { require('node:fs').writeFileSync(path, content); } catch { /* ignore */ }
152
+ }
153
+ }
154
+ }