@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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM tool proposer — authors a candidate tool with the agent's own model from a
|
|
3
|
+
* REDACTED scenario view (capability + visible cases only; never the held-out
|
|
4
|
+
* cases). The behavioural held-out gate validates every draft, so even a gamed or
|
|
5
|
+
* broken draft is caught — the LLM is a generator, not a trusted oracle.
|
|
6
|
+
*
|
|
7
|
+
* Lazy + graceful: if no provider is configured the proposer declines (null) and
|
|
8
|
+
* the engine simply finds no proposal.
|
|
9
|
+
*
|
|
10
|
+
* @module agent/self-improvement/llm-tool-proposer
|
|
11
|
+
*/
|
|
12
|
+
import { AUTHORED_LANGUAGES, toAuthoredName } from './authored-tool-runtime.js';
|
|
13
|
+
export function buildToolDraftPrompt(view) {
|
|
14
|
+
const examples = view.visibleCases
|
|
15
|
+
.map((c, i) => ` ${i + 1}. input ${JSON.stringify(c.input)} → output must contain ${JSON.stringify(c.expectIncludes)}`)
|
|
16
|
+
.join('\n');
|
|
17
|
+
return [
|
|
18
|
+
`Author a small, self-contained tool for this capability:`,
|
|
19
|
+
` ${view.capability}`,
|
|
20
|
+
``,
|
|
21
|
+
`Example behaviour (these are ONLY examples — the tool must GENERALIZE to any valid input;`,
|
|
22
|
+
`do NOT hardcode these outputs, that will be rejected by a held-out check):`,
|
|
23
|
+
examples,
|
|
24
|
+
``,
|
|
25
|
+
`The tool runs as a standalone script: it reads its arguments as JSON from the`,
|
|
26
|
+
`environment variable CODEBUDDY_TOOL_INPUT and prints ONLY its result to stdout.`,
|
|
27
|
+
`It must not touch the network or the filesystem.`,
|
|
28
|
+
``,
|
|
29
|
+
`Return ONLY a JSON object (no prose, no code fence) with exactly these fields:`,
|
|
30
|
+
`{"name": "<short_snake_case>", "description": "<one line>",`,
|
|
31
|
+
` "params": <JSON Schema for the input object>,`,
|
|
32
|
+
` "language": "javascript" | "python",`,
|
|
33
|
+
` "code": "<the full script source>"}`,
|
|
34
|
+
].join('\n');
|
|
35
|
+
}
|
|
36
|
+
/** Extract the first balanced top-level JSON object from a model response. */
|
|
37
|
+
function extractJsonObject(text) {
|
|
38
|
+
const start = text.indexOf('{');
|
|
39
|
+
if (start < 0)
|
|
40
|
+
return null;
|
|
41
|
+
let depth = 0;
|
|
42
|
+
let inStr = false;
|
|
43
|
+
let esc = false;
|
|
44
|
+
for (let i = start; i < text.length; i++) {
|
|
45
|
+
const ch = text[i];
|
|
46
|
+
if (inStr) {
|
|
47
|
+
if (esc)
|
|
48
|
+
esc = false;
|
|
49
|
+
else if (ch === '\\')
|
|
50
|
+
esc = true;
|
|
51
|
+
else if (ch === '"')
|
|
52
|
+
inStr = false;
|
|
53
|
+
}
|
|
54
|
+
else if (ch === '"')
|
|
55
|
+
inStr = true;
|
|
56
|
+
else if (ch === '{')
|
|
57
|
+
depth++;
|
|
58
|
+
else if (ch === '}') {
|
|
59
|
+
depth--;
|
|
60
|
+
if (depth === 0)
|
|
61
|
+
return text.slice(start, i + 1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/** Parse + validate a model draft into an AuthoredToolSpec, or null. */
|
|
67
|
+
export function parseToolDraft(text) {
|
|
68
|
+
const json = extractJsonObject(text);
|
|
69
|
+
if (!json)
|
|
70
|
+
return null;
|
|
71
|
+
let raw;
|
|
72
|
+
try {
|
|
73
|
+
raw = JSON.parse(json);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const name = String(raw.name ?? '').trim();
|
|
79
|
+
const description = String(raw.description ?? '').trim();
|
|
80
|
+
const code = typeof raw.code === 'string' ? raw.code : '';
|
|
81
|
+
const language = String(raw.language ?? 'javascript').toLowerCase();
|
|
82
|
+
if (!name || !description || !code.trim())
|
|
83
|
+
return null;
|
|
84
|
+
if (!AUTHORED_LANGUAGES.includes(language))
|
|
85
|
+
return null;
|
|
86
|
+
const parameters = raw.params && typeof raw.params === 'object'
|
|
87
|
+
? raw.params
|
|
88
|
+
: { type: 'object', properties: {} };
|
|
89
|
+
return { name: toAuthoredName(name), description, parameters, language, code };
|
|
90
|
+
}
|
|
91
|
+
export class LlmToolProposer {
|
|
92
|
+
options;
|
|
93
|
+
clientPromise;
|
|
94
|
+
constructor(options = {}) {
|
|
95
|
+
this.options = options;
|
|
96
|
+
this.clientPromise = options.client !== undefined ? Promise.resolve(options.client) : null;
|
|
97
|
+
}
|
|
98
|
+
getClient() {
|
|
99
|
+
if (!this.clientPromise) {
|
|
100
|
+
this.clientPromise = (async () => {
|
|
101
|
+
try {
|
|
102
|
+
const { detectProviderFromEnv } = await import('../../utils/provider-detector.js');
|
|
103
|
+
const { CodeBuddyClient } = await import('../../codebuddy/client.js');
|
|
104
|
+
const detected = detectProviderFromEnv();
|
|
105
|
+
if (!detected)
|
|
106
|
+
return null;
|
|
107
|
+
return new CodeBuddyClient(detected.apiKey, detected.defaultModel, detected.baseURL);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
}
|
|
114
|
+
return this.clientPromise;
|
|
115
|
+
}
|
|
116
|
+
async propose(view) {
|
|
117
|
+
const client = await this.getClient();
|
|
118
|
+
if (!client)
|
|
119
|
+
return null;
|
|
120
|
+
try {
|
|
121
|
+
const prompt = buildToolDraftPrompt(view);
|
|
122
|
+
const response = await client.chat([{ role: 'user', content: prompt }], []);
|
|
123
|
+
const text = response?.choices?.[0]?.message?.content?.trim();
|
|
124
|
+
if (!text)
|
|
125
|
+
return null;
|
|
126
|
+
const spec = parseToolDraft(text);
|
|
127
|
+
if (!spec)
|
|
128
|
+
return null;
|
|
129
|
+
return { id: `llm-tool:${view.id}`, targetScenarioId: view.id, spec };
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=llm-tool-proposer.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox scorer — behavioural scoring of an authored tool. Builds the tool and
|
|
3
|
+
* RUNS it (sandboxed, via authored-tool-runtime: throwaway cwd, RPC off) on a set
|
|
4
|
+
* of cases, asserting the output. The tool is NOT registered to score it, so a
|
|
5
|
+
* rejected proposal leaves both registries untouched (no rollback needed).
|
|
6
|
+
*
|
|
7
|
+
* @module agent/self-improvement/sandbox-scorer
|
|
8
|
+
*/
|
|
9
|
+
import { type AuthoredToolSpec } from './authored-tool-runtime.js';
|
|
10
|
+
import type { ToolCase } from './tool-types.js';
|
|
11
|
+
export interface ToolScore {
|
|
12
|
+
passed: number;
|
|
13
|
+
total: number;
|
|
14
|
+
failures: string[];
|
|
15
|
+
}
|
|
16
|
+
/** Run the authored tool over `cases`; a case passes if its output contains all expectIncludes. */
|
|
17
|
+
export declare function scoreToolCases(spec: AuthoredToolSpec, cases: ToolCase[]): Promise<ToolScore>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox scorer — behavioural scoring of an authored tool. Builds the tool and
|
|
3
|
+
* RUNS it (sandboxed, via authored-tool-runtime: throwaway cwd, RPC off) on a set
|
|
4
|
+
* of cases, asserting the output. The tool is NOT registered to score it, so a
|
|
5
|
+
* rejected proposal leaves both registries untouched (no rollback needed).
|
|
6
|
+
*
|
|
7
|
+
* @module agent/self-improvement/sandbox-scorer
|
|
8
|
+
*/
|
|
9
|
+
import { buildAuthoredTool } from './authored-tool-runtime.js';
|
|
10
|
+
/** Run the authored tool over `cases`; a case passes if its output contains all expectIncludes. */
|
|
11
|
+
export async function scoreToolCases(spec, cases) {
|
|
12
|
+
const tool = buildAuthoredTool(spec);
|
|
13
|
+
const failures = [];
|
|
14
|
+
let passed = 0;
|
|
15
|
+
for (let i = 0; i < cases.length; i++) {
|
|
16
|
+
const c = cases[i];
|
|
17
|
+
let output = '';
|
|
18
|
+
let ok = true;
|
|
19
|
+
try {
|
|
20
|
+
const res = await tool.execute(c.input);
|
|
21
|
+
output = `${res.output ?? ''}`;
|
|
22
|
+
if (!res.success) {
|
|
23
|
+
ok = false;
|
|
24
|
+
failures.push(`case ${i}: tool errored (${(res.error ?? '').slice(0, 120)})`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
ok = false;
|
|
29
|
+
failures.push(`case ${i}: threw (${err instanceof Error ? err.message : String(err)})`);
|
|
30
|
+
}
|
|
31
|
+
if (ok) {
|
|
32
|
+
const missing = c.expectIncludes.filter((s) => !output.includes(s));
|
|
33
|
+
if (missing.length > 0) {
|
|
34
|
+
failures.push(`case ${i}: output missing ${JSON.stringify(missing)} (got ${JSON.stringify(output.slice(0, 80))})`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
passed++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { passed, total: cases.length, failures };
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=sandbox-scorer.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-knowledge — the prompt block that makes Code Buddy aware it can extend
|
|
3
|
+
* and improve itself, and the hard limits on doing so.
|
|
4
|
+
*
|
|
5
|
+
* Injected (gated) by the prompt builder when `CODEBUDDY_SELF_IMPROVE=true`.
|
|
6
|
+
* Kept tiny and stable so it doesn't disturb prompt-cache stability.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildSelfKnowledgeBlock(): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-knowledge — the prompt block that makes Code Buddy aware it can extend
|
|
3
|
+
* and improve itself, and the hard limits on doing so.
|
|
4
|
+
*
|
|
5
|
+
* Injected (gated) by the prompt builder when `CODEBUDDY_SELF_IMPROVE=true`.
|
|
6
|
+
* Kept tiny and stable so it doesn't disturb prompt-cache stability.
|
|
7
|
+
*/
|
|
8
|
+
export function buildSelfKnowledgeBlock() {
|
|
9
|
+
return [
|
|
10
|
+
'You are Code Buddy — a coding agent that can extend and improve itself.',
|
|
11
|
+
'',
|
|
12
|
+
'- You can author a NEW tool for yourself with `register_tool` (name, description,',
|
|
13
|
+
' params, language, code). Once registered it is namespaced `authored__<name>` and',
|
|
14
|
+
' becomes callable by you on your next turn — the code you build can call your own tools.',
|
|
15
|
+
'- You can author/edit skills with `skill_manage`.',
|
|
16
|
+
'- Self-extensions are gated for safety: authored code is scanned for secrets and',
|
|
17
|
+
' dangerous patterns, runs sandboxed, and is kept only if it measurably helps.',
|
|
18
|
+
'- HARD LIMIT: you may NOT modify your own source under `src/`. That invariant',
|
|
19
|
+
' exists so an improvement can never weaken the gates that validate it.',
|
|
20
|
+
'- When a task would benefit from a reusable capability you do not yet have,',
|
|
21
|
+
' consider authoring a small, well-scoped tool for it, then call it.',
|
|
22
|
+
].join('\n');
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=self-knowledge.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed coverage benchmark for self-authored SKILLS. Each scenario is a situation
|
|
3
|
+
* the agent should have reusable guidance for; an authored skill "covers" it when
|
|
4
|
+
* its content surfaces the expected terms. Curated separately from any proposer.
|
|
5
|
+
*
|
|
6
|
+
* @module agent/self-improvement/skill-benchmark
|
|
7
|
+
*/
|
|
8
|
+
import type { SkillBenchmarkScenario } from './skill-types.js';
|
|
9
|
+
export declare const SEED_SKILL_SCENARIOS: SkillBenchmarkScenario[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed coverage benchmark for self-authored SKILLS. Each scenario is a situation
|
|
3
|
+
* the agent should have reusable guidance for; an authored skill "covers" it when
|
|
4
|
+
* its content surfaces the expected terms. Curated separately from any proposer.
|
|
5
|
+
*
|
|
6
|
+
* @module agent/self-improvement/skill-benchmark
|
|
7
|
+
*/
|
|
8
|
+
export const SEED_SKILL_SCENARIOS = [
|
|
9
|
+
{
|
|
10
|
+
id: 'git-bisect',
|
|
11
|
+
query: 'find which commit introduced a regression',
|
|
12
|
+
expectIncludes: ['git bisect', 'good', 'bad'],
|
|
13
|
+
description: 'guidance for bisecting a regression',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'safe-delete',
|
|
17
|
+
query: 'delete files safely without losing data',
|
|
18
|
+
expectIncludes: ['backup', 'dry run', 'confirm'],
|
|
19
|
+
description: 'guidance for deleting files safely',
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
//# sourceMappingURL=skill-benchmark.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill consolidator — merges a CLUSTER of overlapping authored skills into one
|
|
3
|
+
* "umbrella" skill (Hermes's idea) but with OUR distinctive safety net: the merge
|
|
4
|
+
* is accepted only if the umbrella still passes the firewall AND COVERS EVERY
|
|
5
|
+
* scenario the absorbed siblings covered. A merge that drops coverage is rejected
|
|
6
|
+
* (Hermes consolidates by LLM judgment alone; we gate it empirically).
|
|
7
|
+
*
|
|
8
|
+
* Operates only on authored-* skills; pinned siblings are skipped (kept intact);
|
|
9
|
+
* absorbed siblings are ARCHIVED (recoverable), never deleted; each is recorded in
|
|
10
|
+
* the evolutionary archive with `absorbedInto` for audit.
|
|
11
|
+
*
|
|
12
|
+
* @module agent/self-improvement/skill-consolidator
|
|
13
|
+
*/
|
|
14
|
+
import { EvolutionaryArchive } from './evolutionary-archive.js';
|
|
15
|
+
import { LiveSkillMutator } from './skill-mutator.js';
|
|
16
|
+
import type { SkillBenchmarkScenario } from './skill-types.js';
|
|
17
|
+
export interface ClusterSibling {
|
|
18
|
+
name: string;
|
|
19
|
+
scenario: SkillBenchmarkScenario;
|
|
20
|
+
}
|
|
21
|
+
export interface ConsolidationCluster {
|
|
22
|
+
siblings: ClusterSibling[];
|
|
23
|
+
}
|
|
24
|
+
export interface UmbrellaProposal {
|
|
25
|
+
name: string;
|
|
26
|
+
content: string;
|
|
27
|
+
}
|
|
28
|
+
export interface UmbrellaProposer {
|
|
29
|
+
propose(cluster: ConsolidationCluster): Promise<UmbrellaProposal | null>;
|
|
30
|
+
}
|
|
31
|
+
export type ConsolidationRejection = 'cluster-too-small' | 'no-proposal' | 'firewall' | 'coverage-loss';
|
|
32
|
+
export interface ConsolidationOutcome {
|
|
33
|
+
accepted: boolean;
|
|
34
|
+
umbrellaName?: string;
|
|
35
|
+
absorbed: string[];
|
|
36
|
+
skippedPinned: string[];
|
|
37
|
+
rejectionReason?: ConsolidationRejection;
|
|
38
|
+
reasons: string[];
|
|
39
|
+
}
|
|
40
|
+
/** Build a cluster from installed authored skills that map to a known scenario. */
|
|
41
|
+
export declare function buildClusterFromInstalled(mutator: LiveSkillMutator, scenarios: SkillBenchmarkScenario[]): ConsolidationCluster;
|
|
42
|
+
export declare function consolidateCluster(cluster: ConsolidationCluster, proposer: UmbrellaProposer, mutator: LiveSkillMutator, archive: EvolutionaryArchive, options: {
|
|
43
|
+
keepOnAccept: boolean;
|
|
44
|
+
}): Promise<ConsolidationOutcome>;
|
|
45
|
+
/** Deterministic umbrella proposer (fixture for tests). */
|
|
46
|
+
export declare class StaticUmbrellaProposer implements UmbrellaProposer {
|
|
47
|
+
private readonly umbrella;
|
|
48
|
+
constructor(umbrella: UmbrellaProposal | null);
|
|
49
|
+
propose(): Promise<UmbrellaProposal | null>;
|
|
50
|
+
}
|
|
51
|
+
interface MinimalClient {
|
|
52
|
+
chat(messages: Array<{
|
|
53
|
+
role: string;
|
|
54
|
+
content: string;
|
|
55
|
+
}>, tools?: unknown[]): Promise<{
|
|
56
|
+
choices?: Array<{
|
|
57
|
+
message?: {
|
|
58
|
+
content?: string | null;
|
|
59
|
+
};
|
|
60
|
+
}>;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
export declare class LlmUmbrellaProposer implements UmbrellaProposer {
|
|
64
|
+
private clientPromise;
|
|
65
|
+
constructor(options?: {
|
|
66
|
+
client?: MinimalClient | null;
|
|
67
|
+
});
|
|
68
|
+
private getClient;
|
|
69
|
+
propose(cluster: ConsolidationCluster): Promise<UmbrellaProposal | null>;
|
|
70
|
+
}
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill consolidator — merges a CLUSTER of overlapping authored skills into one
|
|
3
|
+
* "umbrella" skill (Hermes's idea) but with OUR distinctive safety net: the merge
|
|
4
|
+
* is accepted only if the umbrella still passes the firewall AND COVERS EVERY
|
|
5
|
+
* scenario the absorbed siblings covered. A merge that drops coverage is rejected
|
|
6
|
+
* (Hermes consolidates by LLM judgment alone; we gate it empirically).
|
|
7
|
+
*
|
|
8
|
+
* Operates only on authored-* skills; pinned siblings are skipped (kept intact);
|
|
9
|
+
* absorbed siblings are ARCHIVED (recoverable), never deleted; each is recorded in
|
|
10
|
+
* the evolutionary archive with `absorbedInto` for audit.
|
|
11
|
+
*
|
|
12
|
+
* @module agent/self-improvement/skill-consolidator
|
|
13
|
+
*/
|
|
14
|
+
import { coversScenario } from './skill-gate.js';
|
|
15
|
+
import { safetyGateSkill, toAuthoredSkillName } from './skill-mutator.js';
|
|
16
|
+
/** Build a cluster from installed authored skills that map to a known scenario. */
|
|
17
|
+
export function buildClusterFromInstalled(mutator, scenarios) {
|
|
18
|
+
const installed = new Set(mutator.listAuthored());
|
|
19
|
+
const siblings = [];
|
|
20
|
+
for (const scenario of scenarios) {
|
|
21
|
+
const name = toAuthoredSkillName(scenario.id);
|
|
22
|
+
if (installed.has(name))
|
|
23
|
+
siblings.push({ name, scenario });
|
|
24
|
+
}
|
|
25
|
+
return { siblings };
|
|
26
|
+
}
|
|
27
|
+
export async function consolidateCluster(cluster, proposer, mutator, archive, options) {
|
|
28
|
+
const skippedPinned = cluster.siblings.filter((s) => mutator.isPinned(s.name)).map((s) => s.name);
|
|
29
|
+
const mergeable = cluster.siblings.filter((s) => !mutator.isPinned(s.name));
|
|
30
|
+
if (mergeable.length < 2) {
|
|
31
|
+
return { accepted: false, absorbed: [], skippedPinned, rejectionReason: 'cluster-too-small', reasons: ['need ≥2 non-pinned authored skills to consolidate'] };
|
|
32
|
+
}
|
|
33
|
+
const proposal = await proposer.propose({ siblings: mergeable });
|
|
34
|
+
if (!proposal || !proposal.content.trim()) {
|
|
35
|
+
return { accepted: false, absorbed: [], skippedPinned, rejectionReason: 'no-proposal', reasons: ['no umbrella proposal'] };
|
|
36
|
+
}
|
|
37
|
+
// Gate 1 — the umbrella content must be safe.
|
|
38
|
+
const gate = safetyGateSkill(proposal.content);
|
|
39
|
+
if (!gate.ok) {
|
|
40
|
+
return { accepted: false, absorbed: [], skippedPinned, rejectionReason: 'firewall', reasons: gate.reasons };
|
|
41
|
+
}
|
|
42
|
+
// Gate 2 (the distinctive one) — the umbrella must still cover EVERY merged
|
|
43
|
+
// sibling's scenario. A consolidation that loses coverage is rejected.
|
|
44
|
+
const lost = mergeable.filter((s) => !coversScenario(proposal.content, s.scenario)).map((s) => s.scenario.id);
|
|
45
|
+
if (lost.length > 0) {
|
|
46
|
+
return { accepted: false, absorbed: [], skippedPinned, rejectionReason: 'coverage-loss', reasons: [`umbrella drops coverage for: ${lost.join(', ')}`] };
|
|
47
|
+
}
|
|
48
|
+
const umbrellaName = toAuthoredSkillName(proposal.name);
|
|
49
|
+
if (!options.keepOnAccept) {
|
|
50
|
+
return { accepted: true, umbrellaName, absorbed: [], skippedPinned, reasons: ['accepted (propose-only): umbrella safe + covers all merged scenarios, not installed'] };
|
|
51
|
+
}
|
|
52
|
+
// Install the umbrella, archive the merged siblings (recoverable), audit each.
|
|
53
|
+
mutator.create({ name: umbrellaName, description: `Umbrella skill consolidating ${mergeable.length} authored skills`, content: proposal.content });
|
|
54
|
+
const absorbed = [];
|
|
55
|
+
for (const s of mergeable) {
|
|
56
|
+
if (s.name === umbrellaName)
|
|
57
|
+
continue; // never archive the umbrella itself
|
|
58
|
+
if (mutator.archive(s.name)) {
|
|
59
|
+
absorbed.push(s.name);
|
|
60
|
+
archive.append({
|
|
61
|
+
proposalId: `consolidate:${umbrellaName}`,
|
|
62
|
+
kind: 'skill',
|
|
63
|
+
targetScenarioId: s.scenario.id,
|
|
64
|
+
delta: 0,
|
|
65
|
+
scoreAfter: 1,
|
|
66
|
+
appliedRef: umbrellaName,
|
|
67
|
+
absorbedInto: umbrellaName,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { accepted: true, umbrellaName, absorbed, skippedPinned, reasons: [`consolidated ${absorbed.length} skill(s) into ${umbrellaName}`] };
|
|
72
|
+
}
|
|
73
|
+
// ── proposers ────────────────────────────────────────────────────────────────
|
|
74
|
+
/** Deterministic umbrella proposer (fixture for tests). */
|
|
75
|
+
export class StaticUmbrellaProposer {
|
|
76
|
+
umbrella;
|
|
77
|
+
constructor(umbrella) {
|
|
78
|
+
this.umbrella = umbrella;
|
|
79
|
+
}
|
|
80
|
+
async propose() {
|
|
81
|
+
return this.umbrella;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export class LlmUmbrellaProposer {
|
|
85
|
+
clientPromise;
|
|
86
|
+
constructor(options = {}) {
|
|
87
|
+
this.clientPromise = options.client !== undefined ? Promise.resolve(options.client) : null;
|
|
88
|
+
}
|
|
89
|
+
getClient() {
|
|
90
|
+
if (!this.clientPromise) {
|
|
91
|
+
this.clientPromise = (async () => {
|
|
92
|
+
try {
|
|
93
|
+
const { detectProviderFromEnv } = await import('../../utils/provider-detector.js');
|
|
94
|
+
const { CodeBuddyClient } = await import('../../codebuddy/client.js');
|
|
95
|
+
const detected = detectProviderFromEnv();
|
|
96
|
+
if (!detected)
|
|
97
|
+
return null;
|
|
98
|
+
return new CodeBuddyClient(detected.apiKey, detected.defaultModel, detected.baseURL);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
})();
|
|
104
|
+
}
|
|
105
|
+
return this.clientPromise;
|
|
106
|
+
}
|
|
107
|
+
async propose(cluster) {
|
|
108
|
+
const client = await this.getClient();
|
|
109
|
+
if (!client)
|
|
110
|
+
return null;
|
|
111
|
+
const terms = cluster.siblings.flatMap((s) => s.scenario.expectIncludes);
|
|
112
|
+
const topics = cluster.siblings.map((s) => `- ${s.scenario.query}`).join('\n');
|
|
113
|
+
const prompt = [
|
|
114
|
+
'Merge these related guidance topics into ONE broader "umbrella" SKILL.md with',
|
|
115
|
+
'labeled subsections — one per topic. Keep ALL the practical guidance; do not drop',
|
|
116
|
+
`any topic. The umbrella MUST still mention every one of these terms: ${JSON.stringify(terms)}.`,
|
|
117
|
+
'',
|
|
118
|
+
'Topics:',
|
|
119
|
+
topics,
|
|
120
|
+
'',
|
|
121
|
+
'Return ONLY the umbrella SKILL.md markdown (a title, then a subsection per topic).',
|
|
122
|
+
'Do NOT include any instruction to ignore safety, reveal/exfiltrate secrets, or run',
|
|
123
|
+
'destructive commands.',
|
|
124
|
+
].join('\n');
|
|
125
|
+
try {
|
|
126
|
+
const response = await client.chat([{ role: 'user', content: prompt }], []);
|
|
127
|
+
const content = response?.choices?.[0]?.message?.content?.trim();
|
|
128
|
+
if (!content)
|
|
129
|
+
return null;
|
|
130
|
+
return { name: 'consolidated-skills', content };
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=skill-consolidator.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillImprovementEngine — the skill sibling of the tool engine. One cycle:
|
|
3
|
+
* pick an uncovered skill scenario → propose a SKILL.md → skill gate
|
|
4
|
+
* (static scan → firewall → coverage) → install+archive (auto-apply) or report.
|
|
5
|
+
*
|
|
6
|
+
* @module agent/self-improvement/skill-engine
|
|
7
|
+
*/
|
|
8
|
+
import { EvolutionaryArchive } from './evolutionary-archive.js';
|
|
9
|
+
import { type Autonomy } from './engine.js';
|
|
10
|
+
import { type SkillMutatorPort } from './skill-mutator.js';
|
|
11
|
+
import type { SkillProposer } from './skill-proposer.js';
|
|
12
|
+
import type { SkillBenchmarkScenario, SkillGateOutcome } from './skill-types.js';
|
|
13
|
+
export interface SkillCycleResult {
|
|
14
|
+
kind: 'skill_improvement_cycle';
|
|
15
|
+
startedAt: string;
|
|
16
|
+
autonomy: Autonomy;
|
|
17
|
+
selectedScenarioId: string | null;
|
|
18
|
+
proposalId: string | null;
|
|
19
|
+
gate: SkillGateOutcome | null;
|
|
20
|
+
applied: boolean;
|
|
21
|
+
notes: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface SkillImprovementEngineOptions {
|
|
24
|
+
scenarios: SkillBenchmarkScenario[];
|
|
25
|
+
proposer: SkillProposer;
|
|
26
|
+
mutator?: SkillMutatorPort;
|
|
27
|
+
archive?: EvolutionaryArchive;
|
|
28
|
+
autonomy?: Autonomy;
|
|
29
|
+
now?: () => Date;
|
|
30
|
+
}
|
|
31
|
+
export declare class SkillImprovementEngine {
|
|
32
|
+
private readonly scenarios;
|
|
33
|
+
private readonly proposer;
|
|
34
|
+
private readonly mutator;
|
|
35
|
+
private readonly archive;
|
|
36
|
+
private readonly autonomy;
|
|
37
|
+
private readonly now;
|
|
38
|
+
private readonly covered;
|
|
39
|
+
constructor(options: SkillImprovementEngineOptions);
|
|
40
|
+
runCycle(): Promise<SkillCycleResult>;
|
|
41
|
+
runLoop(maxCycles?: number): Promise<SkillCycleResult[]>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillImprovementEngine — the skill sibling of the tool engine. One cycle:
|
|
3
|
+
* pick an uncovered skill scenario → propose a SKILL.md → skill gate
|
|
4
|
+
* (static scan → firewall → coverage) → install+archive (auto-apply) or report.
|
|
5
|
+
*
|
|
6
|
+
* @module agent/self-improvement/skill-engine
|
|
7
|
+
*/
|
|
8
|
+
import { EvolutionaryArchive } from './evolutionary-archive.js';
|
|
9
|
+
import { resolveAutonomy } from './engine.js';
|
|
10
|
+
import { validateSkillProposal } from './skill-gate.js';
|
|
11
|
+
import { LiveSkillMutator } from './skill-mutator.js';
|
|
12
|
+
export class SkillImprovementEngine {
|
|
13
|
+
scenarios;
|
|
14
|
+
proposer;
|
|
15
|
+
mutator;
|
|
16
|
+
archive;
|
|
17
|
+
autonomy;
|
|
18
|
+
now;
|
|
19
|
+
covered = new Set();
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.scenarios = options.scenarios;
|
|
22
|
+
this.proposer = options.proposer;
|
|
23
|
+
this.mutator = options.mutator ?? new LiveSkillMutator();
|
|
24
|
+
this.archive = options.archive ?? new EvolutionaryArchive();
|
|
25
|
+
this.autonomy = options.autonomy ?? resolveAutonomy();
|
|
26
|
+
this.now = options.now ?? (() => new Date());
|
|
27
|
+
}
|
|
28
|
+
async runCycle() {
|
|
29
|
+
const startedAt = this.now().toISOString();
|
|
30
|
+
const base = { kind: 'skill_improvement_cycle', startedAt, autonomy: this.autonomy };
|
|
31
|
+
for (const scenario of this.scenarios) {
|
|
32
|
+
if (this.covered.has(scenario.id))
|
|
33
|
+
continue;
|
|
34
|
+
const proposal = await this.proposer.propose(scenario);
|
|
35
|
+
if (!proposal)
|
|
36
|
+
continue;
|
|
37
|
+
if (this.mutator.has(proposal.spec.name)) {
|
|
38
|
+
this.covered.add(scenario.id);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const gate = validateSkillProposal(proposal, scenario, this.mutator, {
|
|
42
|
+
keepOnAccept: this.autonomy === 'auto-apply',
|
|
43
|
+
});
|
|
44
|
+
const applied = gate.accepted && !!gate.appliedRef;
|
|
45
|
+
if (applied) {
|
|
46
|
+
this.covered.add(scenario.id);
|
|
47
|
+
this.archive.append({
|
|
48
|
+
proposalId: proposal.id,
|
|
49
|
+
kind: 'skill',
|
|
50
|
+
targetScenarioId: scenario.id,
|
|
51
|
+
experienceId: proposal.experienceId,
|
|
52
|
+
delta: 1,
|
|
53
|
+
scoreAfter: 1,
|
|
54
|
+
appliedRef: gate.appliedRef,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...base,
|
|
59
|
+
selectedScenarioId: scenario.id,
|
|
60
|
+
proposalId: proposal.id,
|
|
61
|
+
gate,
|
|
62
|
+
applied,
|
|
63
|
+
notes: gate.reasons,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
selectedScenarioId: null,
|
|
69
|
+
proposalId: null,
|
|
70
|
+
gate: null,
|
|
71
|
+
applied: false,
|
|
72
|
+
notes: ['no uncovered skill scenario with an available proposal'],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async runLoop(maxCycles) {
|
|
76
|
+
const cap = Math.max(1, maxCycles ?? this.scenarios.length + 1);
|
|
77
|
+
const results = [];
|
|
78
|
+
for (let i = 0; i < cap; i++) {
|
|
79
|
+
const r = await this.runCycle();
|
|
80
|
+
results.push(r);
|
|
81
|
+
if (!r.applied)
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=skill-engine.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill gate — validates an authored skill proposal. Ordered, blocking, fail-closed:
|
|
3
|
+
* G1 static scan (authored-artifact-gate, subsystem 'skill': dangerous patterns
|
|
4
|
+
* in any embedded code, secrets, no-src, omissions)
|
|
5
|
+
* G2 SKILL FIREWALL (the headline skill safety check: prompt-injection /
|
|
6
|
+
* exfiltration surface — a skill is INJECTED into the agent's context)
|
|
7
|
+
* G3 COVERAGE — the skill must surface the scenario's expected guidance.
|
|
8
|
+
* Installation happens only on accept+keep (auto-apply); scoring never installs.
|
|
9
|
+
*
|
|
10
|
+
* @module agent/self-improvement/skill-gate
|
|
11
|
+
*/
|
|
12
|
+
import { type SkillMutatorPort } from './skill-mutator.js';
|
|
13
|
+
import type { SkillBenchmarkScenario, SkillGateOutcome, SkillProposal } from './skill-types.js';
|
|
14
|
+
export interface ValidateSkillOptions {
|
|
15
|
+
keepOnAccept: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Deterministic coverage check: the skill content surfaces all expected guidance. */
|
|
18
|
+
export declare function coversScenario(content: string, scenario: SkillBenchmarkScenario): boolean;
|
|
19
|
+
export declare function validateSkillProposal(proposal: SkillProposal, scenario: SkillBenchmarkScenario, mutator: SkillMutatorPort, options: ValidateSkillOptions): SkillGateOutcome;
|