mirror of
https://github.com/lukaszraczylo/claude-adam.git
synced 2026-06-09 23:19:12 +00:00
4b36d6c09e
Full codebase review (multi-agent, adversarially verified) surfaced several documented-but-dead mechanisms and doc/code drift. Fixes: - adam-observe: struggle signals now emit `active_skills`, so silent_drift's primary cluster key AND §5b skill-attribution sub-clustering (+1 rubric bonus) actually fire — both were silently dead (no struggle signal carried the field). - adam-cooldown: new `--compute` CLI deterministically derives proposal_fingerprint. The exported computeProposalFingerprint() was never called and the analyst was told to hand-compute a djb2 hash it cannot reproduce. Spec now mandates a *stable* cluster id so fingerprints reproduce across /reflect runs. Removed one dead normalization line. - spec: reinforcement proposals excluded from A/B tracking — agents/adam.md contradicted itself (:376 included, :476 excluded); SKILL.md aligned. - adam-nudge: PENDING_CHECK_PATHS now mirrors the full install set (adam-utils / adam-batch / adam-rollback were missing). - adam-explain: synthesized clustering summary carries `regressions: 0` (structural consistency with parsed summaries). - docs: test-count drift (87/94 -> 126) and "350-line hook" (-> ~600) fixed; adam-score header documents severity_sum/severity_by_type; adam-batch §4 reference corrected. Tests: +12 assertions (114 -> 126), all green. New regression tests cover the active_skills fix and --compute, plus boundary gaps the review flagged: retry_loop/weak_agent thresholds, A/B exact +/-25% deltas, cooldown 30d blacklist edge.
185 lines
6.3 KiB
JavaScript
Executable File
185 lines
6.3 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// adam-batch.mjs — pre-clusters windowed journal entries into coherent failure
|
|
// batches before analyst dispatch. Implements MOSS §3.1: "anchored to an
|
|
// automatically curated batch of production-failure evidence."
|
|
//
|
|
// Each batch groups entries by (signal_type, cluster_key) where cluster_key
|
|
// follows the same clustering rules as agents/adam.md ## Signal types / ## Process step 4:
|
|
// correction → tokenized phrase (cross-cwd)
|
|
// retry_loop → tool
|
|
// weak_agent → subagent_type
|
|
// tool_error_loop→ fp
|
|
// dead_end → session
|
|
// edit_churn → file basename
|
|
// build_loop → session
|
|
// subagent_dispatch_pattern → subagent_type
|
|
// silent_drift → active_skills[0]
|
|
// error_after_recovery → (recovered_from, original_fp)
|
|
// correction_free_streak → active_skills[0]
|
|
// clean_recovery → (recovered_from, active_skills[0])
|
|
// task_completed → sorted tool_kinds tuple
|
|
//
|
|
// CLI:
|
|
// adam-batch.mjs [--input <jsonl-path>] [--min-entries N] [--min-sessions N]
|
|
//
|
|
// Output: JSON object with `batches` array and `unbatched` count.
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import { readJsonlSafe } from "./adam-utils.mjs";
|
|
|
|
const DEFAULT_MIN_ENTRIES = 1;
|
|
const DEFAULT_MIN_SESSIONS = 1;
|
|
|
|
const CORRECTION_STOPWORDS = new Set([
|
|
"the", "a", "an", "and", "or", "but", "of", "to", "for", "in", "on",
|
|
"with", "use", "when", "where", "what", "why", "how", "this", "that",
|
|
"these", "those", "is", "are", "was", "were", "be", "been", "being",
|
|
"do", "does", "did", "doing", "has", "have", "had", "your", "you",
|
|
"i", "it", "as", "at", "by", "from", "not", "no",
|
|
]);
|
|
|
|
function tokenizePhrase(phrase) {
|
|
if (!phrase || typeof phrase !== "string") return "";
|
|
return phrase.toLowerCase()
|
|
.split(/\s+/)
|
|
.map(t => t.replace(/^[^\w']+|[^\w']+$/g, ""))
|
|
.filter(t => t && !CORRECTION_STOPWORDS.has(t))
|
|
.sort()
|
|
.join("|");
|
|
}
|
|
|
|
function clusterKey(entry) {
|
|
if (!entry || typeof entry !== "object") return null;
|
|
const t = entry.type;
|
|
switch (t) {
|
|
case "correction":
|
|
return tokenizePhrase(entry.phrase) || "unknown";
|
|
case "retry_loop":
|
|
return entry.tool || "unknown";
|
|
case "weak_agent":
|
|
case "subagent_dispatch_pattern":
|
|
return entry.subagent_type || "unknown";
|
|
case "tool_error_loop":
|
|
return entry.fp || "unknown";
|
|
case "dead_end":
|
|
case "build_loop":
|
|
return entry.session || "unknown";
|
|
case "edit_churn":
|
|
return entry.file ? entry.file.split("/").pop() : "unknown";
|
|
case "silent_drift":
|
|
case "correction_free_streak":
|
|
return Array.isArray(entry.active_skills) ? (entry.active_skills[0] || "") : "";
|
|
case "error_after_recovery":
|
|
return `${entry.recovered_from || "?"}:${entry.original_fp || "?"}`;
|
|
case "clean_recovery":
|
|
return `${entry.recovered_from || "?"}:${Array.isArray(entry.active_skills) ? (entry.active_skills[0] || "") : ""}`;
|
|
case "task_completed":
|
|
return Array.isArray(entry.tool_kinds) ? entry.tool_kinds.slice().sort().join(",") : "unknown";
|
|
default:
|
|
return entry.session || "unknown";
|
|
}
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = { input: null, minEntries: DEFAULT_MIN_ENTRIES, minSessions: DEFAULT_MIN_SESSIONS, help: false };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
const a = argv[i];
|
|
if (a === "--input" && i + 1 < argv.length) args.input = argv[++i];
|
|
else if (a === "--min-entries" && i + 1 < argv.length) {
|
|
const n = Number(argv[++i]);
|
|
if (!Number.isNaN(n) && n > 0) args.minEntries = n;
|
|
}
|
|
else if (a === "--min-sessions" && i + 1 < argv.length) {
|
|
const n = Number(argv[++i]);
|
|
if (!Number.isNaN(n) && n > 0) args.minSessions = n;
|
|
}
|
|
else if (a === "--help" || a === "-h") args.help = true;
|
|
}
|
|
return args;
|
|
}
|
|
|
|
export function buildBatches(entries, opts = {}) {
|
|
const minEntries = opts.minEntries || DEFAULT_MIN_ENTRIES;
|
|
const minSessions = opts.minSessions || DEFAULT_MIN_SESSIONS;
|
|
const map = new Map();
|
|
|
|
for (const e of entries || []) {
|
|
if (!e || typeof e !== "object" || !e.type) continue;
|
|
const key = `${e.type}::${clusterKey(e)}`;
|
|
if (!map.has(key)) {
|
|
map.set(key, {
|
|
batch_id: null,
|
|
signal_type: e.type,
|
|
cluster_key: clusterKey(e),
|
|
entries: [],
|
|
sessions: new Set(),
|
|
cwds: new Set(),
|
|
});
|
|
}
|
|
const batch = map.get(key);
|
|
batch.entries.push(e);
|
|
if (e.session) batch.sessions.add(e.session);
|
|
if (e.cwd) batch.cwds.add(e.cwd);
|
|
}
|
|
|
|
const batches = [];
|
|
let unbatched = 0;
|
|
let id = 1;
|
|
for (const [, batch] of map) {
|
|
if (batch.entries.length < minEntries || batch.sessions.size < minSessions) {
|
|
unbatched += batch.entries.length;
|
|
continue;
|
|
}
|
|
batch.batch_id = `b${id++}`;
|
|
batches.push({
|
|
batch_id: batch.batch_id,
|
|
signal_type: batch.signal_type,
|
|
cluster_key: batch.cluster_key,
|
|
entry_count: batch.entries.length,
|
|
session_count: batch.sessions.size,
|
|
cwd_count: batch.cwds.size,
|
|
has_context_window: batch.entries.some(e => Array.isArray(e.context_window) && e.context_window.length > 0),
|
|
entries: batch.entries,
|
|
});
|
|
}
|
|
|
|
batches.sort((a, b) => b.entry_count - a.entry_count);
|
|
return { batches, unbatched, total: (entries || []).length };
|
|
}
|
|
|
|
function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help) {
|
|
process.stdout.write("usage: adam-batch.mjs [--input <jsonl-path>] [--min-entries N] [--min-sessions N]\n");
|
|
process.exit(0);
|
|
}
|
|
try {
|
|
let entries;
|
|
if (args.input) {
|
|
entries = readJsonlSafe(args.input);
|
|
} else if (!process.stdin.isTTY) {
|
|
const buf = readFileSync(0, "utf8");
|
|
entries = [];
|
|
for (const line of buf.split("\n")) {
|
|
if (!line) continue;
|
|
try { entries.push(JSON.parse(line)); } catch { /* skip */ }
|
|
}
|
|
} else {
|
|
process.stderr.write("adam-batch: no input (use --input or pipe)\n");
|
|
process.exit(1);
|
|
}
|
|
const result = buildBatches(entries, { minEntries: args.minEntries, minSessions: args.minSessions });
|
|
process.stdout.write(JSON.stringify(result) + "\n");
|
|
process.exit(0);
|
|
} catch (e) {
|
|
process.stderr.write(`adam-batch error: ${e.message}\n`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
main();
|
|
}
|
|
|
|
export { clusterKey, tokenizePhrase };
|