@phuetz/code-buddy 1.2.0 → 1.3.0
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 +56 -3
- package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
- package/dist/agent/base-agent.d.ts +4 -0
- package/dist/agent/base-agent.js +6 -0
- package/dist/agent/facades/infrastructure-facade.d.ts +9 -2
- package/dist/agent/facades/infrastructure-facade.js +15 -6
- package/dist/agent/self-improvement/authored-artifact-gate.d.ts +18 -0
- package/dist/agent/self-improvement/authored-artifact-gate.js +42 -0
- package/dist/agent/self-improvement/authored-tool-runtime.d.ts +27 -0
- package/dist/agent/self-improvement/authored-tool-runtime.js +57 -0
- package/dist/agent/self-improvement/authored-tool-store.d.ts +24 -0
- package/dist/agent/self-improvement/authored-tool-store.js +57 -0
- package/dist/agent/self-improvement/llm-tool-proposer.d.ts +41 -0
- package/dist/agent/self-improvement/llm-tool-proposer.js +136 -0
- package/dist/agent/self-improvement/sandbox-scorer.d.ts +17 -0
- package/dist/agent/self-improvement/sandbox-scorer.js +43 -0
- package/dist/agent/self-improvement/self-knowledge.d.ts +8 -0
- package/dist/agent/self-improvement/self-knowledge.js +24 -0
- package/dist/agent/self-improvement/skill-benchmark.d.ts +9 -0
- package/dist/agent/self-improvement/skill-benchmark.js +22 -0
- package/dist/agent/self-improvement/skill-consolidator.d.ts +71 -0
- package/dist/agent/self-improvement/skill-consolidator.js +137 -0
- package/dist/agent/self-improvement/skill-engine.d.ts +42 -0
- package/dist/agent/self-improvement/skill-engine.js +87 -0
- package/dist/agent/self-improvement/skill-gate.d.ts +19 -0
- package/dist/agent/self-improvement/skill-gate.js +62 -0
- package/dist/agent/self-improvement/skill-mutator.d.ts +74 -0
- package/dist/agent/self-improvement/skill-mutator.js +223 -0
- package/dist/agent/self-improvement/skill-proposer.d.ts +40 -0
- package/dist/agent/self-improvement/skill-proposer.js +82 -0
- package/dist/agent/self-improvement/skill-types.d.ts +41 -0
- package/dist/agent/self-improvement/skill-types.js +13 -0
- package/dist/agent/self-improvement/tool-benchmark.d.ts +10 -0
- package/dist/agent/self-improvement/tool-benchmark.js +37 -0
- package/dist/agent/self-improvement/tool-engine.d.ts +54 -0
- package/dist/agent/self-improvement/tool-engine.js +101 -0
- package/dist/agent/self-improvement/tool-gate.d.ts +20 -0
- package/dist/agent/self-improvement/tool-gate.js +78 -0
- package/dist/agent/self-improvement/tool-proposer.d.ts +31 -0
- package/dist/agent/self-improvement/tool-proposer.js +34 -0
- package/dist/agent/self-improvement/tool-skill-mutator.d.ts +40 -0
- package/dist/agent/self-improvement/tool-skill-mutator.js +79 -0
- package/dist/agent/self-improvement/tool-types.d.ts +48 -0
- package/dist/agent/self-improvement/tool-types.js +9 -0
- package/dist/agent/self-improvement/types.d.ts +3 -1
- package/dist/agent/tool-handler.js +3 -0
- package/dist/codebuddy/providers/provider-chatgpt-responses.js +6 -1
- package/dist/codebuddy/tools.d.ts +7 -0
- package/dist/codebuddy/tools.js +40 -0
- package/dist/commands/cli/improve-command.js +123 -0
- package/dist/commands/enhanced-command-handler.js +1 -1
- package/dist/commands/handlers/missing-handlers.d.ts +1 -1
- package/dist/commands/handlers/missing-handlers.js +26 -3
- package/dist/commands/skills-cli/index.js +123 -0
- package/dist/commands/slash/builtin-commands.js +1 -1
- package/dist/companion/percepts.js +11 -1
- package/dist/context/bootstrap-loader.js +6 -23
- package/dist/context/import-directive-parser.d.ts +4 -0
- package/dist/context/import-directive-parser.js +51 -6
- package/dist/context/instruction-excludes.d.ts +30 -1
- package/dist/context/instruction-excludes.js +71 -1
- package/dist/context/jit-context.d.ts +8 -10
- package/dist/context/jit-context.js +28 -106
- package/dist/context/project-context.d.ts +90 -0
- package/dist/context/project-context.js +295 -0
- package/dist/daemon/autonomous-loop.d.ts +31 -1
- package/dist/daemon/autonomous-loop.js +80 -2
- package/dist/harness/contract.d.ts +28 -28
- package/dist/identity/identity-manager.js +3 -2
- package/dist/index.js +17 -1
- package/dist/mcp/mcp-resources.js +2 -3
- package/dist/sensory/dreaming.d.ts +45 -0
- package/dist/sensory/dreaming.js +114 -0
- package/dist/sensory/heartbeat-scheduler.d.ts +38 -0
- package/dist/sensory/heartbeat-scheduler.js +72 -0
- package/dist/sensory/reactions.d.ts +24 -0
- package/dist/sensory/reactions.js +31 -0
- package/dist/sensory/screen-reaction.d.ts +23 -0
- package/dist/sensory/screen-reaction.js +59 -0
- package/dist/sensory/sensory-bridge.d.ts +23 -0
- package/dist/sensory/sensory-bridge.js +85 -0
- package/dist/sensory/sensory-memory.d.ts +20 -0
- package/dist/sensory/sensory-memory.js +39 -0
- package/dist/sensory/speech-reaction.d.ts +21 -0
- package/dist/sensory/speech-reaction.js +83 -0
- package/dist/sensory/vision-reaction.d.ts +31 -0
- package/dist/sensory/vision-reaction.js +74 -0
- package/dist/server/index.js +89 -0
- package/dist/services/prompt-builder.d.ts +10 -0
- package/dist/services/prompt-builder.js +75 -9
- package/dist/skills/parser.js +3 -0
- package/dist/skills/skill-importer.d.ts +58 -0
- package/dist/skills/skill-importer.js +261 -0
- package/dist/skills/skill-sources.d.ts +20 -0
- package/dist/skills/skill-sources.js +102 -0
- package/dist/skills/types.d.ts +6 -0
- package/dist/tools/register-tool-handler.d.ts +25 -0
- package/dist/tools/register-tool-handler.js +100 -0
- package/dist/tools/registry.d.ts +6 -0
- package/dist/tools/registry.js +8 -0
- package/dist/utils/init-project.d.ts +7 -0
- package/dist/utils/init-project.js +37 -0
- package/dist/utils/settings-manager.d.ts +12 -0
- package/package.json +1 -1
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JIT (Just-In-Time) Context Discovery
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* When a tool accesses a path, this loads the instruction files in that
|
|
5
|
+
* subtree that weren't already injected at startup (delegated to the unified
|
|
6
|
+
* `project-context` loader, sharing its dedup registry), plus two JIT-only
|
|
7
|
+
* concerns: auto-discovered doc pages (DOC_DIR_MAP) and path-scoped rules.
|
|
7
8
|
*
|
|
8
|
-
* Inspired by Gemini CLI's jit-context.
|
|
9
|
+
* Inspired by Gemini CLI's jit-context.
|
|
9
10
|
*/
|
|
10
11
|
export declare const JIT_CONTEXT_PREFIX = "\n\n--- Discovered Context ---\n";
|
|
11
12
|
export declare const JIT_CONTEXT_SUFFIX = "\n--- End Context ---";
|
|
12
13
|
/**
|
|
13
|
-
* Clear
|
|
14
|
+
* Clear caches (for testing / `/context reload`). Resets both the doc dedup set
|
|
15
|
+
* and the active context registry so the next pass re-scans from scratch.
|
|
14
16
|
*/
|
|
15
17
|
export declare function clearJitCache(): void;
|
|
16
18
|
/**
|
|
17
|
-
* Discover and load context
|
|
18
|
-
*
|
|
19
|
-
* Walks upward from the accessed file's directory to the project root,
|
|
20
|
-
* checking for context files at each level. Only loads files that
|
|
21
|
-
* haven't been loaded before in this session.
|
|
19
|
+
* Discover and load JIT context for a given accessed path.
|
|
22
20
|
*
|
|
23
21
|
* @param accessedPath - The file/directory path being accessed by a tool
|
|
24
22
|
* @param projectRoot - The project root directory (stop walking here)
|
|
@@ -1,28 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JIT (Just-In-Time) Context Discovery
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* When a tool accesses a path, this loads the instruction files in that
|
|
5
|
+
* subtree that weren't already injected at startup (delegated to the unified
|
|
6
|
+
* `project-context` loader, sharing its dedup registry), plus two JIT-only
|
|
7
|
+
* concerns: auto-discovered doc pages (DOC_DIR_MAP) and path-scoped rules.
|
|
7
8
|
*
|
|
8
|
-
* Inspired by Gemini CLI's jit-context.
|
|
9
|
+
* Inspired by Gemini CLI's jit-context.
|
|
9
10
|
*/
|
|
10
11
|
import * as fs from 'fs';
|
|
11
12
|
import * as path from 'path';
|
|
12
13
|
import { logger } from '../utils/logger.js';
|
|
13
14
|
import { discoverRulesForPath } from './rules-loader.js';
|
|
14
|
-
import {
|
|
15
|
-
import { resolveImportDirectives } from './import-directive-parser.js';
|
|
16
|
-
/** Context file names to discover */
|
|
17
|
-
const CONTEXT_FILES = [
|
|
18
|
-
'CODEBUDDY.md',
|
|
19
|
-
'CONTEXT.md',
|
|
20
|
-
'INSTRUCTIONS.md',
|
|
21
|
-
'AGENTS.md',
|
|
22
|
-
'README.md',
|
|
23
|
-
];
|
|
24
|
-
/** Directories that may contain context files */
|
|
25
|
-
const CONTEXT_DIRS = ['.codebuddy', '.claude'];
|
|
15
|
+
import { resolveJitContext, getActiveContextRegistry } from './project-context.js';
|
|
26
16
|
/**
|
|
27
17
|
* Map of source directory prefixes → relevant doc page slugs.
|
|
28
18
|
* When a tool accesses a path under a key, the matching doc is auto-discovered.
|
|
@@ -45,26 +35,22 @@ const DOC_DIR_MAP = {
|
|
|
45
35
|
'src/checkpoints': ['architecture'],
|
|
46
36
|
'tests': ['testing'],
|
|
47
37
|
};
|
|
48
|
-
/** Set of already-loaded paths (
|
|
38
|
+
/** Set of already-loaded doc paths (instruction files dedup via the registry). */
|
|
49
39
|
const loadedPaths = new Set();
|
|
50
40
|
/** Maximum context size per discovery (chars) */
|
|
51
41
|
const MAX_JIT_CONTEXT_CHARS = 4000;
|
|
52
|
-
/** Maximum directory depth to traverse upward */
|
|
53
|
-
const MAX_UPWARD_DEPTH = 10;
|
|
54
42
|
export const JIT_CONTEXT_PREFIX = '\n\n--- Discovered Context ---\n';
|
|
55
43
|
export const JIT_CONTEXT_SUFFIX = '\n--- End Context ---';
|
|
56
44
|
/**
|
|
57
|
-
* Clear
|
|
45
|
+
* Clear caches (for testing / `/context reload`). Resets both the doc dedup set
|
|
46
|
+
* and the active context registry so the next pass re-scans from scratch.
|
|
58
47
|
*/
|
|
59
48
|
export function clearJitCache() {
|
|
60
49
|
loadedPaths.clear();
|
|
50
|
+
getActiveContextRegistry().clear();
|
|
61
51
|
}
|
|
62
52
|
/**
|
|
63
|
-
* Discover and load context
|
|
64
|
-
*
|
|
65
|
-
* Walks upward from the accessed file's directory to the project root,
|
|
66
|
-
* checking for context files at each level. Only loads files that
|
|
67
|
-
* haven't been loaded before in this session.
|
|
53
|
+
* Discover and load JIT context for a given accessed path.
|
|
68
54
|
*
|
|
69
55
|
* @param accessedPath - The file/directory path being accessed by a tool
|
|
70
56
|
* @param projectRoot - The project root directory (stop walking here)
|
|
@@ -74,84 +60,18 @@ export function discoverJitContext(accessedPath, projectRoot = process.cwd()) {
|
|
|
74
60
|
try {
|
|
75
61
|
const normalizedRoot = path.resolve(projectRoot);
|
|
76
62
|
const normalizedPath = path.resolve(accessedPath);
|
|
77
|
-
// Start from the accessed file's directory
|
|
78
|
-
let currentDir = fs.existsSync(normalizedPath) && fs.statSync(normalizedPath).isDirectory()
|
|
79
|
-
? normalizedPath
|
|
80
|
-
: path.dirname(normalizedPath);
|
|
81
63
|
const discoveredContent = [];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (!loadedPaths.has(filePath) && fs.existsSync(filePath)) {
|
|
92
|
-
// CC10: Check instruction excludes
|
|
93
|
-
if (shouldExcludeInstructionFile(filePath, normalizedRoot)) {
|
|
94
|
-
logger.debug(`JIT context: excluded ${path.relative(normalizedRoot, filePath)}`);
|
|
95
|
-
loadedPaths.add(filePath); // Mark as "seen" to avoid re-checking
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
let content = fs.readFileSync(filePath, 'utf-8');
|
|
100
|
-
if (content.trim()) {
|
|
101
|
-
// CC9: Resolve @import directives
|
|
102
|
-
content = resolveImportDirectives(content, {
|
|
103
|
-
baseDir: path.dirname(filePath),
|
|
104
|
-
projectRoot: normalizedRoot,
|
|
105
|
-
});
|
|
106
|
-
const relativePath = path.relative(normalizedRoot, filePath).replace(/\\/g, '/');
|
|
107
|
-
discoveredContent.push(`[${relativePath}]\n${content.trim()}`);
|
|
108
|
-
loadedPaths.add(filePath);
|
|
109
|
-
logger.debug(`JIT context: loaded ${relativePath}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
catch { /* read error — skip */ }
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
// Also check .codebuddy/ and .claude/ subdirectories
|
|
116
|
-
for (const contextDir of CONTEXT_DIRS) {
|
|
117
|
-
const dirPath = path.join(currentDir, contextDir);
|
|
118
|
-
if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
|
|
119
|
-
for (const contextFile of CONTEXT_FILES) {
|
|
120
|
-
const filePath = path.join(dirPath, contextFile);
|
|
121
|
-
if (!loadedPaths.has(filePath) && fs.existsSync(filePath)) {
|
|
122
|
-
// CC10: Check instruction excludes
|
|
123
|
-
if (shouldExcludeInstructionFile(filePath, normalizedRoot)) {
|
|
124
|
-
logger.debug(`JIT context: excluded ${path.relative(normalizedRoot, filePath)}`);
|
|
125
|
-
loadedPaths.add(filePath);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
let content = fs.readFileSync(filePath, 'utf-8');
|
|
130
|
-
if (content.trim()) {
|
|
131
|
-
// CC9: Resolve @import directives
|
|
132
|
-
content = resolveImportDirectives(content, {
|
|
133
|
-
baseDir: path.dirname(filePath),
|
|
134
|
-
projectRoot: normalizedRoot,
|
|
135
|
-
});
|
|
136
|
-
const relativePath = path.relative(normalizedRoot, filePath).replace(/\\/g, '/');
|
|
137
|
-
discoveredContent.push(`[${relativePath}]\n${content.trim()}`);
|
|
138
|
-
loadedPaths.add(filePath);
|
|
139
|
-
logger.debug(`JIT context: loaded ${relativePath}`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
catch { /* read error — skip */ }
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
// Move up
|
|
148
|
-
const parent = path.dirname(currentDir);
|
|
149
|
-
if (parent === currentDir)
|
|
150
|
-
break; // reached filesystem root
|
|
151
|
-
currentDir = parent;
|
|
152
|
-
depth++;
|
|
64
|
+
// 1. Instruction files in the accessed subtree, via the unified loader.
|
|
65
|
+
// The shared active registry skips anything already injected at startup,
|
|
66
|
+
// so the same AGENTS.md/CODEBUDDY.md is never duplicated.
|
|
67
|
+
const ctx = resolveJitContext(accessedPath, {
|
|
68
|
+
projectRoot: normalizedRoot,
|
|
69
|
+
registry: getActiveContextRegistry(),
|
|
70
|
+
});
|
|
71
|
+
if (ctx.text) {
|
|
72
|
+
discoveredContent.push(ctx.text);
|
|
153
73
|
}
|
|
154
|
-
// Auto-discover relevant doc pages based on accessed path
|
|
74
|
+
// 2. Auto-discover relevant doc pages based on the accessed path.
|
|
155
75
|
const relativePath = path.relative(normalizedRoot, normalizedPath).replace(/\\/g, '/');
|
|
156
76
|
for (const [prefix, slugPatterns] of Object.entries(DOC_DIR_MAP)) {
|
|
157
77
|
if (!relativePath.startsWith(prefix))
|
|
@@ -160,16 +80,16 @@ export function discoverJitContext(accessedPath, projectRoot = process.cwd()) {
|
|
|
160
80
|
if (!fs.existsSync(docsDir))
|
|
161
81
|
break;
|
|
162
82
|
try {
|
|
163
|
-
const docFiles = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
|
|
83
|
+
const docFiles = fs.readdirSync(docsDir).filter((f) => f.endsWith('.md'));
|
|
164
84
|
for (const pattern of slugPatterns) {
|
|
165
|
-
const match = docFiles.find(f => f.includes(pattern));
|
|
85
|
+
const match = docFiles.find((f) => f.includes(pattern));
|
|
166
86
|
if (!match)
|
|
167
87
|
continue;
|
|
168
88
|
const docPath = path.join(docsDir, match);
|
|
169
89
|
if (loadedPaths.has(docPath))
|
|
170
90
|
continue;
|
|
171
91
|
const content = fs.readFileSync(docPath, 'utf-8');
|
|
172
|
-
// Only inject the first 2 sections (title + first H2) to stay compact
|
|
92
|
+
// Only inject the first 2 sections (title + first H2) to stay compact.
|
|
173
93
|
const sections = content.split(/(?=^## )/m);
|
|
174
94
|
const compact = sections.slice(0, 2).join('').trim();
|
|
175
95
|
if (compact) {
|
|
@@ -181,10 +101,12 @@ export function discoverJitContext(accessedPath, projectRoot = process.cwd()) {
|
|
|
181
101
|
break; // One doc per prefix
|
|
182
102
|
}
|
|
183
103
|
}
|
|
184
|
-
catch {
|
|
104
|
+
catch {
|
|
105
|
+
/* docs dir not readable */
|
|
106
|
+
}
|
|
185
107
|
break; // One prefix match
|
|
186
108
|
}
|
|
187
|
-
//
|
|
109
|
+
// 3. Path-scoped rules matching this access.
|
|
188
110
|
const rulesContext = discoverRulesForPath(accessedPath, projectRoot);
|
|
189
111
|
if (rulesContext) {
|
|
190
112
|
discoveredContent.push(rulesContext);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Context — the canonical hierarchical loader for project-instruction
|
|
3
|
+
* files (the AGENTS.md / CLAUDE.md / GEMINI.md / CODEBUDDY.md hierarchy).
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth that replaces the previously fragmented readers
|
|
6
|
+
* (jit-context, bootstrap-loader's hierarchical walk, context-files). It owns:
|
|
7
|
+
* - the config-driven accepted-filename list (`loadContextConfig`),
|
|
8
|
+
* - the per-directory probe (incl. `.codebuddy/` + `.claude/` descent),
|
|
9
|
+
* - variant precedence (`<name>.local.md` > `<name>.override.md` > `<name>`),
|
|
10
|
+
* - `@import` resolution (code-fence-safe), token/byte budgeting,
|
|
11
|
+
* - realpath dedup shared across the startup pass and JIT (the `ContextRegistry`).
|
|
12
|
+
*
|
|
13
|
+
* Precedence (documented once, here):
|
|
14
|
+
* - Tiers: global (`~/.codebuddy/`) is appended FIRST (lowest precedence),
|
|
15
|
+
* then the project hierarchy from project-root DOWN to cwd. Files closer to
|
|
16
|
+
* cwd appear later in the text → win on conflict (Codex AGENTS.md model).
|
|
17
|
+
* - Within a directory, every accepted filename present is COMPOSED (so
|
|
18
|
+
* `CODEBUDDY.md` extends `AGENTS.md`), in the configured order.
|
|
19
|
+
* - Within a (directory, name), the first existing variant wins and REPLACES
|
|
20
|
+
* the base: `<name>.local.md` (gitignored) > `<name>.override.md` > `<name>`.
|
|
21
|
+
* - Dedup key is `fs.realpathSync` so a `CLAUDE.md`/`GEMINI.md` symlinked to
|
|
22
|
+
* `AGENTS.md` collapses to one injection.
|
|
23
|
+
*
|
|
24
|
+
* Determinism: iteration is over the fixed `dirs × probeDirs × names` product
|
|
25
|
+
* with relative origin headers and no mtimes — the rendered text is byte-stable
|
|
26
|
+
* across runs, which keeps prompt caching intact.
|
|
27
|
+
*/
|
|
28
|
+
export type ContextTier = 'global' | 'hierarchy' | 'jit';
|
|
29
|
+
export type ContextVariant = 'base' | 'override' | 'local';
|
|
30
|
+
export interface ContextSource {
|
|
31
|
+
/** Absolute path of the loaded file. */
|
|
32
|
+
path: string;
|
|
33
|
+
/** `fs.realpathSync` of `path` — the dedup key. */
|
|
34
|
+
realpath: string;
|
|
35
|
+
/** Display path (relative to project root; `~/.codebuddy/...` for global). */
|
|
36
|
+
relPath: string;
|
|
37
|
+
tier: ContextTier;
|
|
38
|
+
variant: ContextVariant;
|
|
39
|
+
/** Size of this source's rendered body (chars). */
|
|
40
|
+
bytes: number;
|
|
41
|
+
/** Whether this source was truncated to fit the budget. */
|
|
42
|
+
truncated: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface ResolvedContext {
|
|
45
|
+
/** Merged, header-decorated text, ready to inject. */
|
|
46
|
+
text: string;
|
|
47
|
+
sources: ContextSource[];
|
|
48
|
+
bytes: number;
|
|
49
|
+
truncated: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface ResolveOptions {
|
|
52
|
+
cwd?: string;
|
|
53
|
+
/** Project root; resolved via ROOT_MARKERS when omitted. */
|
|
54
|
+
projectRoot?: string;
|
|
55
|
+
/** Shared dedup registry (startup ↔ JIT). A private one is used if omitted. */
|
|
56
|
+
registry?: ContextRegistry;
|
|
57
|
+
/** Byte budget override (defaults to config `maxBytes` / `jitMaxBytes`). */
|
|
58
|
+
budgetBytes?: number;
|
|
59
|
+
/** Accepted filenames override (defaults to config). */
|
|
60
|
+
fileNames?: string[];
|
|
61
|
+
/** Home-dir override (for testing). */
|
|
62
|
+
homeDir?: string;
|
|
63
|
+
}
|
|
64
|
+
/** Tracks realpaths already injected so a file is never loaded twice. */
|
|
65
|
+
export declare class ContextRegistry {
|
|
66
|
+
private readonly seen;
|
|
67
|
+
has(realpath: string): boolean;
|
|
68
|
+
add(realpath: string): void;
|
|
69
|
+
clear(): void;
|
|
70
|
+
get size(): number;
|
|
71
|
+
}
|
|
72
|
+
export declare function createContextRegistry(): ContextRegistry;
|
|
73
|
+
export declare function setActiveContextRegistry(registry: ContextRegistry): void;
|
|
74
|
+
export declare function getActiveContextRegistry(): ContextRegistry;
|
|
75
|
+
/** Project root via ROOT_MARKERS, walking up from `cwd` (null if none found). */
|
|
76
|
+
export declare function findProjectRoot(cwd: string, markers?: string[]): string | null;
|
|
77
|
+
/** Accepted instruction filenames for a project (config-driven). */
|
|
78
|
+
export declare function getAcceptedFileNames(projectRoot?: string): string[];
|
|
79
|
+
/**
|
|
80
|
+
* Startup pass: global tier + the project hierarchy from root DOWN to cwd.
|
|
81
|
+
* Call once per prompt build. Populates `registry` (if given) so the later JIT
|
|
82
|
+
* pass skips files already in the system prompt.
|
|
83
|
+
*/
|
|
84
|
+
export declare function resolveProjectContext(opts?: ResolveOptions): ResolvedContext;
|
|
85
|
+
/**
|
|
86
|
+
* JIT pass: when a tool touches `accessedPath`, load any context files along its
|
|
87
|
+
* directory chain (root → accessed dir) that weren't already injected at
|
|
88
|
+
* startup (skipped via the shared `registry`). Smaller per-touch budget.
|
|
89
|
+
*/
|
|
90
|
+
export declare function resolveJitContext(accessedPath: string, opts?: ResolveOptions): ResolvedContext;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Context — the canonical hierarchical loader for project-instruction
|
|
3
|
+
* files (the AGENTS.md / CLAUDE.md / GEMINI.md / CODEBUDDY.md hierarchy).
|
|
4
|
+
*
|
|
5
|
+
* Single source of truth that replaces the previously fragmented readers
|
|
6
|
+
* (jit-context, bootstrap-loader's hierarchical walk, context-files). It owns:
|
|
7
|
+
* - the config-driven accepted-filename list (`loadContextConfig`),
|
|
8
|
+
* - the per-directory probe (incl. `.codebuddy/` + `.claude/` descent),
|
|
9
|
+
* - variant precedence (`<name>.local.md` > `<name>.override.md` > `<name>`),
|
|
10
|
+
* - `@import` resolution (code-fence-safe), token/byte budgeting,
|
|
11
|
+
* - realpath dedup shared across the startup pass and JIT (the `ContextRegistry`).
|
|
12
|
+
*
|
|
13
|
+
* Precedence (documented once, here):
|
|
14
|
+
* - Tiers: global (`~/.codebuddy/`) is appended FIRST (lowest precedence),
|
|
15
|
+
* then the project hierarchy from project-root DOWN to cwd. Files closer to
|
|
16
|
+
* cwd appear later in the text → win on conflict (Codex AGENTS.md model).
|
|
17
|
+
* - Within a directory, every accepted filename present is COMPOSED (so
|
|
18
|
+
* `CODEBUDDY.md` extends `AGENTS.md`), in the configured order.
|
|
19
|
+
* - Within a (directory, name), the first existing variant wins and REPLACES
|
|
20
|
+
* the base: `<name>.local.md` (gitignored) > `<name>.override.md` > `<name>`.
|
|
21
|
+
* - Dedup key is `fs.realpathSync` so a `CLAUDE.md`/`GEMINI.md` symlinked to
|
|
22
|
+
* `AGENTS.md` collapses to one injection.
|
|
23
|
+
*
|
|
24
|
+
* Determinism: iteration is over the fixed `dirs × probeDirs × names` product
|
|
25
|
+
* with relative origin headers and no mtimes — the rendered text is byte-stable
|
|
26
|
+
* across runs, which keeps prompt caching intact.
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from 'fs';
|
|
29
|
+
import * as path from 'path';
|
|
30
|
+
import * as os from 'os';
|
|
31
|
+
import { logger } from '../utils/logger.js';
|
|
32
|
+
import { resolveImportDirectives } from './import-directive-parser.js';
|
|
33
|
+
import { loadContextConfig, shouldExcludeInstructionFile, } from './instruction-excludes.js';
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Constants
|
|
36
|
+
// ============================================================================
|
|
37
|
+
/** Project-root markers, walked upward from cwd. */
|
|
38
|
+
const ROOT_MARKERS = ['.git', 'package.json', 'Cargo.toml', 'go.mod', 'pyproject.toml', '.hg'];
|
|
39
|
+
const MAX_HIERARCHY_DEPTH = 10;
|
|
40
|
+
/** Subdirectories probed inside every level of the hierarchy. */
|
|
41
|
+
const SUBDIR_PROBES = ['.codebuddy', '.claude'];
|
|
42
|
+
const TRUNCATION_MARKER = '\n\n... (truncated)';
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// Dedup registry (shared between the startup pass and JIT)
|
|
45
|
+
// ============================================================================
|
|
46
|
+
/** Tracks realpaths already injected so a file is never loaded twice. */
|
|
47
|
+
export class ContextRegistry {
|
|
48
|
+
seen = new Set();
|
|
49
|
+
has(realpath) {
|
|
50
|
+
return this.seen.has(realpath);
|
|
51
|
+
}
|
|
52
|
+
add(realpath) {
|
|
53
|
+
this.seen.add(realpath);
|
|
54
|
+
}
|
|
55
|
+
clear() {
|
|
56
|
+
this.seen.clear();
|
|
57
|
+
}
|
|
58
|
+
get size() {
|
|
59
|
+
return this.seen.size;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function createContextRegistry() {
|
|
63
|
+
return new ContextRegistry();
|
|
64
|
+
}
|
|
65
|
+
// Module-level "active" registry. The prompt builder publishes its per-build
|
|
66
|
+
// registry here (`setActiveContextRegistry`) so the standalone JIT pass — which
|
|
67
|
+
// has no handle to the PromptBuilder — can dedup against files already injected
|
|
68
|
+
// at startup. Defaults to a persistent registry until the first build.
|
|
69
|
+
let activeRegistry = new ContextRegistry();
|
|
70
|
+
export function setActiveContextRegistry(registry) {
|
|
71
|
+
activeRegistry = registry;
|
|
72
|
+
}
|
|
73
|
+
export function getActiveContextRegistry() {
|
|
74
|
+
return activeRegistry;
|
|
75
|
+
}
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Public API
|
|
78
|
+
// ============================================================================
|
|
79
|
+
/** Project root via ROOT_MARKERS, walking up from `cwd` (null if none found). */
|
|
80
|
+
export function findProjectRoot(cwd, markers = ROOT_MARKERS) {
|
|
81
|
+
let dir = path.resolve(cwd);
|
|
82
|
+
let depth = 0;
|
|
83
|
+
while (depth < MAX_HIERARCHY_DEPTH) {
|
|
84
|
+
for (const marker of markers) {
|
|
85
|
+
try {
|
|
86
|
+
if (fs.existsSync(path.join(dir, marker)))
|
|
87
|
+
return dir;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* ignore */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const parent = path.dirname(dir);
|
|
94
|
+
if (parent === dir)
|
|
95
|
+
break;
|
|
96
|
+
dir = parent;
|
|
97
|
+
depth++;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
/** Accepted instruction filenames for a project (config-driven). */
|
|
102
|
+
export function getAcceptedFileNames(projectRoot = process.cwd()) {
|
|
103
|
+
return loadContextConfig(projectRoot).fileNames;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Startup pass: global tier + the project hierarchy from root DOWN to cwd.
|
|
107
|
+
* Call once per prompt build. Populates `registry` (if given) so the later JIT
|
|
108
|
+
* pass skips files already in the system prompt.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveProjectContext(opts = {}) {
|
|
111
|
+
const cwd = path.resolve(opts.cwd ?? process.cwd());
|
|
112
|
+
const root = path.resolve(opts.projectRoot ?? findProjectRoot(cwd) ?? cwd);
|
|
113
|
+
const cfg = loadContextConfig(root);
|
|
114
|
+
const names = opts.fileNames ?? cfg.fileNames;
|
|
115
|
+
const homeDir = opts.homeDir ?? os.homedir();
|
|
116
|
+
const registry = opts.registry ?? new ContextRegistry();
|
|
117
|
+
const loaded = [];
|
|
118
|
+
// Tier 1 — global (~/.codebuddy/), lowest precedence (appended first).
|
|
119
|
+
const globalDir = path.join(homeDir, '.codebuddy');
|
|
120
|
+
loaded.push(...collectDir(globalDir, 'global', names, root, registry, homeDir, cfg));
|
|
121
|
+
// Tier 2 — hierarchy, project root → cwd inclusive (closest appended last).
|
|
122
|
+
for (const dir of getDirectoryChain(root, cwd)) {
|
|
123
|
+
for (const probe of [dir, ...SUBDIR_PROBES.map((s) => path.join(dir, s))]) {
|
|
124
|
+
loaded.push(...collectDir(probe, 'hierarchy', names, root, registry, homeDir, cfg));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return budgetMerge(loaded, opts.budgetBytes ?? cfg.maxBytes);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* JIT pass: when a tool touches `accessedPath`, load any context files along its
|
|
131
|
+
* directory chain (root → accessed dir) that weren't already injected at
|
|
132
|
+
* startup (skipped via the shared `registry`). Smaller per-touch budget.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveJitContext(accessedPath, opts = {}) {
|
|
135
|
+
const accessedDir = path.dirname(path.resolve(accessedPath));
|
|
136
|
+
const root = path.resolve(opts.projectRoot ?? findProjectRoot(accessedDir) ?? accessedDir);
|
|
137
|
+
const cfg = loadContextConfig(root);
|
|
138
|
+
const names = opts.fileNames ?? cfg.fileNames;
|
|
139
|
+
const homeDir = opts.homeDir ?? os.homedir();
|
|
140
|
+
const registry = opts.registry ?? new ContextRegistry();
|
|
141
|
+
const loaded = [];
|
|
142
|
+
for (const dir of getDirectoryChain(root, accessedDir)) {
|
|
143
|
+
for (const probe of [dir, ...SUBDIR_PROBES.map((s) => path.join(dir, s))]) {
|
|
144
|
+
loaded.push(...collectDir(probe, 'jit', names, root, registry, homeDir, cfg));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return budgetMerge(loaded, opts.budgetBytes ?? cfg.jitMaxBytes);
|
|
148
|
+
}
|
|
149
|
+
/** Directory chain from `root` to `target` (inclusive). */
|
|
150
|
+
function getDirectoryChain(root, target) {
|
|
151
|
+
const resolved = path.resolve(target);
|
|
152
|
+
const resolvedRoot = path.resolve(root);
|
|
153
|
+
const chain = [resolvedRoot];
|
|
154
|
+
const rel = path.relative(resolvedRoot, resolved);
|
|
155
|
+
if (!rel || rel.startsWith('..'))
|
|
156
|
+
return chain;
|
|
157
|
+
let current = resolvedRoot;
|
|
158
|
+
for (const seg of rel.split(path.sep).filter(Boolean)) {
|
|
159
|
+
current = path.join(current, seg);
|
|
160
|
+
chain.push(current);
|
|
161
|
+
}
|
|
162
|
+
return chain;
|
|
163
|
+
}
|
|
164
|
+
/** Variant filenames for a base name, most-specific first. */
|
|
165
|
+
function variantNames(base) {
|
|
166
|
+
const ext = path.extname(base); // '.md'
|
|
167
|
+
const stem = ext ? base.slice(0, base.length - ext.length) : base;
|
|
168
|
+
return [
|
|
169
|
+
{ name: `${stem}.local${ext}`, variant: 'local' },
|
|
170
|
+
{ name: `${stem}.override${ext}`, variant: 'override' },
|
|
171
|
+
{ name: base, variant: 'base' },
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Collect every accepted instruction file present in one directory (composed),
|
|
176
|
+
* applying variant precedence, excludes, realpath dedup and @import resolution.
|
|
177
|
+
*/
|
|
178
|
+
function collectDir(probeDir, tier, names, root, registry, homeDir, cfg) {
|
|
179
|
+
const out = [];
|
|
180
|
+
const isGlobal = tier === 'global';
|
|
181
|
+
for (const base of names) {
|
|
182
|
+
// First existing variant wins (local > override > base) and replaces it.
|
|
183
|
+
let chosen = null;
|
|
184
|
+
for (const v of variantNames(base)) {
|
|
185
|
+
const filePath = path.join(probeDir, v.name);
|
|
186
|
+
try {
|
|
187
|
+
if (fs.statSync(filePath).isFile()) {
|
|
188
|
+
chosen = { filePath, variant: v.variant };
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
/* not present */
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!chosen)
|
|
197
|
+
continue;
|
|
198
|
+
// Project files honor codebuddyMdExcludes; global files are never excluded.
|
|
199
|
+
if (!isGlobal && shouldExcludeInstructionFile(chosen.filePath, root))
|
|
200
|
+
continue;
|
|
201
|
+
let realpath;
|
|
202
|
+
try {
|
|
203
|
+
realpath = fs.realpathSync(chosen.filePath);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
realpath = chosen.filePath;
|
|
207
|
+
}
|
|
208
|
+
if (registry.has(realpath))
|
|
209
|
+
continue;
|
|
210
|
+
let body;
|
|
211
|
+
try {
|
|
212
|
+
body = fs.readFileSync(chosen.filePath, 'utf-8');
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (!body.trim())
|
|
218
|
+
continue;
|
|
219
|
+
// User-authored, trusted files: warn (don't drop) on shell-exec-like
|
|
220
|
+
// patterns — dropping would break legitimate AGENTS.md that documents them.
|
|
221
|
+
if (containsDangerousPatterns(body)) {
|
|
222
|
+
logger.warn(`Context file ${chosen.filePath} contains shell-exec-like patterns; loading anyway (user-authored).`);
|
|
223
|
+
}
|
|
224
|
+
body = resolveImportDirectives(body, {
|
|
225
|
+
baseDir: probeDir,
|
|
226
|
+
projectRoot: root,
|
|
227
|
+
homeDir,
|
|
228
|
+
maxDepth: cfg.importMaxDepth,
|
|
229
|
+
maxBytes: cfg.importMaxBytes,
|
|
230
|
+
});
|
|
231
|
+
registry.add(realpath);
|
|
232
|
+
out.push({
|
|
233
|
+
path: chosen.filePath,
|
|
234
|
+
realpath,
|
|
235
|
+
relPath: displayPath(chosen.filePath, root, homeDir, isGlobal),
|
|
236
|
+
tier,
|
|
237
|
+
variant: chosen.variant,
|
|
238
|
+
body,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
/** Deterministic, abs-path-free display path for the origin header. */
|
|
244
|
+
function displayPath(filePath, root, homeDir, isGlobal) {
|
|
245
|
+
if (isGlobal) {
|
|
246
|
+
const rel = path.relative(path.join(homeDir, '.codebuddy'), filePath).replace(/\\/g, '/');
|
|
247
|
+
return `~/.codebuddy/${rel}`;
|
|
248
|
+
}
|
|
249
|
+
return path.relative(root, filePath).replace(/\\/g, '/') || path.basename(filePath);
|
|
250
|
+
}
|
|
251
|
+
/** Merge loaded sources under a budget, truncating the overflowing source. */
|
|
252
|
+
function budgetMerge(sources, budget) {
|
|
253
|
+
let total = 0;
|
|
254
|
+
let truncated = false;
|
|
255
|
+
const parts = [];
|
|
256
|
+
const outSources = [];
|
|
257
|
+
for (const s of sources) {
|
|
258
|
+
if (total >= budget) {
|
|
259
|
+
truncated = true;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
let body = s.body;
|
|
263
|
+
let sourceTruncated = false;
|
|
264
|
+
const remaining = budget - total;
|
|
265
|
+
if (body.length > remaining) {
|
|
266
|
+
body = body.slice(0, remaining) + TRUNCATION_MARKER;
|
|
267
|
+
sourceTruncated = true;
|
|
268
|
+
truncated = true;
|
|
269
|
+
}
|
|
270
|
+
parts.push(`<!-- context: ${s.relPath} (${s.tier}) -->\n${body}`);
|
|
271
|
+
total += body.length;
|
|
272
|
+
outSources.push({
|
|
273
|
+
path: s.path,
|
|
274
|
+
realpath: s.realpath,
|
|
275
|
+
relPath: s.relPath,
|
|
276
|
+
tier: s.tier,
|
|
277
|
+
variant: s.variant,
|
|
278
|
+
bytes: body.length,
|
|
279
|
+
truncated: sourceTruncated,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return { text: parts.join('\n\n'), sources: outSources, bytes: total, truncated };
|
|
283
|
+
}
|
|
284
|
+
/** Shell-exec-like patterns — used to WARN (not drop) on user-authored files. */
|
|
285
|
+
function containsDangerousPatterns(content) {
|
|
286
|
+
return [
|
|
287
|
+
/\beval\s*\(/,
|
|
288
|
+
/\bnew\s+Function\s*\(/,
|
|
289
|
+
/\brequire\s*\(\s*['"]child_process['"]\s*\)/,
|
|
290
|
+
/\bexec(?:Sync)?\s*\(/,
|
|
291
|
+
/\bspawn(?:Sync)?\s*\(/,
|
|
292
|
+
/<script\b/i,
|
|
293
|
+
].some((p) => p.test(content));
|
|
294
|
+
}
|
|
295
|
+
//# sourceMappingURL=project-context.js.map
|
|
@@ -37,6 +37,15 @@ export interface TaskExecutionResult {
|
|
|
37
37
|
output?: string;
|
|
38
38
|
}
|
|
39
39
|
export type TaskExecutor = (task: ColabTask, model: AutonomousModelChoice) => Promise<TaskExecutionResult>;
|
|
40
|
+
/**
|
|
41
|
+
* Idle-time self-improvement hook. Runs ONE bounded improvement cycle and reports
|
|
42
|
+
* whether it kept anything. Injected so the loop stays testable and the heavy
|
|
43
|
+
* engine is swappable; the default (lazy) hook runs the tool-improvement engine.
|
|
44
|
+
*/
|
|
45
|
+
export type SelfImproveHook = () => Promise<{
|
|
46
|
+
applied: boolean;
|
|
47
|
+
detail: string;
|
|
48
|
+
}>;
|
|
40
49
|
export interface AutonomousLoopConfig {
|
|
41
50
|
store: FleetColabStore;
|
|
42
51
|
tierConfig: ModelTierConfig;
|
|
@@ -49,9 +58,19 @@ export interface AutonomousLoopConfig {
|
|
|
49
58
|
* goal-mode tasks complete like plain tasks (no judge gate).
|
|
50
59
|
*/
|
|
51
60
|
goalJudge?: ColabGoalJudge;
|
|
61
|
+
/**
|
|
62
|
+
* Idle-time self-improvement. When the queue is empty AND
|
|
63
|
+
* `CODEBUDDY_SELF_IMPROVE=true`, run one bounded improvement cycle (cooldown-
|
|
64
|
+
* gated). Injected for tests; defaults to the tool-improvement engine.
|
|
65
|
+
*/
|
|
66
|
+
selfImprove?: SelfImproveHook;
|
|
67
|
+
/** Minimum ms between idle self-improvement cycles (default 15 min). */
|
|
68
|
+
selfImproveCooldownMs?: number;
|
|
69
|
+
/** Clock for the cooldown (tests). Default Date.now. */
|
|
70
|
+
now?: () => number;
|
|
52
71
|
}
|
|
53
72
|
export interface TickResult {
|
|
54
|
-
outcome: 'disabled' | 'idle' | 'completed' | 'failed' | 'saturated' | 'goal_continue' | 'goal_blocked';
|
|
73
|
+
outcome: 'disabled' | 'idle' | 'completed' | 'failed' | 'saturated' | 'goal_continue' | 'goal_blocked' | 'self_improved';
|
|
55
74
|
taskId?: string;
|
|
56
75
|
taskTitle?: string;
|
|
57
76
|
model?: AutonomousModelChoice;
|
|
@@ -71,7 +90,18 @@ export declare class FleetAutonomousLoop {
|
|
|
71
90
|
*/
|
|
72
91
|
private readonly failures;
|
|
73
92
|
private readonly goalJudge;
|
|
93
|
+
private readonly selfImprove;
|
|
94
|
+
private readonly selfImproveCooldownMs;
|
|
95
|
+
private readonly now;
|
|
96
|
+
private lastSelfImproveAt;
|
|
74
97
|
constructor(config: AutonomousLoopConfig);
|
|
98
|
+
/**
|
|
99
|
+
* Idle hook: when the queue is empty and self-improvement is opted in, run one
|
|
100
|
+
* bounded, cooldown-gated improvement cycle instead of sitting fully idle.
|
|
101
|
+
* Double-gated (idle + CODEBUDDY_SELF_IMPROVE) and bounded (cooldown + the
|
|
102
|
+
* engine no-ops once seed scenarios are covered). Never throws.
|
|
103
|
+
*/
|
|
104
|
+
private maybeSelfImprove;
|
|
75
105
|
/** Run a single autonomous tick. Never throws — failures are logged + reported. */
|
|
76
106
|
tick(): Promise<TickResult>;
|
|
77
107
|
/**
|