skillmaxxing 0.1.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/.claude-plugin/marketplace.json +11 -0
- package/.claude-plugin/plugin.json +9 -0
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/agents/claude.js +12 -0
- package/dist/agents/codex.js +12 -0
- package/dist/agents/cursor.js +12 -0
- package/dist/agents/hermes.js +12 -0
- package/dist/agents/opencode.js +12 -0
- package/dist/agents/registry.js +22 -0
- package/dist/agents/types.js +1 -0
- package/dist/cli.js +291 -0
- package/dist/commands/discover.js +76 -0
- package/dist/commands/doctor.js +84 -0
- package/dist/commands/init.js +47 -0
- package/dist/commands/install.js +74 -0
- package/dist/commands/list.js +74 -0
- package/dist/commands/optimize.js +152 -0
- package/dist/commands/plugin.js +232 -0
- package/dist/commands/remove.js +48 -0
- package/dist/commands/skillify.js +74 -0
- package/dist/commands/update.js +52 -0
- package/dist/commands/workspace.js +117 -0
- package/dist/create/match.js +23 -0
- package/dist/create/reflect.js +49 -0
- package/dist/create/skillify.js +117 -0
- package/dist/discover/collect.js +40 -0
- package/dist/discover/github.js +27 -0
- package/dist/discover/index.js +39 -0
- package/dist/discover/local.js +55 -0
- package/dist/discover/rank.js +63 -0
- package/dist/discover/types.js +1 -0
- package/dist/eval/runner.js +81 -0
- package/dist/eval/schema.js +78 -0
- package/dist/eval/scorers.js +19 -0
- package/dist/lock/global.js +53 -0
- package/dist/lock/project.js +67 -0
- package/dist/optimize/budget.js +22 -0
- package/dist/optimize/buffer.js +33 -0
- package/dist/optimize/diff.js +89 -0
- package/dist/optimize/loop.js +49 -0
- package/dist/plugin/guidance.js +30 -0
- package/dist/plugin/reflect.js +63 -0
- package/dist/plugin/sessions.js +58 -0
- package/dist/source/parser.js +63 -0
- package/dist/source/resolver.js +120 -0
- package/dist/state/store.js +120 -0
- package/dist/state/trust.js +31 -0
- package/dist/types.js +1 -0
- package/dist/util/collision.js +46 -0
- package/dist/util/exec.js +78 -0
- package/dist/util/frontmatter.js +72 -0
- package/dist/util/fs.js +77 -0
- package/dist/util/git.js +35 -0
- package/dist/util/log.js +33 -0
- package/dist/util/sanitize.js +36 -0
- package/dist/util/similarity.js +27 -0
- package/dist/util/versions.js +104 -0
- package/dist/workspace/channels.js +14 -0
- package/dist/workspace/collab.js +103 -0
- package/dist/workspace/registry.js +113 -0
- package/hooks/hooks.json +26 -0
- package/index/index.json +5 -0
- package/package.json +53 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { ALL_AGENTS } from '../agents/registry.js';
|
|
6
|
+
import { readGlobalLock } from '../lock/global.js';
|
|
7
|
+
import { isSymlink, fileExists } from '../util/fs.js';
|
|
8
|
+
import * as log from '../util/log.js';
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
export async function doctor() {
|
|
11
|
+
log.heading('skill-maxing doctor');
|
|
12
|
+
let issues = 0;
|
|
13
|
+
log.heading('Agent Detection');
|
|
14
|
+
for (const agent of ALL_AGENTS) {
|
|
15
|
+
const installed = await agent.detectInstalled();
|
|
16
|
+
const cliFound = await checkCli(agent.cliCommand);
|
|
17
|
+
if (installed) {
|
|
18
|
+
log.success(`${agent.displayName}: config directory found`);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
log.dim(`${agent.displayName}: not installed`);
|
|
22
|
+
}
|
|
23
|
+
if (cliFound) {
|
|
24
|
+
log.success(`${agent.displayName}: '${agent.cliCommand}' on PATH`);
|
|
25
|
+
}
|
|
26
|
+
else if (installed) {
|
|
27
|
+
log.warn(`${agent.displayName}: config exists but '${agent.cliCommand}' not on PATH`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
log.heading('Global Skills Health');
|
|
31
|
+
const lock = readGlobalLock();
|
|
32
|
+
const entries = Object.entries(lock.skills);
|
|
33
|
+
if (entries.length === 0) {
|
|
34
|
+
log.info('No global skills installed.');
|
|
35
|
+
}
|
|
36
|
+
for (const [name, entry] of entries) {
|
|
37
|
+
for (const agentName of entry.agents) {
|
|
38
|
+
const agent = ALL_AGENTS.find(a => a.name === agentName);
|
|
39
|
+
if (!agent) {
|
|
40
|
+
log.warn(`${name}: references unknown agent '${agentName}'`);
|
|
41
|
+
issues++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const skillDir = path.join(agent.globalSkillsDir, name);
|
|
45
|
+
if (!fileExists(skillDir)) {
|
|
46
|
+
log.error(`${name}: missing at ${skillDir} (agent: ${agent.displayName})`);
|
|
47
|
+
issues++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (isSymlink(skillDir)) {
|
|
51
|
+
const target = fs.readlinkSync(skillDir);
|
|
52
|
+
const resolved = path.resolve(path.dirname(skillDir), target);
|
|
53
|
+
if (!fileExists(resolved)) {
|
|
54
|
+
log.error(`${name}: broken symlink → ${resolved} (agent: ${agent.displayName})`);
|
|
55
|
+
issues++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
60
|
+
if (!fileExists(skillMd)) {
|
|
61
|
+
log.warn(`${name}: directory exists but no SKILL.md (agent: ${agent.displayName})`);
|
|
62
|
+
issues++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
log.success(`${name}: OK (${agent.displayName})`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
log.heading('Summary');
|
|
69
|
+
if (issues === 0) {
|
|
70
|
+
log.success('No issues found.');
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
log.warn(`${issues} issue(s) found. Run 'skill-maxing update' to fix stale installs.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function checkCli(command) {
|
|
77
|
+
try {
|
|
78
|
+
await execFileAsync('which', [command]);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { detectInstalledAgents } from '../agents/registry.js';
|
|
4
|
+
import { ensureDir } from '../util/fs.js';
|
|
5
|
+
import * as log from '../util/log.js';
|
|
6
|
+
const TEMPLATE = `---
|
|
7
|
+
name: my-skill
|
|
8
|
+
description: A brief description of what this skill does
|
|
9
|
+
version: 1.0.0
|
|
10
|
+
tools:
|
|
11
|
+
- Bash
|
|
12
|
+
- Read
|
|
13
|
+
triggers:
|
|
14
|
+
- activate this skill
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# my-skill
|
|
18
|
+
|
|
19
|
+
Instructions for the agent go here.
|
|
20
|
+
|
|
21
|
+
## When to use
|
|
22
|
+
|
|
23
|
+
Describe when this skill should be activated.
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
1. First step
|
|
28
|
+
2. Second step
|
|
29
|
+
3. Third step
|
|
30
|
+
`;
|
|
31
|
+
export async function init(args) {
|
|
32
|
+
const dir = args.dir ?? process.cwd();
|
|
33
|
+
const name = args.name ?? 'my-skill';
|
|
34
|
+
const agents = await detectInstalledAgents();
|
|
35
|
+
log.heading('skill-maxing init');
|
|
36
|
+
log.info(`Detected agents: ${agents.length > 0 ? agents.map(a => a.displayName).join(', ') : 'none'}`);
|
|
37
|
+
const skillDir = path.join(dir, name);
|
|
38
|
+
if (fs.existsSync(skillDir)) {
|
|
39
|
+
log.warn(`Directory already exists: ${skillDir}`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
ensureDir(skillDir);
|
|
43
|
+
const skillMd = TEMPLATE.replace(/my-skill/g, name);
|
|
44
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillMd);
|
|
45
|
+
log.success(`Created ${skillDir}/SKILL.md`);
|
|
46
|
+
log.info(`Edit the SKILL.md, then run: skill-maxing install ./${name}`);
|
|
47
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { parseSource, sourceLabel } from '../source/parser.js';
|
|
3
|
+
import { resolveSource, cleanupResolved } from '../source/resolver.js';
|
|
4
|
+
import { detectInstalledAgents, getAgentOrThrow } from '../agents/registry.js';
|
|
5
|
+
import { symlinkOrCopy, fileExists } from '../util/fs.js';
|
|
6
|
+
import { addGlobalLockEntry, getGlobalLockEntry } from '../lock/global.js';
|
|
7
|
+
import { addProjectLockEntry, computeSkillHash, readProjectLock } from '../lock/project.js';
|
|
8
|
+
import { ensureValidName } from '../util/collision.js';
|
|
9
|
+
import * as log from '../util/log.js';
|
|
10
|
+
export async function install(args) {
|
|
11
|
+
const parsed = parseSource(args.source);
|
|
12
|
+
log.info(`Resolving ${sourceLabel(parsed)}...`);
|
|
13
|
+
const skills = await resolveSource(parsed);
|
|
14
|
+
log.info(`Found ${skills.length} skill(s): ${skills.map(s => s.name).join(', ')}`);
|
|
15
|
+
let agents;
|
|
16
|
+
if (args.agents && args.agents.length > 0) {
|
|
17
|
+
agents = args.agents.map(a => getAgentOrThrow(a));
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
agents = await detectInstalledAgents();
|
|
21
|
+
if (agents.length === 0) {
|
|
22
|
+
log.warn('No supported agents detected. Use --agent to specify one.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
log.info(`Detected agents: ${agents.map(a => a.displayName).join(', ')}`);
|
|
26
|
+
}
|
|
27
|
+
const projectDir = process.cwd();
|
|
28
|
+
for (const skill of skills) {
|
|
29
|
+
const nameCheck = ensureValidName(skill.name);
|
|
30
|
+
if (!nameCheck.ok) {
|
|
31
|
+
log.warn(`Skipping skill: ${nameCheck.reason}`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// A skill already recorded in our lock is managed by skill-maxing and may be
|
|
35
|
+
// refreshed (this is the `update` path). An on-disk skill we do NOT track is
|
|
36
|
+
// unmanaged — refuse to clobber it (it may be a locally-optimized skill)
|
|
37
|
+
// unless --force (review C2).
|
|
38
|
+
const tracked = args.scope === 'global'
|
|
39
|
+
? !!getGlobalLockEntry(skill.name)
|
|
40
|
+
: !!readProjectLock(projectDir).skills[skill.name];
|
|
41
|
+
log.heading(`Installing ${skill.name}`);
|
|
42
|
+
for (const agent of agents) {
|
|
43
|
+
const destDir = args.scope === 'global'
|
|
44
|
+
? path.join(agent.globalSkillsDir, skill.name)
|
|
45
|
+
: path.join(projectDir, agent.projectSkillsDir, skill.name);
|
|
46
|
+
if (fileExists(destDir) && !tracked && !args.force) {
|
|
47
|
+
log.warn(`${skill.name} already exists at ${destDir} and is not managed by skill-maxing. ` +
|
|
48
|
+
`Use --force to overwrite. Skipping.`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const mode = symlinkOrCopy(skill.dir, destDir, args.copy);
|
|
52
|
+
log.success(`${agent.displayName} (${args.scope}): ${mode === 'symlink' ? 'linked' : 'copied'} → ${destDir}`);
|
|
53
|
+
}
|
|
54
|
+
if (args.scope === 'global') {
|
|
55
|
+
addGlobalLockEntry(skill.name, {
|
|
56
|
+
source: parsed.raw,
|
|
57
|
+
sourceType: parsed.type,
|
|
58
|
+
ref: parsed.ref,
|
|
59
|
+
commitSha: skill.commitSha,
|
|
60
|
+
agents: agents.map(a => a.name),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
addProjectLockEntry(projectDir, skill.name, {
|
|
65
|
+
source: parsed.raw,
|
|
66
|
+
sourceType: parsed.type,
|
|
67
|
+
ref: parsed.ref,
|
|
68
|
+
computedHash: computeSkillHash(skill.dir),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
cleanupResolved(skills);
|
|
73
|
+
log.success(`Installed ${skills.length} skill(s) into ${agents.length} agent(s)`);
|
|
74
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { ALL_AGENTS } from '../agents/registry.js';
|
|
4
|
+
import { readSkillMeta } from '../util/frontmatter.js';
|
|
5
|
+
import { isSymlink } from '../util/fs.js';
|
|
6
|
+
import * as log from '../util/log.js';
|
|
7
|
+
export async function list(args) {
|
|
8
|
+
const projectDir = process.cwd();
|
|
9
|
+
const skills = [];
|
|
10
|
+
const agents = args.agent
|
|
11
|
+
? ALL_AGENTS.filter(a => a.name === args.agent)
|
|
12
|
+
: ALL_AGENTS;
|
|
13
|
+
for (const agent of agents) {
|
|
14
|
+
if (!args.scope || args.scope === 'global') {
|
|
15
|
+
skills.push(...scanDir(agent, agent.globalSkillsDir, 'global'));
|
|
16
|
+
}
|
|
17
|
+
if (!args.scope || args.scope === 'project') {
|
|
18
|
+
const projDir = path.join(projectDir, agent.projectSkillsDir);
|
|
19
|
+
skills.push(...scanDir(agent, projDir, 'project'));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (args.json) {
|
|
23
|
+
console.log(JSON.stringify(skills, null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (skills.length === 0) {
|
|
27
|
+
log.info('No skills installed.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
log.heading(`Installed skills (${skills.length})`);
|
|
31
|
+
const rows = [['Name', 'Agent', 'Scope', 'Link', 'Description']];
|
|
32
|
+
for (const s of skills) {
|
|
33
|
+
rows.push([
|
|
34
|
+
s.name,
|
|
35
|
+
s.agent,
|
|
36
|
+
s.scope,
|
|
37
|
+
s.isSymlink ? 'sym' : 'copy',
|
|
38
|
+
truncate(s.meta.description, 50),
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
log.table(rows);
|
|
42
|
+
}
|
|
43
|
+
function scanDir(agent, dir, scope) {
|
|
44
|
+
const results = [];
|
|
45
|
+
try {
|
|
46
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
47
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
48
|
+
continue;
|
|
49
|
+
const skillDir = path.join(dir, entry.name);
|
|
50
|
+
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
51
|
+
if (!fs.existsSync(skillMd))
|
|
52
|
+
continue;
|
|
53
|
+
const content = fs.readFileSync(skillMd, 'utf-8');
|
|
54
|
+
const meta = readSkillMeta(content);
|
|
55
|
+
if (!meta)
|
|
56
|
+
continue;
|
|
57
|
+
results.push({
|
|
58
|
+
name: meta.name,
|
|
59
|
+
meta,
|
|
60
|
+
path: skillDir,
|
|
61
|
+
agent: agent.name,
|
|
62
|
+
scope,
|
|
63
|
+
isSymlink: isSymlink(skillDir),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// directory doesn't exist
|
|
69
|
+
}
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
function truncate(s, max) {
|
|
73
|
+
return s.length <= max ? s : s.substring(0, max - 3) + '...';
|
|
74
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import { ensureDir, removeDir, copyDir, fileExists } from '../util/fs.js';
|
|
5
|
+
import { applyEdits } from '../optimize/diff.js';
|
|
6
|
+
import { editBudget } from '../optimize/budget.js';
|
|
7
|
+
import { selectEdits, gate, noHeldOutRegression } from '../optimize/loop.js';
|
|
8
|
+
import { loadEvalManifest } from '../eval/schema.js';
|
|
9
|
+
import { scoreRollouts } from '../eval/runner.js';
|
|
10
|
+
import { promote, revert } from '../util/versions.js';
|
|
11
|
+
import { loadState, ensureState, saveState, setLifecycle } from '../state/store.js';
|
|
12
|
+
import * as log from '../util/log.js';
|
|
13
|
+
const CANDIDATES_DIR = path.join(os.homedir(), '.skillmax', 'candidates');
|
|
14
|
+
function readJson(p) {
|
|
15
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
16
|
+
}
|
|
17
|
+
function bumpPatch(version) {
|
|
18
|
+
const m = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
19
|
+
if (!m)
|
|
20
|
+
return `${version}+1`;
|
|
21
|
+
return `${m[1]}.${m[2]}.${Number(m[3]) + 1}`;
|
|
22
|
+
}
|
|
23
|
+
export async function optimize(args) {
|
|
24
|
+
switch (args.action) {
|
|
25
|
+
case 'score':
|
|
26
|
+
return scoreAction(args);
|
|
27
|
+
case 'apply':
|
|
28
|
+
return applyAction(args);
|
|
29
|
+
case 'gate':
|
|
30
|
+
return gateAction(args);
|
|
31
|
+
case 'promote':
|
|
32
|
+
return promoteAction(args);
|
|
33
|
+
case 'revert':
|
|
34
|
+
return revertAction(args);
|
|
35
|
+
default:
|
|
36
|
+
log.error(`Unknown optimize action: ${args.action}`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Score rollout outputs against an eval manifest (deterministic; defers agent-judge).
|
|
41
|
+
async function scoreAction(args) {
|
|
42
|
+
if (!args.evalPath || !args.rolloutsPath) {
|
|
43
|
+
log.error('Usage: optimize score --eval <manifest> --rollouts <rollouts.json>');
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const manifest = loadEvalManifest(args.evalPath); // throws on empty/invalid (AE3)
|
|
48
|
+
const rollouts = readJson(args.rolloutsPath);
|
|
49
|
+
const result = await scoreRollouts(manifest, rollouts, {
|
|
50
|
+
skillId: args.skillName,
|
|
51
|
+
allowExec: args.allowExec,
|
|
52
|
+
});
|
|
53
|
+
if (args.json) {
|
|
54
|
+
console.log(JSON.stringify(result, null, 2));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
log.heading(`Score: ${result.aggregate ?? 'n/a'}`);
|
|
58
|
+
for (const t of result.perTask) {
|
|
59
|
+
log.info(` ${t.taskId}: ${t.pending ? 'PENDING (agent-judge)' : t.score}${t.detail ? ` (${t.detail})` : ''}`);
|
|
60
|
+
}
|
|
61
|
+
if (result.pendingJudgments.length > 0) {
|
|
62
|
+
log.warn(`${result.pendingJudgments.length} task(s) need agent-judge scoring before the gate can run.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Apply bounded edits to a managed COPY (never the symlinked source — KTD5).
|
|
66
|
+
function applyAction(args) {
|
|
67
|
+
if (!args.skillDir || !args.editsPath) {
|
|
68
|
+
log.error('Usage: optimize apply --skill-dir <dir> --edits <edits.json> [--step N --total M]');
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const name = args.skillName ?? path.basename(args.skillDir);
|
|
73
|
+
const skillMd = path.join(args.skillDir, 'SKILL.md');
|
|
74
|
+
if (!fileExists(skillMd)) {
|
|
75
|
+
log.error(`no SKILL.md in ${args.skillDir}`);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const candidateDir = path.join(CANDIDATES_DIR, name);
|
|
80
|
+
removeDir(candidateDir);
|
|
81
|
+
ensureDir(path.dirname(candidateDir));
|
|
82
|
+
copyDir(args.skillDir, candidateDir); // managed working copy
|
|
83
|
+
const edits = readJson(args.editsPath);
|
|
84
|
+
const budget = editBudget(args.step ?? 0, args.total ?? 1, {
|
|
85
|
+
base: args.base,
|
|
86
|
+
min: args.min,
|
|
87
|
+
scheduler: args.scheduler,
|
|
88
|
+
});
|
|
89
|
+
const selected = selectEdits(edits, budget);
|
|
90
|
+
const content = fs.readFileSync(path.join(candidateDir, 'SKILL.md'), 'utf-8');
|
|
91
|
+
const result = applyEdits(content, selected);
|
|
92
|
+
fs.writeFileSync(path.join(candidateDir, 'SKILL.md'), result.content);
|
|
93
|
+
if (args.json) {
|
|
94
|
+
console.log(JSON.stringify({ candidateDir, budget, applied: result.applied.length, rejected: result.rejected }, null, 2));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
log.success(`Candidate at ${candidateDir} (budget ${budget}, applied ${result.applied.length})`);
|
|
98
|
+
for (const r of result.rejected)
|
|
99
|
+
log.warn(` rejected ${r.edit.op}: ${r.reason}`);
|
|
100
|
+
}
|
|
101
|
+
// Deterministic gate decision (exit 1 on reject so the loop can branch).
|
|
102
|
+
function gateAction(args) {
|
|
103
|
+
if (args.current === undefined || args.candidate === undefined) {
|
|
104
|
+
log.error('Usage: optimize gate --current <score> --candidate <score> [--best <score>]');
|
|
105
|
+
process.exitCode = 1;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const res = gate(args.current, args.candidate, args.best ?? args.current);
|
|
109
|
+
log.info(`gate: ${res.action}`);
|
|
110
|
+
if (res.action === 'reject')
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
}
|
|
113
|
+
// Human-approved promotion: atomic swap, prior version retained, score recorded.
|
|
114
|
+
function promoteAction(args) {
|
|
115
|
+
if (!args.skillName || !args.liveDir || !args.candidateDir) {
|
|
116
|
+
log.error('Usage: optimize promote --skill <name> --live <dir> --candidate <dir> [--score S]');
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const now = new Date().toISOString();
|
|
121
|
+
const state = loadState(args.skillName) ?? ensureState({ name: args.skillName, origin: 'optimized' }, now);
|
|
122
|
+
const priorVersion = state.version;
|
|
123
|
+
const newVersion = bumpPatch(priorVersion);
|
|
124
|
+
promote({
|
|
125
|
+
id: args.skillName,
|
|
126
|
+
liveDir: args.liveDir,
|
|
127
|
+
candidateDir: args.candidateDir,
|
|
128
|
+
priorVersion,
|
|
129
|
+
});
|
|
130
|
+
state.version = newVersion;
|
|
131
|
+
state.lifecycle = 'live';
|
|
132
|
+
state.origin = 'optimized';
|
|
133
|
+
state.updatedAt = now;
|
|
134
|
+
if (args.score !== undefined) {
|
|
135
|
+
state.scoreHistory.push({ version: newVersion, score: args.score, at: now });
|
|
136
|
+
}
|
|
137
|
+
saveState(state);
|
|
138
|
+
log.success(`Promoted ${args.skillName} ${priorVersion} → ${newVersion} (prior retained, reversible).`);
|
|
139
|
+
}
|
|
140
|
+
// Revert to a retained version (atomic).
|
|
141
|
+
function revertAction(args) {
|
|
142
|
+
if (!args.skillName || !args.version || !args.liveDir) {
|
|
143
|
+
log.error('Usage: optimize revert --skill <name> --version <v> --live <dir>');
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
revert(args.skillName, args.version, args.liveDir);
|
|
148
|
+
const now = new Date().toISOString();
|
|
149
|
+
setLifecycle(args.skillName, 'reverted', now);
|
|
150
|
+
log.success(`Reverted ${args.skillName} to ${args.version}.`);
|
|
151
|
+
}
|
|
152
|
+
export { noHeldOutRegression };
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { ensureDir } from '../util/fs.js';
|
|
6
|
+
import { SKILLS_GUIDANCE, REFLECT_NUDGE } from '../plugin/guidance.js';
|
|
7
|
+
import { recordToolUse, shouldReflect, markReflected } from '../plugin/sessions.js';
|
|
8
|
+
import { runReflectionDetached, isReflecting } from '../plugin/reflect.js';
|
|
9
|
+
import * as log from '../util/log.js';
|
|
10
|
+
const DEFAULT_THRESHOLD = 10;
|
|
11
|
+
/** Identify hooks we own (any of our bin names), keyed on our unique subcommands. */
|
|
12
|
+
function isOurHook(command) {
|
|
13
|
+
return typeof command === 'string' && /\bplugin (guidance|on-tool|on-stop)\b/.test(command);
|
|
14
|
+
}
|
|
15
|
+
// ---------- shared helpers ----------
|
|
16
|
+
function resolveCli() {
|
|
17
|
+
for (const bin of ['skillmaxxing', 'skill-maxing', 'skillmax']) {
|
|
18
|
+
try {
|
|
19
|
+
execSync(`command -v ${bin}`, { stdio: 'ignore' });
|
|
20
|
+
return bin;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* not on PATH */
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return 'npx -y skillmaxxing';
|
|
27
|
+
}
|
|
28
|
+
function readStdin() {
|
|
29
|
+
try {
|
|
30
|
+
const raw = fs.readFileSync(0, 'utf-8');
|
|
31
|
+
return raw ? JSON.parse(raw) : {};
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function claudeSettingsPath(project) {
|
|
38
|
+
return project
|
|
39
|
+
? path.join(process.cwd(), '.claude', 'settings.json')
|
|
40
|
+
: path.join(os.homedir(), '.claude', 'settings.json');
|
|
41
|
+
}
|
|
42
|
+
function readJson(file) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function writeJson(file, data) {
|
|
51
|
+
ensureDir(path.dirname(file));
|
|
52
|
+
const tmp = file + '.tmp';
|
|
53
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
54
|
+
fs.renameSync(tmp, file);
|
|
55
|
+
}
|
|
56
|
+
/** Drop any hook groups in `list` that belong to skill-maxing. */
|
|
57
|
+
function stripOurs(list) {
|
|
58
|
+
if (!Array.isArray(list))
|
|
59
|
+
return [];
|
|
60
|
+
return list.filter((g) => !(g.hooks ?? []).some((h) => isOurHook(h.command)));
|
|
61
|
+
}
|
|
62
|
+
// ---------- install / uninstall / status ----------
|
|
63
|
+
function install(args) {
|
|
64
|
+
const agent = args.agent ?? 'claude';
|
|
65
|
+
const mode = args.mode ?? 'auto';
|
|
66
|
+
const threshold = args.threshold ?? DEFAULT_THRESHOLD;
|
|
67
|
+
const cli = resolveCli();
|
|
68
|
+
if (agent === 'codex') {
|
|
69
|
+
installCodex();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const file = claudeSettingsPath(args.project ?? false);
|
|
73
|
+
const settings = readJson(file);
|
|
74
|
+
settings.hooks = settings.hooks ?? {};
|
|
75
|
+
// SessionStart: standing guidance (Layer A) — always wired.
|
|
76
|
+
settings.hooks.SessionStart = [
|
|
77
|
+
...stripOurs(settings.hooks.SessionStart),
|
|
78
|
+
{ hooks: [{ type: 'command', command: `${cli} plugin guidance` }] },
|
|
79
|
+
];
|
|
80
|
+
// PostToolUse + Stop: the background reflection loop (Layer B) — auto mode only.
|
|
81
|
+
if (mode === 'auto') {
|
|
82
|
+
settings.hooks.PostToolUse = [
|
|
83
|
+
...stripOurs(settings.hooks.PostToolUse),
|
|
84
|
+
{ matcher: '*', hooks: [{ type: 'command', command: `${cli} plugin on-tool` }] },
|
|
85
|
+
];
|
|
86
|
+
settings.hooks.Stop = [
|
|
87
|
+
...stripOurs(settings.hooks.Stop),
|
|
88
|
+
{
|
|
89
|
+
hooks: [
|
|
90
|
+
{
|
|
91
|
+
type: 'command',
|
|
92
|
+
command: `${cli} plugin on-stop --agent ${agent} --mode ${mode} --threshold ${threshold}`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// nudge mode: remove any auto hooks we previously installed
|
|
100
|
+
settings.hooks.PostToolUse = stripOurs(settings.hooks.PostToolUse);
|
|
101
|
+
settings.hooks.Stop = stripOurs(settings.hooks.Stop);
|
|
102
|
+
}
|
|
103
|
+
writeJson(file, settings);
|
|
104
|
+
log.success(`Skill Maxing installed for Claude Code (${mode} mode).`);
|
|
105
|
+
log.info(` hooks written to ${file}`);
|
|
106
|
+
log.info(` SessionStart: standing skill-creation guidance`);
|
|
107
|
+
if (mode === 'auto') {
|
|
108
|
+
log.info(` Stop: background reflection after ${threshold}+ tool calls (claude -p, trusted:false drafts)`);
|
|
109
|
+
}
|
|
110
|
+
log.info('No explicit trigger needed — restart your agent session to activate.');
|
|
111
|
+
log.info('Uninstall any time with: skill-maxing plugin uninstall');
|
|
112
|
+
}
|
|
113
|
+
const CODEX_MARK_START = '<!-- skill-maxing:start -->';
|
|
114
|
+
const CODEX_MARK_END = '<!-- skill-maxing:end -->';
|
|
115
|
+
function installCodex() {
|
|
116
|
+
// Codex lacks programmatic Stop/PostToolUse hooks, so we deliver the standing
|
|
117
|
+
// guidance via AGENTS.md (Layer A). The agent then self-evolves per instruction.
|
|
118
|
+
const file = path.join(process.cwd(), 'AGENTS.md');
|
|
119
|
+
let body = '';
|
|
120
|
+
try {
|
|
121
|
+
body = fs.readFileSync(file, 'utf-8');
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* new file */
|
|
125
|
+
}
|
|
126
|
+
const block = `${CODEX_MARK_START}\n## Skill Maxing\n\n${SKILLS_GUIDANCE}\n${CODEX_MARK_END}`;
|
|
127
|
+
const re = new RegExp(`${CODEX_MARK_START}[\\s\\S]*?${CODEX_MARK_END}`);
|
|
128
|
+
body = re.test(body) ? body.replace(re, block) : `${body.trimEnd()}\n\n${block}\n`;
|
|
129
|
+
fs.writeFileSync(file, body.replace(/^\n+/, ''));
|
|
130
|
+
log.success('Skill Maxing installed for Codex (guidance written to AGENTS.md).');
|
|
131
|
+
log.info('Codex has no Stop hook; self-evolution runs in-session via standing guidance.');
|
|
132
|
+
}
|
|
133
|
+
function uninstall(args) {
|
|
134
|
+
const agent = args.agent ?? 'claude';
|
|
135
|
+
if (agent === 'codex') {
|
|
136
|
+
const file = path.join(process.cwd(), 'AGENTS.md');
|
|
137
|
+
try {
|
|
138
|
+
const body = fs.readFileSync(file, 'utf-8');
|
|
139
|
+
const re = new RegExp(`\\n*${CODEX_MARK_START}[\\s\\S]*?${CODEX_MARK_END}\\n*`);
|
|
140
|
+
fs.writeFileSync(file, body.replace(re, '\n'));
|
|
141
|
+
log.success('Removed Skill Maxing guidance from AGENTS.md.');
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
log.info('Nothing to uninstall.');
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const file = claudeSettingsPath(args.project ?? false);
|
|
149
|
+
const settings = readJson(file);
|
|
150
|
+
if (settings.hooks) {
|
|
151
|
+
settings.hooks.SessionStart = stripOurs(settings.hooks.SessionStart);
|
|
152
|
+
settings.hooks.PostToolUse = stripOurs(settings.hooks.PostToolUse);
|
|
153
|
+
settings.hooks.Stop = stripOurs(settings.hooks.Stop);
|
|
154
|
+
for (const k of ['SessionStart', 'PostToolUse', 'Stop']) {
|
|
155
|
+
if (Array.isArray(settings.hooks[k]) && settings.hooks[k].length === 0)
|
|
156
|
+
delete settings.hooks[k];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
writeJson(file, settings);
|
|
160
|
+
log.success(`Removed Skill Maxing hooks from ${file}.`);
|
|
161
|
+
}
|
|
162
|
+
function status(args) {
|
|
163
|
+
const file = claudeSettingsPath(args.project ?? false);
|
|
164
|
+
const settings = readJson(file);
|
|
165
|
+
const owned = [];
|
|
166
|
+
for (const k of ['SessionStart', 'PostToolUse', 'Stop']) {
|
|
167
|
+
const groups = settings.hooks?.[k] ?? [];
|
|
168
|
+
if (groups.some((g) => (g.hooks ?? []).some((h) => isOurHook(h.command))))
|
|
169
|
+
owned.push(k);
|
|
170
|
+
}
|
|
171
|
+
if (owned.length > 0) {
|
|
172
|
+
log.success(`Skill Maxing is active for Claude Code: ${owned.join(', ')} hooks (${file}).`);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
log.info('Skill Maxing is not installed for Claude Code. Run: skill-maxing plugin install');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// ---------- hook entrypoints ----------
|
|
179
|
+
function guidance() {
|
|
180
|
+
// SessionStart additionalContext — injects the standing nudge into the agent.
|
|
181
|
+
console.log(JSON.stringify({
|
|
182
|
+
hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: SKILLS_GUIDANCE },
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
function onTool() {
|
|
186
|
+
if (isReflecting())
|
|
187
|
+
return; // don't count the reflector's own work
|
|
188
|
+
const input = readStdin();
|
|
189
|
+
const id = typeof input.session_id === 'string' ? input.session_id : 'default';
|
|
190
|
+
recordToolUse(id);
|
|
191
|
+
}
|
|
192
|
+
function onStop(args) {
|
|
193
|
+
if (isReflecting())
|
|
194
|
+
return; // recursion guard: the reflector must never re-trigger
|
|
195
|
+
const input = readStdin();
|
|
196
|
+
const id = typeof input.session_id === 'string' ? input.session_id : 'default';
|
|
197
|
+
const transcriptPath = typeof input.transcript_path === 'string' ? input.transcript_path : '';
|
|
198
|
+
const mode = args.mode ?? 'auto';
|
|
199
|
+
const threshold = args.threshold ?? DEFAULT_THRESHOLD;
|
|
200
|
+
if (!shouldReflect(id, threshold))
|
|
201
|
+
return; // not enough substantive work yet
|
|
202
|
+
if (mode === 'nudge') {
|
|
203
|
+
// Surface a one-line reminder to the user; the agent acts on standing guidance.
|
|
204
|
+
console.log(JSON.stringify({ systemMessage: `Skill Maxing: ${REFLECT_NUDGE}` }));
|
|
205
|
+
markReflected(id, new Date().toISOString());
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// auto mode: fire the background reflector and reset the counter.
|
|
209
|
+
markReflected(id, new Date().toISOString());
|
|
210
|
+
if (transcriptPath) {
|
|
211
|
+
runReflectionDetached({ agent: args.agent ?? 'claude', transcriptPath, cwd: process.cwd() });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export async function plugin(args) {
|
|
215
|
+
switch (args.action) {
|
|
216
|
+
case 'install':
|
|
217
|
+
return install(args);
|
|
218
|
+
case 'uninstall':
|
|
219
|
+
return uninstall(args);
|
|
220
|
+
case 'status':
|
|
221
|
+
return status(args);
|
|
222
|
+
case 'guidance':
|
|
223
|
+
return guidance();
|
|
224
|
+
case 'on-tool':
|
|
225
|
+
return onTool();
|
|
226
|
+
case 'on-stop':
|
|
227
|
+
return onStop(args);
|
|
228
|
+
default:
|
|
229
|
+
log.error(`Unknown plugin action: ${args.action}`);
|
|
230
|
+
process.exitCode = 1;
|
|
231
|
+
}
|
|
232
|
+
}
|