mirror of
https://github.com/lukaszraczylo/claude-adam.git
synced 2026-06-08 23:09:16 +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).
86 lines
2.7 KiB
JavaScript
Executable File
86 lines
2.7 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// Usage: adam-archive.mjs <proposal-path>
|
|
// Reads `source_entries` from proposal frontmatter, moves matching journal
|
|
// entries from journal.jsonl to journal/actioned-<id>.jsonl. Used by the
|
|
// adam-self-improvement skill after each apply/reject so subsequent /reflect
|
|
// runs do not re-cluster already-actioned signals.
|
|
|
|
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { homedir } from "node:os";
|
|
import { parseFrontmatter } from "./adam-utils.mjs";
|
|
|
|
const ROOT = join(homedir(), ".claude", "adam");
|
|
const JOURNAL = join(ROOT, "journal.jsonl");
|
|
const JOURNAL_DIR = join(ROOT, "journal");
|
|
|
|
function main() {
|
|
const proposalPath = process.argv[2];
|
|
if (!proposalPath) {
|
|
console.error("usage: adam-archive.mjs <proposal-path>");
|
|
process.exit(2);
|
|
}
|
|
|
|
let proposal;
|
|
try {
|
|
proposal = readFileSync(proposalPath, "utf8");
|
|
} catch (e) {
|
|
console.error(`cannot read ${proposalPath}: ${e.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const fm = parseFrontmatter(proposal);
|
|
const id = fm.id || "unknown";
|
|
const sourceEntries = Array.isArray(fm.source_entries) ? fm.source_entries : [];
|
|
|
|
if (sourceEntries.length === 0) {
|
|
console.log(`${id}: no source_entries in frontmatter — nothing to archive`);
|
|
return;
|
|
}
|
|
|
|
if (!existsSync(JOURNAL)) {
|
|
console.log(`${id}: journal does not exist at ${JOURNAL}`);
|
|
return;
|
|
}
|
|
|
|
const lines = readFileSync(JOURNAL, "utf8").split("\n").filter(Boolean);
|
|
// tsCounts: how many entries with this ts the proposal claims as its own.
|
|
// Same-millisecond duplicates: only consume up to the recorded count.
|
|
const tsCounts = new Map();
|
|
for (const ts of sourceEntries) tsCounts.set(ts, (tsCounts.get(ts) || 0) + 1);
|
|
const matched = [];
|
|
const remaining = [];
|
|
|
|
for (const line of lines) {
|
|
try {
|
|
const e = JSON.parse(line);
|
|
const remainingCount = e.ts ? (tsCounts.get(e.ts) || 0) : 0;
|
|
if (remainingCount > 0) {
|
|
matched.push(line);
|
|
tsCounts.set(e.ts, remainingCount - 1);
|
|
} else {
|
|
remaining.push(line);
|
|
}
|
|
} catch {
|
|
remaining.push(line);
|
|
}
|
|
}
|
|
|
|
if (matched.length === 0) {
|
|
console.log(`${id}: no matching entries in journal (already archived?)`);
|
|
return;
|
|
}
|
|
|
|
mkdirSync(JOURNAL_DIR, { recursive: true });
|
|
const archivePath = join(JOURNAL_DIR, `actioned-${id}.jsonl`);
|
|
appendFileSync(archivePath, matched.join("\n") + "\n");
|
|
writeFileSync(JOURNAL, remaining.length ? remaining.join("\n") + "\n" : "");
|
|
|
|
console.log(`${id}: archived ${matched.length}/${lines.length} entries → ${archivePath}`);
|
|
}
|
|
|
|
try { main(); } catch (e) {
|
|
console.error(`error: ${e.message}`);
|
|
process.exit(1);
|
|
}
|