Files
claude-adam/adam/scripts/adam-archive.mjs
T
lukaszraczylo 012c40b9ab chore(v0.3.3): analyst observability, A/B measurement, journal hygiene
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).
2026-05-13 01:02:33 +01:00

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);
}