mirror of
https://github.com/lukaszraczylo/claude-adam.git
synced 2026-06-09 23:19:12 +00:00
012c40b9ab
Storage/window/exclusion split (#7): ISO-week journal rotation with safety fuse replaces size-based rotation (fixes silent under-counting when clusters straddle boundaries). Per-signal sliding windows via adam-window.mjs guard against stale signal accumulation. Legacy YYYY-MM-DD-<ts>.jsonl files remain readable. Error fingerprint normalization (#3): adam-observe.mjs extracts canonical error codes (ENOENT, ECONNREFUSED, etc.) and normalizes paths/timestamps/hex before hashing. 'Connection refused' and 'ECONNREFUSED' now cluster identically. Correction corpus expansion (#1): strong tokens (stop, wrong, undo, try again, different approach, etc.) fire on any occurrence. Weak tokens (no, actually, wait) require negation/contrast co-occurrence within 8 tokens. Kills the 'actually, I think...' false positive. Analyst observability (#6): mandatory clustering trace block; adam-explain.mjs parses to summary/full/json. Cluster decisions now surface rejection reasons (threshold, contradiction, window). Persisted to ~/.claude/adam/last-trace.txt. Dead_end nudge proposal type (#2): single-session auto-apply gate (>=3 dead_end events). Action appends to active-nudges.json, surfaced via adam-nudge.mjs at next SessionStart. Lower blast than skill_edit. Per-(skill, fingerprint) cooldown (#4): adam-cooldown.mjs replaces coarse per-skill check. proposal_fingerprint = djb2(skill_slug + cluster_id + normalized_diff_body). Legacy applied/rejected records gate via 'legacy' fingerprint fallback through resolveSkill helper (handles target_skill, skill, or target: <path>). task_completed scoring integration (#8): adam-score.mjs computes per-session urgency dampener (3 task_completed -> 0.5) and reinforcement candidates (skills cited in >=3 clean completions). New 'reinforcement' proposal type appends to reinforcements.jsonl on apply (no code/memory mutation). A/B effectiveness measurement (#5): every auto-applied edit appends to ab-tracking.jsonl. adam-ab-measure.mjs computes 7d pre/post signal-count delta per entry (improved / neutral / regressed / no_baseline / pending). Analyst surfaces regressions at top of /reflect output. Upgrade UX overhaul (#9): adam-upgrade.mjs implements --list/--diff/--accept /--accept-all. SessionStart nudge prints pending-merge warning when .adam-new files exist (latency ~20ms via fixed shortlist). install.sh emits unmissable final-message hint after creating any .adam-new file. Simplify pass: adam-utils.mjs deduplicates readJsonlSafe / listJsonlFiles / parseFrontmatter across 8 scripts. Net -46 LOC. Test coverage: 30 -> 87 tests. Every new feature has feature-validating assertions (false-case coverage included). T77 statically verifies install.sh references every adam-*.mjs source script (would have caught the missing adam-utils inclusion that review #2 surfaced).
92 lines
3.3 KiB
JavaScript
Executable File
92 lines
3.3 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// adam-apply-reinforcement.mjs — apply-path for `reinforcement` proposals.
|
|
//
|
|
// Reads a proposal markdown file, validates the apply gate
|
|
// (confidence >= 4 AND blast_radius == "low" AND type == "reinforcement"),
|
|
// and on success appends one JSON line to ~/.claude/adam/reinforcements.jsonl
|
|
// of shape `{ts, skill_slug, count, source_session}`.
|
|
//
|
|
// CLI: adam-apply-reinforcement.mjs <proposal-path> [--home <path>]
|
|
// Output: JSON one-liner on stdout: {"status":"applied"|"gated", "reason":"..."}
|
|
// Exit: 0 on apply, 0 on gated, 1 on I/O or parse error.
|
|
//
|
|
// SKILL.md invokes this in the auto-apply path when the proposal type is
|
|
// `reinforcement`. No code/memory/skill modifications.
|
|
|
|
import { readFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { homedir } from "node:os";
|
|
import { parseFrontmatter } from "./adam-utils.mjs";
|
|
|
|
// Re-exported for backward compat — callers historically imported it from here.
|
|
export { parseFrontmatter };
|
|
|
|
function parseArgs(argv) {
|
|
const args = { home: null, path: null, help: false };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === "--home" && i + 1 < argv.length) args.home = argv[++i];
|
|
else if (a === "--help" || a === "-h") args.help = true;
|
|
else if (!args.path && !a.startsWith("--")) args.path = a;
|
|
}
|
|
return args;
|
|
}
|
|
|
|
export function checkGate(fm) {
|
|
if ((fm.type || "") !== "reinforcement") {
|
|
return { ok: false, reason: `type != reinforcement (got: ${fm.type || "<none>"})` };
|
|
}
|
|
const conf = Number(fm.confidence);
|
|
if (Number.isNaN(conf) || conf < 4) {
|
|
return { ok: false, reason: `confidence < 4 (got: ${fm.confidence ?? "<none>"})` };
|
|
}
|
|
if ((fm.blast_radius || "").toLowerCase() !== "low") {
|
|
return { ok: false, reason: `blast_radius != low (got: ${fm.blast_radius || "<none>"})` };
|
|
}
|
|
if (!fm.skill_slug) {
|
|
return { ok: false, reason: "skill_slug missing in frontmatter" };
|
|
}
|
|
return { ok: true };
|
|
}
|
|
|
|
export function buildEntry(fm, now = Date.now()) {
|
|
return {
|
|
ts: now,
|
|
skill_slug: String(fm.skill_slug),
|
|
count: Number(fm.count) || 0,
|
|
source_session: fm.source_session || "",
|
|
};
|
|
}
|
|
|
|
function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help || !args.path) {
|
|
process.stdout.write("usage: adam-apply-reinforcement.mjs <proposal-path> [--home <path>]\n");
|
|
process.exit(args.help ? 0 : 1);
|
|
}
|
|
const claudeHome = args.home || join(homedir(), ".claude");
|
|
const outPath = join(claudeHome, "adam", "reinforcements.jsonl");
|
|
try {
|
|
const content = readFileSync(args.path, "utf8");
|
|
const fm = parseFrontmatter(content);
|
|
const gate = checkGate(fm);
|
|
if (!gate.ok) {
|
|
process.stdout.write(JSON.stringify({ status: "gated", reason: gate.reason }) + "\n");
|
|
process.exit(0);
|
|
}
|
|
const entry = buildEntry(fm);
|
|
const dir = dirname(outPath);
|
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
appendFileSync(outPath, JSON.stringify(entry) + "\n");
|
|
process.stdout.write(JSON.stringify({ status: "applied", path: outPath }) + "\n");
|
|
process.exit(0);
|
|
} catch (e) {
|
|
process.stderr.write(`adam-apply-reinforcement error: ${e.message}\n`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
main();
|
|
}
|