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).
98 lines
3.3 KiB
JavaScript
Executable File
98 lines
3.3 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// adam-nudge-eligibility.mjs — checks whether the current session has accrued
|
|
// enough `dead_end` entries to warrant emitting a cross-session nudge.
|
|
//
|
|
// CLI: adam-nudge-eligibility.mjs [--home <path>] [--session <id>]
|
|
// --home defaults to $HOME/.claude
|
|
// --session if absent, reads state.json `session_id` field
|
|
//
|
|
// Output (stdout): JSON one-liner
|
|
// eligible: {"eligible": true, "session_id": "...", "dead_end_count": N, "last_ts": "..."}
|
|
// not: {"eligible": false, "session_id": "...", "dead_end_count": N, "last_ts": "..."|null}
|
|
// Exit codes:
|
|
// 0 — read succeeded (eligible OR not)
|
|
// 1 — read failure / unable to resolve session
|
|
//
|
|
// Threshold: ≥3 dead_end entries within a single session_id across the active
|
|
// journal + all rotated journal/*.jsonl files. Threshold matches the
|
|
// "dead-end checkpoint" feedback rule.
|
|
|
|
import { readFileSync, existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { homedir } from "node:os";
|
|
import { readJsonlSafe, listJsonlFiles } from "./adam-utils.mjs";
|
|
|
|
export const DEAD_END_THRESHOLD = 3;
|
|
|
|
function parseArgs(argv) {
|
|
const args = { home: null, session: 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 === "--session" && i + 1 < argv.length) args.session = argv[++i];
|
|
else if (a === "--help" || a === "-h") args.help = true;
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function resolveSession(home, fallback) {
|
|
if (fallback) return fallback;
|
|
const statePath = join(home, "adam", "state.json");
|
|
if (!existsSync(statePath)) return null;
|
|
try {
|
|
const st = JSON.parse(readFileSync(statePath, "utf8"));
|
|
return st && typeof st.session_id === "string" ? st.session_id : null;
|
|
} catch { return null; }
|
|
}
|
|
|
|
export function checkEligibility(home, sessionId) {
|
|
const adamRoot = join(home, "adam");
|
|
const sources = [
|
|
join(adamRoot, "journal.jsonl"),
|
|
...listJsonlFiles(join(adamRoot, "journal")),
|
|
];
|
|
let count = 0;
|
|
let lastTs = null;
|
|
for (const p of sources) {
|
|
for (const e of readJsonlSafe(p)) {
|
|
if (!e || e.type !== "dead_end") continue;
|
|
if (e.session !== sessionId && e.session_id !== sessionId) continue;
|
|
count++;
|
|
const ts = typeof e.ts === "string" ? e.ts : null;
|
|
if (ts && (!lastTs || ts > lastTs)) lastTs = ts;
|
|
}
|
|
}
|
|
return {
|
|
eligible: count >= DEAD_END_THRESHOLD,
|
|
session_id: sessionId,
|
|
dead_end_count: count,
|
|
last_ts: lastTs,
|
|
};
|
|
}
|
|
|
|
function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help) {
|
|
process.stdout.write("usage: adam-nudge-eligibility.mjs [--home <path>] [--session <id>]\n");
|
|
process.exit(0);
|
|
}
|
|
const home = args.home || join(homedir(), ".claude");
|
|
const sessionId = resolveSession(home, args.session);
|
|
if (!sessionId) {
|
|
process.stderr.write("adam-nudge-eligibility: no session_id (pass --session or seed state.json)\n");
|
|
process.exit(1);
|
|
}
|
|
try {
|
|
const result = checkEligibility(home, sessionId);
|
|
process.stdout.write(JSON.stringify(result) + "\n");
|
|
process.exit(0);
|
|
} catch (e) {
|
|
process.stderr.write(`adam-nudge-eligibility error: ${e.message}\n`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
main();
|
|
}
|