mirror of
https://github.com/lukaszraczylo/claude-adam.git
synced 2026-10-30 07:17:12 +00:00
Compare commits
8
Commits
4d1276a73f
...
7ccb07a412
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ccb07a412 | ||
|
|
f418afa275 | ||
|
|
e3fe691b45 | ||
|
|
b2fff8f271 | ||
|
|
f1a186af7e | ||
|
|
67e8eede85 | ||
|
|
0ec17517d9 | ||
|
|
2b6e8da885 |
@@ -13,7 +13,7 @@ Watches the friction in your coding sessions, clusters the signals via an LLM an
|
|||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/lukaszraczylo/claude-adam/releases)
|
[](https://github.com/lukaszraczylo/claude-adam/releases)
|
||||||
[](./adam/tests/run-tests.sh)
|
[](./adam/tests/run-tests.sh)
|
||||||
[](https://nodejs.org)
|
[](https://nodejs.org)
|
||||||
[]()
|
[]()
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ The installer copies files into `~/.claude/`, offers to merge ADAM's hook entrie
|
|||||||
Then:
|
Then:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
bash ~/.claude/adam/tests/run-tests.sh # expect: 140 passed, 0 failed
|
bash ~/.claude/adam/tests/run-tests.sh # expect: 150 passed, 0 failed
|
||||||
# … start a fresh Claude Code session …
|
# … start a fresh Claude Code session …
|
||||||
/reflect # walks the proposal queue
|
/reflect # walks the proposal queue
|
||||||
/reflect --explain # also shows the analyst's clustering trace
|
/reflect --explain # also shows the analyst's clustering trace
|
||||||
@@ -235,11 +235,12 @@ Or pass `--explain` to `/reflect` to render the full trace inline.
|
|||||||
```
|
```
|
||||||
~/.claude/
|
~/.claude/
|
||||||
├── hooks/
|
├── hooks/
|
||||||
│ ├── adam-observe.mjs # signal collector (UserPromptSubmit / PreToolUse / PostToolUse)
|
│ ├── adam-observe.mjs # signal collector (UserPromptSubmit / PreToolUse / PostToolUse / PostToolUseFailure)
|
||||||
│ └── adam-nudge.mjs # SessionStart reminder + pending-upgrade warning
|
│ └── adam-nudge.mjs # SessionStart reminder + pending-upgrade warning
|
||||||
├── agents/adam.md # analyst subagent (system prompt + rubric)
|
├── agents/adam.md # analyst subagent (system prompt + rubric)
|
||||||
├── skills/adam-self-improvement/
|
├── skills/adam-self-improvement/
|
||||||
│ └── SKILL.md # /reflect protocol
|
│ ├── SKILL.md # /reflect protocol
|
||||||
|
│ └── apply-by-type.md # per-type apply steps, read before any apply
|
||||||
├── commands/reflect.md # /reflect slash command
|
├── commands/reflect.md # /reflect slash command
|
||||||
└── adam/
|
└── adam/
|
||||||
├── journal.jsonl # active observations
|
├── journal.jsonl # active observations
|
||||||
@@ -262,14 +263,20 @@ Or pass `--explain` to `/reflect` to render the full trace inline.
|
|||||||
│ ├── adam-cooldown.mjs # per-(skill, fingerprint) cooldown gate
|
│ ├── adam-cooldown.mjs # per-(skill, fingerprint) cooldown gate
|
||||||
│ ├── adam-nudge-eligibility.mjs # dead_end session-count check
|
│ ├── adam-nudge-eligibility.mjs # dead_end session-count check
|
||||||
│ ├── adam-explain.mjs # clustering trace parser/renderer
|
│ ├── adam-explain.mjs # clustering trace parser/renderer
|
||||||
|
│ ├── adam-batch.mjs # evidence batching (optional output cap)
|
||||||
|
│ ├── adam-rollback.mjs # auto-rollback of regressed proposals
|
||||||
|
│ ├── adam-skill-utility.mjs # per-skill good:bad utility report
|
||||||
│ ├── adam-apply-reinforcement.mjs # reinforcement proposal apply
|
│ ├── adam-apply-reinforcement.mjs # reinforcement proposal apply
|
||||||
|
│ ├── adam-apply-nudge.mjs # nudge proposal apply (message + cue)
|
||||||
│ ├── adam-upgrade.mjs # .adam-new file UX (list/diff/accept)
|
│ ├── adam-upgrade.mjs # .adam-new file UX (list/diff/accept)
|
||||||
│ └── adam-archive.mjs # post-apply journal cleanup
|
│ └── adam-archive.mjs # post-apply journal cleanup
|
||||||
└── tests/run-tests.sh # 140 isolated tests; never touches live state
|
└── tests/run-tests.sh # 150 isolated tests of the tree it sits in; never touches live state
|
||||||
```
|
```
|
||||||
|
|
||||||
## What's new
|
## What's new
|
||||||
|
|
||||||
|
- **v0.7.1** — ADAM now sees real tool failures. Claude Code sends failed tool calls to the `PostToolUseFailure` hook event, which ADAM never registered, so every `tool_error_loop` came from `ERROR_RE` matching successful output. `adam-observe.mjs` now handles `PostToolUseFailure` through the same detectors as `PostToolUse` (interrupts excluded), and `settings.json.example` registers it; re-run `install.sh` or add the entry by hand. Successful Read, Edit and Write responses are no longer fingerprinted: source files containing words such as `error` produced 151 false `tool_error_loop` entries across 9 sessions. Found by ADAM's own `/reflect`. 150 tests (up from 145).
|
||||||
|
- **v0.7.0** — query-conditioned nudges and a lighter `/reflect` skill. A nudge can carry `cue.cwd`: `adam-nudge.mjs` replays it at SessionStart only in that codebase, and `adam-observe.mjs` now also surfaces it once mid-session on a matching `UserPromptSubmit` (opt out with `ADAM_NO_MIDSESSION_NUDGE=1`). New `adam-apply-nudge.mjs` builds the nudge message and cue from proposal frontmatter, so the analyst no longer hand-writes JSON. New `adam-skill-utility.mjs` ranks skills by good:bad outcome co-occurrence (Wilson lower bound, lift against baseline) and feeds a utility-lift rubric penalty; the analyst re-diagnoses unclear or top-severity clusters once per run. `adam-batch.mjs` gains an optional `--max-batches` / `--top-fraction` cap (off by default) and reports `capped`. The per-type apply steps move from `SKILL.md` to `apply-by-type.md`, so `/reflect` loads less context until it applies a proposal. The test harness now tests the tree it sits in, not `~/.claude`, so a repo checkout no longer tests an older install. 145 tests (up from 140).
|
||||||
- **v0.6.4** — rollback now keeps its promise. `adam-rollback.mjs`'s docstring always claimed it "removes the ab-tracking entry (so it doesn't re-trigger)," but `executeRollback()` never did — so a rolled-back proposal kept flagging as `regressed` on every subsequent `/reflect`, triggering endless `not_found` rollback attempts. It now deletes the matching `ab-tracking.jsonl` row by `proposal_id` (preserving unrelated rows). Surfaced by running ADAM's own loop twice. 140 tests (up from 138).
|
- **v0.6.4** — rollback now keeps its promise. `adam-rollback.mjs`'s docstring always claimed it "removes the ab-tracking entry (so it doesn't re-trigger)," but `executeRollback()` never did — so a rolled-back proposal kept flagging as `regressed` on every subsequent `/reflect`, triggering endless `not_found` rollback attempts. It now deletes the matching `ab-tracking.jsonl` row by `proposal_id` (preserving unrelated rows). Surfaced by running ADAM's own loop twice. 140 tests (up from 138).
|
||||||
- **v0.6.3** — release-update notifier. `install.sh` now writes a `~/.claude/adam/.version` marker; `adam-nudge.mjs` (SessionStart) compares it against the latest GitHub release at most once/day (cached, 1.5 s network cap, best-effort — never blocks) and prints a **notify-only** one-line update prompt. Deliberately not auto-applied: re-running the installer resets ADAM's own `/reflect`-applied skill edits, so you choose when to update. Opt out with `ADAM_NO_UPDATE_CHECK=1`. See "Staying up to date". 138 tests (up from 134).
|
- **v0.6.3** — release-update notifier. `install.sh` now writes a `~/.claude/adam/.version` marker; `adam-nudge.mjs` (SessionStart) compares it against the latest GitHub release at most once/day (cached, 1.5 s network cap, best-effort — never blocks) and prints a **notify-only** one-line update prompt. Deliberately not auto-applied: re-running the installer resets ADAM's own `/reflect`-applied skill edits, so you choose when to update. Opt out with `ADAM_NO_UPDATE_CHECK=1`. See "Staying up to date". 138 tests (up from 134).
|
||||||
- **v0.6.2** — two fixes surfaced by running ADAM's loop on a large real journal. **(1) A/B volume normalization** (`adam-ab-measure.mjs`): regressions are now measured on the signal's *share* of total activity (rate = count / window-total), not raw count — so a generally busier journal after an apply no longer masquerades as a regression. Falls back to raw delta when the signal is the only activity in the window (preserves prior behavior + tests); output adds `raw_delta_pct`, `pre_total`, `post_total`, `normalized` for transparency. **(2) Memory frontmatter schema** (`agents/adam.md`, `SKILL.md`): the drafting protocol now emits the live auto-memory shape — `name` = slug + a `metadata: {node_type, type, originSessionId}` block — instead of flat `type:`/`originSessionId:`, so auto-applied memories load and categorize correctly. 134 tests (up from 132).
|
- **v0.6.2** — two fixes surfaced by running ADAM's loop on a large real journal. **(1) A/B volume normalization** (`adam-ab-measure.mjs`): regressions are now measured on the signal's *share* of total activity (rate = count / window-total), not raw count — so a generally busier journal after an apply no longer masquerades as a regression. Falls back to raw delta when the signal is the only activity in the window (preserves prior behavior + tests); output adds `raw_delta_pct`, `pre_total`, `post_total`, `normalized` for transparency. **(2) Memory frontmatter schema** (`agents/adam.md`, `SKILL.md`): the drafting protocol now emits the live auto-memory shape — `name` = slug + a `metadata: {node_type, type, originSessionId}` block — instead of flat `type:`/`originSessionId:`, so auto-applied memories load and categorize correctly. 134 tests (up from 132).
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// adam-apply-nudge.mjs — apply-path for `nudge` proposals.
|
||||||
|
//
|
||||||
|
// Reads a nudge proposal markdown file, validates the apply gate
|
||||||
|
// (type == "nudge" AND source_session present), and on success appends one
|
||||||
|
// entry to ~/.claude/adam/active-nudges.json. The message and the `cue` are
|
||||||
|
// built DETERMINISTICALLY in code from the proposal frontmatter — the analyst
|
||||||
|
// does not hand-author the JSON, so the cue (ReContext-style query-conditioning)
|
||||||
|
// is always populated when a cwd is known, not left to chance.
|
||||||
|
//
|
||||||
|
// Frontmatter consumed: type, source_session, cwd (optional — becomes cue.cwd),
|
||||||
|
// dead_end_count (optional — shapes the message), max_displays (optional, ≥1).
|
||||||
|
//
|
||||||
|
// CLI: adam-apply-nudge.mjs <proposal-path> [--home <path>]
|
||||||
|
// Output: JSON one-liner on stdout: {"status":"applied"|"gated", "reason"|"path"}
|
||||||
|
// Exit: 0 on apply, 0 on gated, 1 on I/O or parse error.
|
||||||
|
//
|
||||||
|
// SKILL.md invokes this in the nudge auto-apply path. No code/memory/skill edits.
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { parseFrontmatter } from "./adam-utils.mjs";
|
||||||
|
|
||||||
|
export { parseFrontmatter };
|
||||||
|
|
||||||
|
const SEVEN_DAYS_MS = 7 * 86400000;
|
||||||
|
const DEFAULT_MAX_DISPLAYS = 3;
|
||||||
|
|
||||||
|
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 || "") !== "nudge") {
|
||||||
|
return { ok: false, reason: `type != nudge (got: ${fm.type || "<none>"})` };
|
||||||
|
}
|
||||||
|
if (!fm.source_session) {
|
||||||
|
return { ok: false, reason: "source_session missing in frontmatter" };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildEntry(fm, now = Date.now()) {
|
||||||
|
const cwd = fm.cwd ? String(fm.cwd) : "";
|
||||||
|
const base = cwd ? cwd.split("/").filter(Boolean).pop() : "";
|
||||||
|
const count = Number(fm.dead_end_count) > 0 ? Number(fm.dead_end_count) : 3;
|
||||||
|
const maxDisplays = Number(fm.max_displays) > 0 ? Number(fm.max_displays) : DEFAULT_MAX_DISPLAYS;
|
||||||
|
const message = `adam: a previous session hit ${count} dead-ends${base ? ` in ${base}` : ""} — consider a checkpoint before continuing.`;
|
||||||
|
const entry = {
|
||||||
|
kind: "dead_end_reminder",
|
||||||
|
message,
|
||||||
|
created_at: now,
|
||||||
|
expires_at_ts: now + SEVEN_DAYS_MS,
|
||||||
|
max_displays: maxDisplays,
|
||||||
|
displays_used: 0,
|
||||||
|
source_session: String(fm.source_session || ""),
|
||||||
|
};
|
||||||
|
// The cue is what makes replay query-conditioned: bind the reminder to the
|
||||||
|
// project it came from so it only surfaces there (SessionStart + mid-session).
|
||||||
|
if (cwd) entry.cue = { cwd };
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appendNudge(nudgesPath, entry) {
|
||||||
|
let arr = [];
|
||||||
|
if (existsSync(nudgesPath)) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(readFileSync(nudgesPath, "utf8"));
|
||||||
|
if (Array.isArray(parsed)) arr = parsed;
|
||||||
|
} catch { arr = []; }
|
||||||
|
}
|
||||||
|
arr.push(entry);
|
||||||
|
const dir = dirname(nudgesPath);
|
||||||
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||||
|
writeFileSync(nudgesPath, JSON.stringify(arr, null, 2));
|
||||||
|
return arr.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
const args = parseArgs(process.argv.slice(2));
|
||||||
|
if (args.help || !args.path) {
|
||||||
|
process.stdout.write("usage: adam-apply-nudge.mjs <proposal-path> [--home <path>]\n");
|
||||||
|
process.exit(args.help ? 0 : 1);
|
||||||
|
}
|
||||||
|
const claudeHome = args.home || join(homedir(), ".claude");
|
||||||
|
const nudgesPath = join(claudeHome, "adam", "active-nudges.json");
|
||||||
|
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 count = appendNudge(nudgesPath, entry);
|
||||||
|
process.stdout.write(JSON.stringify({ status: "applied", path: nudgesPath, total: count, cue: entry.cue || null }) + "\n");
|
||||||
|
process.exit(0);
|
||||||
|
} catch (e) {
|
||||||
|
process.stderr.write(`adam-apply-nudge error: ${e.message}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
@@ -83,7 +83,7 @@ function clusterKey(entry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
const args = { input: null, minEntries: DEFAULT_MIN_ENTRIES, minSessions: DEFAULT_MIN_SESSIONS, help: false };
|
const args = { input: null, minEntries: DEFAULT_MIN_ENTRIES, minSessions: DEFAULT_MIN_SESSIONS, maxBatches: null, topFraction: null, help: false };
|
||||||
for (let i = 0; i < argv.length; i++) {
|
for (let i = 0; i < argv.length; i++) {
|
||||||
const a = argv[i];
|
const a = argv[i];
|
||||||
if (a === "--input" && i + 1 < argv.length) args.input = argv[++i];
|
if (a === "--input" && i + 1 < argv.length) args.input = argv[++i];
|
||||||
@@ -95,6 +95,14 @@ function parseArgs(argv) {
|
|||||||
const n = Number(argv[++i]);
|
const n = Number(argv[++i]);
|
||||||
if (!Number.isNaN(n) && n > 0) args.minSessions = n;
|
if (!Number.isNaN(n) && n > 0) args.minSessions = n;
|
||||||
}
|
}
|
||||||
|
else if (a === "--max-batches" && i + 1 < argv.length) {
|
||||||
|
const n = Number(argv[++i]);
|
||||||
|
if (!Number.isNaN(n) && n > 0) args.maxBatches = n;
|
||||||
|
}
|
||||||
|
else if (a === "--top-fraction" && i + 1 < argv.length) {
|
||||||
|
const n = Number(argv[++i]);
|
||||||
|
if (!Number.isNaN(n) && n > 0 && n < 1) args.topFraction = n;
|
||||||
|
}
|
||||||
else if (a === "--help" || a === "-h") args.help = true;
|
else if (a === "--help" || a === "-h") args.help = true;
|
||||||
}
|
}
|
||||||
return args;
|
return args;
|
||||||
@@ -146,13 +154,28 @@ export function buildBatches(entries, opts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
batches.sort((a, b) => b.entry_count - a.entry_count);
|
batches.sort((a, b) => b.entry_count - a.entry_count);
|
||||||
return { batches, unbatched, total: (entries || []).length };
|
|
||||||
|
// Optional output cap (default: no cap — the full ranked list is returned).
|
||||||
|
// ReContext P2: relevance is concentrated, so surfacing only the top head keeps
|
||||||
|
// reviewer attention on the highest-value clusters. `topFraction` keeps the top
|
||||||
|
// ceil(f * N) after ranking (so the survivor count tracks corpus volume);
|
||||||
|
// `maxBatches` is a hard ceiling. The more restrictive of the two applies.
|
||||||
|
// Never silent: `capped` reports how many ranked batches were dropped by the cap.
|
||||||
|
let keepN = batches.length;
|
||||||
|
const frac = Number(opts.topFraction);
|
||||||
|
if (frac > 0 && frac < 1) keepN = Math.min(keepN, Math.max(1, Math.ceil(frac * batches.length)));
|
||||||
|
const maxB = Number(opts.maxBatches);
|
||||||
|
if (Number.isFinite(maxB) && maxB > 0) keepN = Math.min(keepN, maxB);
|
||||||
|
const capped = batches.length - keepN;
|
||||||
|
const kept = capped > 0 ? batches.slice(0, keepN) : batches;
|
||||||
|
|
||||||
|
return { batches: kept, unbatched, capped, total: (entries || []).length };
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
const args = parseArgs(process.argv.slice(2));
|
const args = parseArgs(process.argv.slice(2));
|
||||||
if (args.help) {
|
if (args.help) {
|
||||||
process.stdout.write("usage: adam-batch.mjs [--input <jsonl-path>] [--min-entries N] [--min-sessions N]\n");
|
process.stdout.write("usage: adam-batch.mjs [--input <jsonl-path>] [--min-entries N] [--min-sessions N] [--max-batches N] [--top-fraction F]\n");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -170,7 +193,7 @@ function main() {
|
|||||||
process.stderr.write("adam-batch: no input (use --input or pipe)\n");
|
process.stderr.write("adam-batch: no input (use --input or pipe)\n");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
const result = buildBatches(entries, { minEntries: args.minEntries, minSessions: args.minSessions });
|
const result = buildBatches(entries, { minEntries: args.minEntries, minSessions: args.minSessions, maxBatches: args.maxBatches, topFraction: args.topFraction });
|
||||||
process.stdout.write(JSON.stringify(result) + "\n");
|
process.stdout.write(JSON.stringify(result) + "\n");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+170
-16
@@ -5,20 +5,24 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
REAL_HOME="$HOME"
|
REAL_HOME="$HOME"
|
||||||
HOOK="$REAL_HOME/.claude/hooks/adam-observe.mjs"
|
# Test the tree this script lives in: <base>/adam/tests/run-tests.sh. The repo
|
||||||
NUDGE="$REAL_HOME/.claude/hooks/adam-nudge.mjs"
|
# and an install (~/.claude) share the layout <base>/hooks, <base>/adam/scripts.
|
||||||
ARCHIVE="$REAL_HOME/.claude/adam/scripts/adam-archive.mjs"
|
BASE="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
WINDOW="$REAL_HOME/.claude/adam/scripts/adam-window.mjs"
|
HOOK="$BASE/hooks/adam-observe.mjs"
|
||||||
EXPLAIN="$REAL_HOME/.claude/adam/scripts/adam-explain.mjs"
|
NUDGE="$BASE/hooks/adam-nudge.mjs"
|
||||||
ELIGIBILITY="$REAL_HOME/.claude/adam/scripts/adam-nudge-eligibility.mjs"
|
ARCHIVE="$BASE/adam/scripts/adam-archive.mjs"
|
||||||
COOLDOWN="$REAL_HOME/.claude/adam/scripts/adam-cooldown.mjs"
|
WINDOW="$BASE/adam/scripts/adam-window.mjs"
|
||||||
SCORE="$REAL_HOME/.claude/adam/scripts/adam-score.mjs"
|
EXPLAIN="$BASE/adam/scripts/adam-explain.mjs"
|
||||||
ABMEASURE="$REAL_HOME/.claude/adam/scripts/adam-ab-measure.mjs"
|
ELIGIBILITY="$BASE/adam/scripts/adam-nudge-eligibility.mjs"
|
||||||
APPLYREIN="$REAL_HOME/.claude/adam/scripts/adam-apply-reinforcement.mjs"
|
COOLDOWN="$BASE/adam/scripts/adam-cooldown.mjs"
|
||||||
UPGRADE="$REAL_HOME/.claude/adam/scripts/adam-upgrade.mjs"
|
SCORE="$BASE/adam/scripts/adam-score.mjs"
|
||||||
BATCH="$REAL_HOME/.claude/adam/scripts/adam-batch.mjs"
|
ABMEASURE="$BASE/adam/scripts/adam-ab-measure.mjs"
|
||||||
ROLLBACK="$REAL_HOME/.claude/adam/scripts/adam-rollback.mjs"
|
APPLYREIN="$BASE/adam/scripts/adam-apply-reinforcement.mjs"
|
||||||
SKILLUTIL="$REAL_HOME/.claude/adam/scripts/adam-skill-utility.mjs"
|
UPGRADE="$BASE/adam/scripts/adam-upgrade.mjs"
|
||||||
|
BATCH="$BASE/adam/scripts/adam-batch.mjs"
|
||||||
|
ROLLBACK="$BASE/adam/scripts/adam-rollback.mjs"
|
||||||
|
SKILLUTIL="$BASE/adam/scripts/adam-skill-utility.mjs"
|
||||||
|
APPLYNUDGE="$BASE/adam/scripts/adam-apply-nudge.mjs"
|
||||||
|
|
||||||
TMP_HOME="$(mktemp -d -t adam-test.XXXXXX)"
|
TMP_HOME="$(mktemp -d -t adam-test.XXXXXX)"
|
||||||
trap 'rm -rf "$TMP_HOME"' EXIT INT TERM
|
trap 'rm -rf "$TMP_HOME"' EXIT INT TERM
|
||||||
@@ -39,6 +43,7 @@ UPGRADE_RUN() { HOME="$TMP_HOME" node "$UPGRADE" "$@"; }
|
|||||||
BATCH_RUN() { HOME="$TMP_HOME" node "$BATCH" "$@"; }
|
BATCH_RUN() { HOME="$TMP_HOME" node "$BATCH" "$@"; }
|
||||||
ROLLBACK_RUN(){ HOME="$TMP_HOME" node "$ROLLBACK" "$@"; }
|
ROLLBACK_RUN(){ HOME="$TMP_HOME" node "$ROLLBACK" "$@"; }
|
||||||
SKILLUTIL_RUN(){ HOME="$TMP_HOME" node "$SKILLUTIL" "$@"; }
|
SKILLUTIL_RUN(){ HOME="$TMP_HOME" node "$SKILLUTIL" "$@"; }
|
||||||
|
APPLYNUDGE_RUN(){ HOME="$TMP_HOME" node "$APPLYNUDGE" "$@" --home "$TMP_HOME/.claude"; }
|
||||||
|
|
||||||
PASS=0
|
PASS=0
|
||||||
FAIL=0
|
FAIL=0
|
||||||
@@ -182,6 +187,25 @@ for i in 1 2 3; do
|
|||||||
done
|
done
|
||||||
assert_grep "$ROOT/journal.jsonl" '"type":"tool_error_loop"' "3x same error logs tool_error_loop"
|
assert_grep "$ROOT/journal.jsonl" '"type":"tool_error_loop"' "3x same error logs tool_error_loop"
|
||||||
|
|
||||||
|
# --- Test 9b: PostToolUseFailure feeds the same detectors ---
|
||||||
|
echo "Test 9b: 3x PostToolUseFailure with the same error logs tool_error_loop"
|
||||||
|
reset_state
|
||||||
|
for i in 1 2 3; do
|
||||||
|
echo '{"hook_event_name":"PostToolUseFailure","tool_name":"Bash","tool_input":{"command":"npm test"},"error":"Exit code 1\nError: Cannot find module express","is_interrupt":false,"session_id":"s9b","cwd":"/tmp/x"}' \
|
||||||
|
| HOOK_RUN >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
assert_grep "$ROOT/journal.jsonl" '"type":"tool_error_loop"' "3x PostToolUseFailure logs tool_error_loop"
|
||||||
|
|
||||||
|
# --- Test 9c: interrupted tool calls are not errors ---
|
||||||
|
echo "Test 9c: 8x PostToolUseFailure with is_interrupt:true counts as tool use but logs no tool_error_loop"
|
||||||
|
reset_state
|
||||||
|
for i in 1 2 3 4 5 6 7 8; do
|
||||||
|
echo '{"hook_event_name":"PostToolUseFailure","tool_name":"Bash","tool_input":{"command":"sleep 99"},"error":"Error: aborted","is_interrupt":true,"session_id":"s9c","cwd":"/tmp/x"}' \
|
||||||
|
| HOOK_RUN >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
assert_grep "$ROOT/journal.jsonl" '"type":"dead_end"' "interrupted calls still count as tool events"
|
||||||
|
assert_no_grep "$ROOT/journal.jsonl" '"type":"tool_error_loop"' "interrupts do not log tool_error_loop"
|
||||||
|
|
||||||
# --- Test 10: dead_end on long autonomous run ---
|
# --- Test 10: dead_end on long autonomous run ---
|
||||||
echo "Test 10: dead_end after 8 tools without UserPromptSubmit"
|
echo "Test 10: dead_end after 8 tools without UserPromptSubmit"
|
||||||
reset_state
|
reset_state
|
||||||
@@ -626,6 +650,27 @@ else
|
|||||||
echo " FAIL: hex/epoch fingerprint mismatch (a=$fp_a b=$fp_b)"; FAIL=$((FAIL+1))
|
echo " FAIL: hex/epoch fingerprint mismatch (a=$fp_a b=$fp_b)"; FAIL=$((FAIL+1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Test 36b: fingerprint — file-tool success objects are not errors ---
|
||||||
|
echo "Test 36b: Read/Edit/Write success objects with error words return null"
|
||||||
|
fp_r=$(node -e "import('$HOOK').then(m => console.log(m.errorFingerprint({type:'text',file:{filePath:'/x/cache.go',content:'return fmt.Errorf(\"failed: not found\")'}})))")
|
||||||
|
fp_e=$(node -e "import('$HOOK').then(m => console.log(m.errorFingerprint({filePath:'/x/a.ts',oldString:'throw new Error(1)',newString:'x',originalFile:'undefined error'})))")
|
||||||
|
fp_w=$(node -e "import('$HOOK').then(m => console.log(m.errorFingerprint({type:'create',filePath:'/x/b.ts',content:'const errorRateTier = \"failed\"'})))")
|
||||||
|
if [ "$fp_r" = "null" ] && [ "$fp_e" = "null" ] && [ "$fp_w" = "null" ]; then
|
||||||
|
echo " PASS: file-tool success shapes not fingerprinted"; PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo " FAIL: success shape fingerprinted (r=$fp_r e=$fp_e w=$fp_w)"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Test 36c: fingerprint — real error shapes still fingerprinted ---
|
||||||
|
echo "Test 36c: is_error:true with filePath and plain error strings still fingerprint"
|
||||||
|
fp_x=$(node -e "import('$HOOK').then(m => console.log(m.errorFingerprint({is_error:true,filePath:'/x/a.ts',content:'String to replace not found in file.'})))")
|
||||||
|
fp_s=$(node -e "import('$HOOK').then(m => console.log(m.errorFingerprint('Error: result (70,310 characters) exceeds maximum allowed tokens')))")
|
||||||
|
if [ "$fp_x" != "null" ] && [ "$fp_s" != "null" ]; then
|
||||||
|
echo " PASS: error shapes keep fingerprints ($fp_x, $fp_s)"; PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo " FAIL: error shape lost fingerprint (x=$fp_x s=$fp_s)"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Test 37: correction corpus — strong tokens fire ---
|
# --- Test 37: correction corpus — strong tokens fire ---
|
||||||
echo "Test 37: strong-correction tokens each emit correction signal"
|
echo "Test 37: strong-correction tokens each emit correction signal"
|
||||||
strong_ok=1
|
strong_ok=1
|
||||||
@@ -1381,9 +1426,10 @@ rm -f "$ROOT/applied/2026-05-11-legacy-target.md"
|
|||||||
|
|
||||||
# --- Test 77: install.sh covers every adam-*.mjs script in scripts/ ---
|
# --- Test 77: install.sh covers every adam-*.mjs script in scripts/ ---
|
||||||
echo "Test 77: install.sh references every adam-*.mjs file"
|
echo "Test 77: install.sh references every adam-*.mjs file"
|
||||||
SCRIPTS_DIR="$REAL_HOME/.claude/adam/scripts"
|
SCRIPTS_DIR="$BASE/adam/scripts"
|
||||||
INSTALL_SH=""
|
INSTALL_SH=""
|
||||||
for cand in "$REAL_HOME/Documents/projects/private/claude-adam/install.sh" \
|
for cand in "$BASE/install.sh" \
|
||||||
|
"$REAL_HOME/Documents/projects/private/claude-adam/install.sh" \
|
||||||
"$REAL_HOME/Documents/projects/private/adam/install.sh"; do
|
"$REAL_HOME/Documents/projects/private/adam/install.sh"; do
|
||||||
[ -f "$cand" ] && INSTALL_SH="$cand" && break
|
[ -f "$cand" ] && INSTALL_SH="$cand" && break
|
||||||
done
|
done
|
||||||
@@ -2176,6 +2222,114 @@ else
|
|||||||
fi
|
fi
|
||||||
rm -f "$SU_INPUT"
|
rm -f "$SU_INPUT"
|
||||||
|
|
||||||
|
# --- Test 120: nudge cue.cwd gates surfacing on the current session's cwd ---
|
||||||
|
echo "Test 120: nudge cue.cwd — matching cwd prints, mismatch deferred (kept, not consumed)"
|
||||||
|
reset_state
|
||||||
|
cue_now=$(node -e 'console.log(Date.now())')
|
||||||
|
cue_future=$(node -e 'console.log(Date.now() + 7*86400000)')
|
||||||
|
cat > "$ROOT/active-nudges.json" <<EOF
|
||||||
|
[{"kind":"dead_end_reminder","message":"adam: cue-match checkpoint","created_at":$cue_now,"expires_at_ts":$cue_future,"max_displays":3,"displays_used":0,"source_session":"sOLD","cue":{"cwd":"/proj/a"}}]
|
||||||
|
EOF
|
||||||
|
out_match=$(echo '{"hook_event_name":"SessionStart","session_id":"sNEW","cwd":"/proj/a"}' | NUDGE_RUN 2>&1 || true)
|
||||||
|
cat > "$ROOT/active-nudges.json" <<EOF
|
||||||
|
[{"kind":"dead_end_reminder","message":"adam: cue-miss checkpoint","created_at":$cue_now,"expires_at_ts":$cue_future,"max_displays":3,"displays_used":0,"source_session":"sOLD","cue":{"cwd":"/proj/a"}}]
|
||||||
|
EOF
|
||||||
|
out_miss=$(echo '{"hook_event_name":"SessionStart","session_id":"sNEW","cwd":"/proj/b"}' | NUDGE_RUN 2>&1 || true)
|
||||||
|
cue_remaining=$(node -e 'const j=require("'"$ROOT"'/active-nudges.json");console.log(j.length)')
|
||||||
|
cue_used=$(node -e 'const j=require("'"$ROOT"'/active-nudges.json");console.log((j[0]||{}).displays_used)')
|
||||||
|
if echo "$out_match" | grep -q "cue-match checkpoint" \
|
||||||
|
&& ! echo "$out_miss" | grep -q "cue-miss checkpoint" \
|
||||||
|
&& [ "$cue_remaining" = "1" ] && [ "$cue_used" = "0" ]; then
|
||||||
|
echo " PASS: cue.cwd match prints; mismatch deferred without consuming a display"; PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo " FAIL: match=$out_match miss=$out_miss remaining=$cue_remaining used=$cue_used"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
rm -f "$ROOT/active-nudges.json"
|
||||||
|
|
||||||
|
# --- Test 121: adam-batch output cap (--max-batches / --top-fraction), default off ---
|
||||||
|
echo "Test 121: adam-batch caps ranked output and reports capped; no flag = no cap"
|
||||||
|
cap_input=$(cat <<'EOF'
|
||||||
|
{"ts":"2026-05-20T10:00:00Z","type":"correction","session":"s1","cwd":"/a","phrase":"alpha alpha"}
|
||||||
|
{"ts":"2026-05-20T11:00:00Z","type":"correction","session":"s2","cwd":"/a","phrase":"alpha alpha"}
|
||||||
|
{"ts":"2026-05-20T12:00:00Z","type":"retry_loop","session":"s1","cwd":"/a","tool":"Bash"}
|
||||||
|
{"ts":"2026-05-21T10:00:00Z","type":"tool_error_loop","session":"s1","cwd":"/a","fp":"ENOENT:x","count":3}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
GET() { node -e 'let b="";process.stdin.on("data",d=>b+=d).on("end",()=>{const j=JSON.parse(b);console.log(eval("j."+process.argv[1]))})' "$1"; }
|
||||||
|
mb_out=$(echo "$cap_input" | BATCH_RUN --max-batches 2 2>/dev/null)
|
||||||
|
mb_bc=$(echo "$mb_out" | GET "batches.length"); mb_cap=$(echo "$mb_out" | GET "capped"); mb_top=$(echo "$mb_out" | GET "batches[0].signal_type")
|
||||||
|
none_out=$(echo "$cap_input" | BATCH_RUN 2>/dev/null)
|
||||||
|
none_bc=$(echo "$none_out" | GET "batches.length"); none_cap=$(echo "$none_out" | GET "capped")
|
||||||
|
tf_out=$(echo "$cap_input" | BATCH_RUN --top-fraction 0.5 2>/dev/null)
|
||||||
|
tf_bc=$(echo "$tf_out" | GET "batches.length"); tf_cap=$(echo "$tf_out" | GET "capped")
|
||||||
|
if [ "$mb_bc" = "2" ] && [ "$mb_cap" = "1" ] && [ "$mb_top" = "correction" ] \
|
||||||
|
&& [ "$none_bc" = "3" ] && [ "$none_cap" = "0" ] \
|
||||||
|
&& [ "$tf_bc" = "2" ] && [ "$tf_cap" = "1" ]; then
|
||||||
|
echo " PASS: --max-batches 2 → 2 batches (capped=1, head=correction); no flag → 3/capped=0; --top-fraction 0.5 → 2/capped=1"; PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo " FAIL: mb($mb_bc,$mb_cap,$mb_top) none($none_bc,$none_cap) tf($tf_bc,$tf_cap)"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Test 122: adam-apply-nudge builds a cue'd entry deterministically; gates non-nudge ---
|
||||||
|
echo "Test 122: adam-apply-nudge populates cue.cwd + generated message from frontmatter"
|
||||||
|
reset_state
|
||||||
|
rm -f "$ROOT/active-nudges.json"
|
||||||
|
cat > "$ROOT/proposals/nudge-prop-001.md" <<'EOF'
|
||||||
|
---
|
||||||
|
type: nudge
|
||||||
|
source_session: sNUDGE
|
||||||
|
cwd: /Users/me/proj-alpha
|
||||||
|
dead_end_count: 4
|
||||||
|
confidence: 4
|
||||||
|
blast_radius: low
|
||||||
|
---
|
||||||
|
# Nudge
|
||||||
|
checkpoint reminder
|
||||||
|
EOF
|
||||||
|
an_out=$(APPLYNUDGE_RUN "$ROOT/proposals/nudge-prop-001.md" 2>&1 || true)
|
||||||
|
an_status=$(echo "$an_out" | node -e 'let b="";process.stdin.on("data",d=>b+=d).on("end",()=>{try{console.log(JSON.parse(b).status)}catch{console.log("parse-error")}})')
|
||||||
|
an_cue=$(node -e 'const j=require("'"$ROOT"'/active-nudges.json");console.log((j[0]&&j[0].cue&&j[0].cue.cwd)||"")')
|
||||||
|
an_src=$(node -e 'const j=require("'"$ROOT"'/active-nudges.json");console.log((j[0]||{}).source_session||"")')
|
||||||
|
an_msg=$(node -e 'const j=require("'"$ROOT"'/active-nudges.json");console.log((j[0]||{}).message||"")')
|
||||||
|
cat > "$ROOT/proposals/nudge-prop-002.md" <<'EOF'
|
||||||
|
---
|
||||||
|
type: memory
|
||||||
|
source_session: sX
|
||||||
|
---
|
||||||
|
EOF
|
||||||
|
gate_status=$(APPLYNUDGE_RUN "$ROOT/proposals/nudge-prop-002.md" 2>&1 | node -e 'let b="";process.stdin.on("data",d=>b+=d).on("end",()=>{try{console.log(JSON.parse(b).status)}catch{console.log("parse-error")}})')
|
||||||
|
if [ "$an_status" = "applied" ] && [ "$an_cue" = "/Users/me/proj-alpha" ] && [ "$an_src" = "sNUDGE" ] \
|
||||||
|
&& echo "$an_msg" | grep -q "4 dead-ends in proj-alpha" && [ "$gate_status" = "gated" ]; then
|
||||||
|
echo " PASS: applied with cue.cwd + generated message; non-nudge proposal gated"; PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo " FAIL: status=$an_status cue=$an_cue src=$an_src msg=$an_msg gate=$gate_status"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
rm -f "$ROOT/proposals/nudge-prop-00"*.md "$ROOT/active-nudges.json"
|
||||||
|
|
||||||
|
# --- Test 123: mid-session nudge reactivation on UserPromptSubmit (cue.cwd gated) ---
|
||||||
|
echo "Test 123: adam-observe surfaces a cue-matched nudge mid-session — once/session, cwd-gated, opt-out"
|
||||||
|
ms_future=$(node -e 'console.log(Date.now() + 7*86400000)')
|
||||||
|
ms_nudge() { cat > "$ROOT/active-nudges.json" <<EOF
|
||||||
|
[{"kind":"dead_end_reminder","message":"adam: midsession checkpoint alpha","created_at":111,"expires_at_ts":$ms_future,"max_displays":3,"displays_used":0,"source_session":"sOLD","cue":{"cwd":"/proj/x"}}]
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
reset_state; ms_nudge
|
||||||
|
ms1=$(echo '{"hook_event_name":"UserPromptSubmit","session_id":"sMS","cwd":"/proj/x","prompt":"hello"}' | HOOK_RUN 2>&1 || true)
|
||||||
|
ms2=$(echo '{"hook_event_name":"UserPromptSubmit","session_id":"sMS","cwd":"/proj/x","prompt":"again"}' | HOOK_RUN 2>&1 || true)
|
||||||
|
reset_state; ms_nudge
|
||||||
|
ms_miss=$(echo '{"hook_event_name":"UserPromptSubmit","session_id":"sMS2","cwd":"/proj/y","prompt":"hello"}' | HOOK_RUN 2>&1 || true)
|
||||||
|
reset_state; ms_nudge
|
||||||
|
ms_off=$(echo '{"hook_event_name":"UserPromptSubmit","session_id":"sMS3","cwd":"/proj/x","prompt":"hello"}' | HOME="$TMP_HOME" ADAM_NO_MIDSESSION_NUDGE=1 node "$HOOK" 2>&1 || true)
|
||||||
|
if echo "$ms1" | grep -q "midsession checkpoint alpha" \
|
||||||
|
&& ! echo "$ms2" | grep -q "midsession checkpoint alpha" \
|
||||||
|
&& ! echo "$ms_miss" | grep -q "midsession checkpoint alpha" \
|
||||||
|
&& ! echo "$ms_off" | grep -q "midsession checkpoint alpha"; then
|
||||||
|
echo " PASS: surfaced once on cwd match; deduped 2nd prompt; silent on cwd mismatch + opt-out"; PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
echo " FAIL: ms1=$ms1 || ms2=$ms2 || miss=$ms_miss || off=$ms_off"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
rm -f "$ROOT/active-nudges.json"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Results: $PASS passed, $FAIL failed"
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
[ "$FAIL" = "0" ]
|
[ "$FAIL" = "0" ]
|
||||||
|
|||||||
+18
-14
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: adam
|
name: adam
|
||||||
description: Self-improvement analyst. Reads adam journal + transcript context, clusters observations, scores against a deterministic rubric, and emits proposal files for new skills, memory entries, agent edits, hook changes, CLAUDE.md edits, and soft deletions. Invoked only via the adam-self-improvement skill.
|
description: "Self-improvement analyst. Reads the adam journal and transcript context, clusters observations, scores them against a deterministic rubric, and writes proposal files for new skills, memory entries, agent edits, hook changes, CLAUDE.md edits and soft deletions. Invoked only via the adam-self-improvement skill (/reflect). Not for: editing skills, agents or memory directly (the main thread applies approved proposals), or reviewing code (use code-reviewer)."
|
||||||
tools: Read, Write, Edit, Grep, Glob, Bash
|
tools: Read, Write, Edit, Grep, Glob, Bash
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -17,6 +17,8 @@ The skill dispatches you in one of two stages (MOSS-inspired multi-stage pipelin
|
|||||||
|
|
||||||
If no `stage` is specified in the dispatch prompt, run **both stages sequentially** within a single pass (backward-compatible with pre-MOSS flow).
|
If no `stage` is specified in the dispatch prompt, run **both stages sequentially** within a single pass (backward-compatible with pre-MOSS flow).
|
||||||
|
|
||||||
|
**Adaptive re-diagnose** (ReContext P5 — depth is task-dependent, not uniformly larger): a `stage=diagnose` dispatch may carry `redigest: <cluster_id[,…]>` and `prior_diagnoses_path`. When it does, re-diagnose ONLY those clusters: re-read each cluster's `context_window` (and up to ~30 extra transcript messages) *conditioned on the prior diagnosis*, and overwrite the trigger/action/mismatch/outcome for those `cluster_id`s in `/tmp/adam-diagnoses.json`. This is how an ambiguous cluster earns a second, deeper pass while clear clusters stay at one. Never re-diagnose a cluster more than once per `/reflect`.
|
||||||
|
|
||||||
### Diagnose-stage output format
|
### Diagnose-stage output format
|
||||||
|
|
||||||
When `stage=diagnose`, write `/tmp/adam-diagnoses.json` containing:
|
When `stage=diagnose`, write `/tmp/adam-diagnoses.json` containing:
|
||||||
@@ -161,12 +163,14 @@ The hook emits these `type` values into the journal:
|
|||||||
- `correction_free_streak`: cluster by `active_skills[0]`. Treat ≥3 streaks across ≥2 sessions naming the same skill as cross-session evidence.
|
- `correction_free_streak`: cluster by `active_skills[0]`. Treat ≥3 streaks across ≥2 sessions naming the same skill as cross-session evidence.
|
||||||
- `clean_recovery`: cluster by (`recovered_from`, `active_skills[0]`). A win cluster qualifies for `skill_edit` only when the named skill exists in `skills_root`.
|
- `clean_recovery`: cluster by (`recovered_from`, `active_skills[0]`). A win cluster qualifies for `skill_edit` only when the named skill exists in `skills_root`.
|
||||||
- `task_completed`: cluster by sorted `tool_kinds` tuple (the multi-tool recipe). Single entry qualifies for `skill_new` proposal (drafting protocol applies). Cross-session evidence requires ≥2 entries from distinct sessions with same tuple — without it, proposal queues, never auto-applies. Run the existing skill-overlap rule before drafting: if the recipe matches an existing skill's name/description tokens, route to `skill_edit` instead.
|
- `task_completed`: cluster by sorted `tool_kinds` tuple (the multi-tool recipe). Single entry qualifies for `skill_new` proposal (drafting protocol applies). Cross-session evidence requires ≥2 entries from distinct sessions with same tuple — without it, proposal queues, never auto-applies. Run the existing skill-overlap rule before drafting: if the recipe matches an existing skill's name/description tokens, route to `skill_edit` instead.
|
||||||
|
4b. **Cross-type associative pass** (ReContext P5 — surface evidence related to already-selected spans): after primary clustering, for each cluster gather active observations from *other* signal types that share the cluster's `session` or file basename (e.g. a `dead_end` and the `edit_churn` on the same file/session that led to it). Attach them to the cluster as `associated_entries` — do NOT merge cluster keys or re-count them for the rubric — so the diagnosis can reconstruct causation across signal types instead of from one isolated type. Cap at ~8 associated entries per cluster, nearest-in-time first.
|
||||||
5. **Multi-axis correlation**: for each session that produced ≥2 distinct struggle types (`tool_error_loop`, `dead_end`, `weak_agent`, `retry_loop`, `edit_churn`, `build_loop`, `silent_drift`, `file_reread`, `error_after_recovery`), tag clusters from that session as `multi_axis: true`. This grants +1 confidence at scoring.
|
5. **Multi-axis correlation**: for each session that produced ≥2 distinct struggle types (`tool_error_loop`, `dead_end`, `weak_agent`, `retry_loop`, `edit_churn`, `build_loop`, `silent_drift`, `file_reread`, `error_after_recovery`), tag clusters from that session as `multi_axis: true`. This grants +1 confidence at scoring.
|
||||||
|
|
||||||
5b. **Skill-attribution sub-clustering**: after primary clustering (step 4), for every struggle cluster (`tool_error_loop`, `dead_end`, `weak_agent`, `retry_loop`, `edit_churn`, `build_loop`, `silent_drift`, `file_reread`, `error_after_recovery`) that contains entries with non-empty `active_skills[0]`:
|
5b. **Skill-attribution sub-clustering**: after primary clustering (step 4), for every struggle cluster (`tool_error_loop`, `dead_end`, `weak_agent`, `retry_loop`, `edit_churn`, `build_loop`, `silent_drift`, `file_reread`, `error_after_recovery`) that contains entries with non-empty `active_skills[0]`:
|
||||||
- Split into per-skill sub-clusters keyed on `active_skills[0]`. Entries with empty `active_skills` stay in the original cluster.
|
- Split into per-skill sub-clusters keyed on `active_skills[0]`. Entries with empty `active_skills` stay in the original cluster.
|
||||||
- If a sub-cluster has ≥3 entries AND names a skill that exists in `skills_root`, mark it as a candidate for `skill_edit` (struggle-driven variant; see "Struggle-driven `skill_edit` eligibility"). Otherwise treat the parent cluster normally.
|
- If a sub-cluster has ≥3 entries AND names a skill that exists in `skills_root`, mark it as a candidate for `skill_edit` (struggle-driven variant; see "Struggle-driven `skill_edit` eligibility"). Otherwise treat the parent cluster normally.
|
||||||
- The umbrella cluster (cross-skill) still emits its usual proposal type (memory, etc.) — sub-clusters do NOT replace it, they supplement it.
|
- The umbrella cluster (cross-skill) still emits its usual proposal type (memory, etc.) — sub-clusters do NOT replace it, they supplement it.
|
||||||
|
- **Utility cross-check**: for every `skill_edit`/`skill_new` candidate, look up the named skill in `skill_utility_path` (the pre-computed `adam-skill-utility.mjs --json` report). If the skill is reliably below baseline (negative `lift` + worst-tier `wLB` at ≥ its `--min` events), apply the utility-lift penalty (see rubric) — do NOT pile more text onto a skill that already correlates with friction; prefer a `memory` or a scoped new skill instead.
|
||||||
6. For each cluster qualifying under the rubric — ≥3 occurrences across ≥2 sessions, OR (for struggle types) ≥1 entry within a single session, OR (for `correction`) ≥3 occurrences across ≥2 cwds:
|
6. For each cluster qualifying under the rubric — ≥3 occurrences across ≥2 sessions, OR (for struggle types) ≥1 entry within a single session, OR (for `correction`) ≥3 occurrences across ≥2 cwds:
|
||||||
a. If cluster topic matches a rejected idea via the rejected-ideas fuzzy set (≥2 token overlap with rejection's `# Why`), skip with reason `"rejected-similar"`.
|
a. If cluster topic matches a rejected idea via the rejected-ideas fuzzy set (≥2 token overlap with rejection's `# Why`), skip with reason `"rejected-similar"`.
|
||||||
b. Pull ~20 messages of transcript context from `transcripts_root` to enrich. Never read full transcripts.
|
b. Pull ~20 messages of transcript context from `transcripts_root` to enrich. Never read full transcripts.
|
||||||
@@ -391,7 +395,7 @@ The skill (`adam-self-improvement/SKILL.md` §1) runs `adam-score.mjs` immediate
|
|||||||
|
|
||||||
## A/B effectiveness
|
## A/B effectiveness
|
||||||
|
|
||||||
Every auto-applied edit (`skill_edit`, `skill_new`, `memory`, `nudge`) gets a one-line tracking entry written to `~/.claude/adam/ab-tracking.jsonl` by `adam-self-improvement/SKILL.md` immediately after the proposal is moved to `applied/`. **`reinforcement` is the one exception — it is a positive-only ledger and is intentionally NOT A/B-tracked (see §"`reinforcement` proposals"), to avoid skewing regression detection.** Schema:
|
Every auto-applied edit (`skill_edit`, `skill_new`, `memory`, `nudge`) gets a one-line tracking entry written to `~/.claude/adam/ab-tracking.jsonl` by `adam-self-improvement/apply-by-type.md` (loaded from SKILL.md) immediately after the proposal is moved to `applied/`. **`reinforcement` is the one exception — it is a positive-only ledger and is intentionally NOT A/B-tracked (see §"`reinforcement` proposals"), to avoid skewing regression detection.** Schema:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"applied_at":<ms>,"proposal_id":"<id>","proposal_type":"...","target_skill":"<slug>","proposal_fingerprint":"<hash>","originating_signals":[{"type":"<signal>","count":<N>,"session_ids":[...]}],"pre_window_days":7}
|
{"applied_at":<ms>,"proposal_id":"<id>","proposal_type":"...","target_skill":"<slug>","proposal_fingerprint":"<hash>","originating_signals":[{"type":"<signal>","count":<N>,"session_ids":[...]}],"pre_window_days":7}
|
||||||
@@ -442,6 +446,7 @@ Sum:
|
|||||||
- Cluster severity-sum ≥ 32: **+1** *(additive — a severity-sum of 32 gets +1 from the previous bullet AND +1 here, total +2.)*
|
- Cluster severity-sum ≥ 32: **+1** *(additive — a severity-sum of 32 gets +1 from the previous bullet AND +1 here, total +2.)*
|
||||||
- Skill-attributed sub-cluster (≥3 entries naming the same `active_skills[0]` that exists in `skills_root`): **+1**
|
- Skill-attributed sub-cluster (≥3 entries naming the same `active_skills[0]` that exists in `skills_root`): **+1**
|
||||||
- Type-bias penalty from feedback loop (≥3 rejections, applied:rejected ratio <1:2 for this `type`): **-1**
|
- Type-bias penalty from feedback loop (≥3 rejections, applied:rejected ratio <1:2 for this `type`): **-1**
|
||||||
|
- Utility-lift penalty: proposal is a `skill_edit` (or a `skill_new` whose recipe duplicates an existing skill's scope) whose target skill is ranked reliably below baseline in `skill_utility_path` — negative `lift` AND worst-tier `wLB`, with event count ≥ its `--min`: **-2** *(adding text to a skill that already correlates with friction raises access while likely lowering utilization — SkillsInjector, cited in `adam-skill-utility.mjs`. Read the skill's row as a review prompt, not proof of causation.)*
|
||||||
- Diagnosis flags `Mismatch: unclear` (causation could not be reconstructed from transcript context): **-1**
|
- Diagnosis flags `Mismatch: unclear` (causation could not be reconstructed from transcript context): **-1**
|
||||||
- Blast radius: low **+1**, medium **0**, high **-1** (default per type — see Proposal types table)
|
- Blast radius: low **+1**, medium **0**, high **-1** (default per type — see Proposal types table)
|
||||||
- Surgical (one file, ≤50 LOC for non-skill_new; ≤80 LOC for skill_new): **+1**
|
- Surgical (one file, ≤50 LOC for non-skill_new; ≤80 LOC for skill_new): **+1**
|
||||||
@@ -471,24 +476,23 @@ Sum:
|
|||||||
|
|
||||||
### `nudge` proposals
|
### `nudge` proposals
|
||||||
|
|
||||||
A `nudge` proposal does NOT modify any persistent rubric/skill/memory artifact. Its sole side-effect is to append an entry to `~/.claude/adam/active-nudges.json` so the next SessionStart hook surfaces a one-line reminder to the user in a *different* session.
|
A `nudge` proposal does NOT modify any persistent rubric/skill/memory artifact. Its sole side-effect is to append an entry to `~/.claude/adam/active-nudges.json` so a future session surfaces a one-line checkpoint reminder — at SessionStart (`adam-nudge.mjs`) and mid-session on the matching cwd (`adam-observe.mjs` on UserPromptSubmit).
|
||||||
|
|
||||||
Trigger: `adam-nudge-eligibility.mjs --session <id>` returns `eligible: true` (i.e. ≥3 `dead_end` entries inside a single session). Distinguished from `skill_edit` precisely because there is no learning artifact to mutate — the action surfaces a checkpoint reminder, not a behavior change.
|
Trigger: `adam-nudge-eligibility.mjs --session <id>` returns `eligible: true` (i.e. ≥3 `dead_end` entries inside a single session). Distinguished from `skill_edit` precisely because there is no learning artifact to mutate — the action surfaces a checkpoint reminder, not a behavior change.
|
||||||
|
|
||||||
`active-nudges.json` entry shape (created by the skill at apply time):
|
Required proposal frontmatter (the entry is built deterministically by `adam-apply-nudge.mjs` — do NOT hand-author the JSON message/cue):
|
||||||
|
|
||||||
```json
|
```yaml
|
||||||
{
|
type: nudge
|
||||||
"kind": "dead_end_reminder",
|
source_session: <originating session_id>
|
||||||
"message": "adam: previous session hit 3 dead_ends — consider a checkpoint before continuing.",
|
cwd: <cluster's dominant cwd — becomes cue.cwd; adam-batch.mjs aggregates `cwds` per batch>
|
||||||
"created_at": <ms>,
|
dead_end_count: <N ≥ 3 from adam-nudge-eligibility.mjs>
|
||||||
"expires_at_ts": <ms now + 7 days>,
|
confidence: <int>
|
||||||
"max_displays": 3,
|
blast_radius: low
|
||||||
"displays_used": 0,
|
|
||||||
"source_session": "<originating session_id>"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The helper writes `{kind, message, created_at, expires_at_ts: now+7d, max_displays: 3, displays_used: 0, source_session, cue: {cwd}}`. The `cue.cwd` binds the reminder to the project it came from: both surfaces (SessionStart + mid-session) fire the nudge only when the session's cwd matches, deferring it elsewhere without consuming a display (ReContext-style query-conditioned replay). Omit `cwd` from frontmatter only for a deliberately project-agnostic reminder (then the nudge has no cue and surfaces unconditionally at SessionStart, as legacy).
|
||||||
|
|
||||||
### `reinforcement` proposals
|
### `reinforcement` proposals
|
||||||
|
|
||||||
A `reinforcement` proposal is logged when `adam-score.mjs` reports `count >= 3` clean `task_completed` events citing the same `active_skills[0]` slug. Frontmatter MUST include `skill_slug`, `count`, `source_session`, `confidence`, `blast_radius: low`. Apply gate (`confidence >= 4 AND blast_radius == low`) is identical to the `memory` gate — when both hold, the skill invokes `~/.claude/adam/scripts/adam-apply-reinforcement.mjs <proposal-path>` which appends one JSON line to `~/.claude/adam/reinforcements.jsonl` of shape `{ts, skill_slug, count, source_session}`. No code/memory/skill modifications either side of the gate — reinforcements are a positive-only ledger, separate from `ab-tracking.jsonl` (A/B intentionally does NOT measure positive signals to avoid skewing regression detection).
|
A `reinforcement` proposal is logged when `adam-score.mjs` reports `count >= 3` clean `task_completed` events citing the same `active_skills[0]` slug. Frontmatter MUST include `skill_slug`, `count`, `source_session`, `confidence`, `blast_radius: low`. Apply gate (`confidence >= 4 AND blast_radius == low`) is identical to the `memory` gate — when both hold, the skill invokes `~/.claude/adam/scripts/adam-apply-reinforcement.mjs <proposal-path>` which appends one JSON line to `~/.claude/adam/reinforcements.jsonl` of shape `{ts, skill_slug, count, source_session}`. No code/memory/skill modifications either side of the gate — reinforcements are a positive-only ledger, separate from `ab-tracking.jsonl` (A/B intentionally does NOT measure positive signals to avoid skewing regression detection).
|
||||||
|
|||||||
+24
-7
@@ -3,7 +3,9 @@
|
|||||||
// 1. Pending proposals (≥3 queued in adam/proposals/).
|
// 1. Pending proposals (≥3 queued in adam/proposals/).
|
||||||
// 2. Cross-session nudges (entries in adam/active-nudges.json whose
|
// 2. Cross-session nudges (entries in adam/active-nudges.json whose
|
||||||
// source_session differs from the current session and that haven't
|
// source_session differs from the current session and that haven't
|
||||||
// expired or exhausted their max_displays).
|
// expired or exhausted their max_displays). An entry may carry an optional
|
||||||
|
// `cue.cwd`; when set, it only surfaces in a session whose cwd matches
|
||||||
|
// (otherwise it is deferred, not consumed).
|
||||||
// 3. Pending local-edit upgrades (`.adam-new` sidecars).
|
// 3. Pending local-edit upgrades (`.adam-new` sidecars).
|
||||||
// 4. New-release notice: if a newer GitHub release exists than the installed
|
// 4. New-release notice: if a newer GitHub release exists than the installed
|
||||||
// `.version`, print a notify-only one-line update prompt. Cached + checked
|
// `.version`, print a notify-only one-line update prompt. Cached + checked
|
||||||
@@ -37,6 +39,7 @@ const PENDING_CHECK_PATHS = [
|
|||||||
"hooks/adam-nudge.mjs",
|
"hooks/adam-nudge.mjs",
|
||||||
"agents/adam.md",
|
"agents/adam.md",
|
||||||
"skills/adam-self-improvement/SKILL.md",
|
"skills/adam-self-improvement/SKILL.md",
|
||||||
|
"skills/adam-self-improvement/apply-by-type.md",
|
||||||
"commands/reflect.md",
|
"commands/reflect.md",
|
||||||
"adam/scripts/adam-archive.mjs",
|
"adam/scripts/adam-archive.mjs",
|
||||||
"adam/scripts/adam-upgrade.mjs",
|
"adam/scripts/adam-upgrade.mjs",
|
||||||
@@ -46,7 +49,9 @@ const PENDING_CHECK_PATHS = [
|
|||||||
"adam/scripts/adam-cooldown.mjs",
|
"adam/scripts/adam-cooldown.mjs",
|
||||||
"adam/scripts/adam-score.mjs",
|
"adam/scripts/adam-score.mjs",
|
||||||
"adam/scripts/adam-ab-measure.mjs",
|
"adam/scripts/adam-ab-measure.mjs",
|
||||||
|
"adam/scripts/adam-skill-utility.mjs",
|
||||||
"adam/scripts/adam-apply-reinforcement.mjs",
|
"adam/scripts/adam-apply-reinforcement.mjs",
|
||||||
|
"adam/scripts/adam-apply-nudge.mjs",
|
||||||
"adam/scripts/adam-utils.mjs",
|
"adam/scripts/adam-utils.mjs",
|
||||||
"adam/scripts/adam-batch.mjs",
|
"adam/scripts/adam-batch.mjs",
|
||||||
"adam/scripts/adam-rollback.mjs",
|
"adam/scripts/adam-rollback.mjs",
|
||||||
@@ -59,13 +64,13 @@ function readJson(path, fallback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readSessionInput() {
|
function readSessionInput() {
|
||||||
// SessionStart payload arrives on stdin; capture session_id if present.
|
// SessionStart payload arrives on stdin; return the parsed object so callers
|
||||||
// We don't block on stdin — best-effort, non-blocking.
|
// can read both session_id and cwd. We don't block on stdin — best-effort.
|
||||||
try {
|
try {
|
||||||
const buf = readFileSync(0, "utf8");
|
const buf = readFileSync(0, "utf8");
|
||||||
if (!buf) return null;
|
if (!buf) return null;
|
||||||
const parsed = JSON.parse(buf);
|
const parsed = JSON.parse(buf);
|
||||||
return parsed && typeof parsed.session_id === "string" ? parsed.session_id : null;
|
return parsed && typeof parsed === "object" ? parsed : null;
|
||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +84,7 @@ function emitProposalReminder() {
|
|||||||
} catch { /* proposals dir absent → silent */ }
|
} catch { /* proposals dir absent → silent */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitActiveNudges(currentSession) {
|
function emitActiveNudges(currentSession, currentCwd) {
|
||||||
if (!existsSync(NUDGES_FILE)) return;
|
if (!existsSync(NUDGES_FILE)) return;
|
||||||
const raw = readJson(NUDGES_FILE, null);
|
const raw = readJson(NUDGES_FILE, null);
|
||||||
if (!Array.isArray(raw)) return;
|
if (!Array.isArray(raw)) return;
|
||||||
@@ -99,6 +104,16 @@ function emitActiveNudges(currentSession) {
|
|||||||
kept.push(entry);
|
kept.push(entry);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Cue gate (optional, additive): a nudge may carry `cue.cwd` binding it to
|
||||||
|
// the project it originated in. When set and the current session's cwd does
|
||||||
|
// not match, defer it — keep the entry, do NOT consume a display — instead of
|
||||||
|
// surfacing an irrelevant reminder. Nudges without a cue print unconditionally,
|
||||||
|
// exactly as before (ReContext P5/P7: replay should be conditioned on the cue).
|
||||||
|
if (entry.cue && typeof entry.cue === "object" && entry.cue.cwd &&
|
||||||
|
currentCwd && entry.cue.cwd !== currentCwd) {
|
||||||
|
kept.push(entry);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (typeof entry.message === "string" && entry.message) {
|
if (typeof entry.message === "string" && entry.message) {
|
||||||
process.stdout.write(entry.message + "\n");
|
process.stdout.write(entry.message + "\n");
|
||||||
const nextUsed = used + 1;
|
const nextUsed = used + 1;
|
||||||
@@ -201,14 +216,16 @@ async function emitUpdateCheck() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const stdinSession = readSessionInput();
|
const payload = readSessionInput();
|
||||||
|
const stdinSession = payload && typeof payload.session_id === "string" ? payload.session_id : null;
|
||||||
const stateSession = (() => {
|
const stateSession = (() => {
|
||||||
const st = readJson(STATE_FILE, null);
|
const st = readJson(STATE_FILE, null);
|
||||||
return st && typeof st.session_id === "string" ? st.session_id : null;
|
return st && typeof st.session_id === "string" ? st.session_id : null;
|
||||||
})();
|
})();
|
||||||
const currentSession = stdinSession || stateSession || "";
|
const currentSession = stdinSession || stateSession || "";
|
||||||
|
const currentCwd = (payload && typeof payload.cwd === "string" && payload.cwd) || process.cwd();
|
||||||
emitProposalReminder();
|
emitProposalReminder();
|
||||||
emitActiveNudges(currentSession);
|
emitActiveNudges(currentSession, currentCwd);
|
||||||
emitPendingUpgrades();
|
emitPendingUpgrades();
|
||||||
await emitUpdateCheck();
|
await emitUpdateCheck();
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-5
@@ -249,6 +249,12 @@ function snapshotContext(state) {
|
|||||||
|
|
||||||
function errorFingerprint(toolResponse) {
|
function errorFingerprint(toolResponse) {
|
||||||
if (!toolResponse) return null;
|
if (!toolResponse) return null;
|
||||||
|
// Read/Edit/Write success objects carry `file`/`filePath` and no `is_error`.
|
||||||
|
// PostToolUse never receives failed calls (CC routes them to
|
||||||
|
// PostToolUseFailure), so ERROR_RE here would only match source-file text.
|
||||||
|
if (typeof toolResponse === "object" && !Array.isArray(toolResponse) &&
|
||||||
|
toolResponse.is_error !== true &&
|
||||||
|
("file" in toolResponse || "filePath" in toolResponse)) return null;
|
||||||
let text = "";
|
let text = "";
|
||||||
if (typeof toolResponse === "string") text = toolResponse;
|
if (typeof toolResponse === "string") text = toolResponse;
|
||||||
else if (toolResponse.content !== undefined) {
|
else if (toolResponse.content !== undefined) {
|
||||||
@@ -289,6 +295,37 @@ function errorFingerprint(toolResponse) {
|
|||||||
if (!normalized) return null;
|
if (!normalized) return null;
|
||||||
return `raw:${djb2(normalized)}`;
|
return `raw:${djb2(normalized)}`;
|
||||||
}
|
}
|
||||||
|
// Mid-session nudge reactivation (ReContext P5/P7): on UserPromptSubmit, surface a
|
||||||
|
// stored active-nudge whose `cue.cwd` matches the current directory, so a deferred
|
||||||
|
// reminder lands when the user is actually working in that project instead of only
|
||||||
|
// at SessionStart. Read-only — SessionStart (adam-nudge.mjs) owns display accounting;
|
||||||
|
// this path surfaces each matching nudge at most once per session (state.surfaced_nudges)
|
||||||
|
// and respects expiry + max_displays. Opt out with ADAM_NO_MIDSESSION_NUDGE.
|
||||||
|
// UserPromptSubmit hook stdout is injected into the session context, so this is the
|
||||||
|
// reliable surface (unlike PostToolUse). Any failure is swallowed — never block a prompt.
|
||||||
|
function surfaceMatchingNudges(state, cwd) {
|
||||||
|
if (process.env.ADAM_NO_MIDSESSION_NUDGE || !cwd) return;
|
||||||
|
const nudgesPath = join(ROOT, "active-nudges.json");
|
||||||
|
let raw;
|
||||||
|
try { raw = JSON.parse(readFileSync(nudgesPath, "utf8")); } catch { return; }
|
||||||
|
if (!Array.isArray(raw)) return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (!Array.isArray(state.surfaced_nudges)) state.surfaced_nudges = [];
|
||||||
|
for (const n of raw) {
|
||||||
|
if (!n || typeof n !== "object" || typeof n.message !== "string" || !n.message) continue;
|
||||||
|
if (Number(n.expires_at_ts || 0) <= now) continue;
|
||||||
|
const max = Number(n.max_displays || 0);
|
||||||
|
if (max > 0 && Number(n.displays_used || 0) >= max) continue;
|
||||||
|
const cue = n.cue && typeof n.cue === "object" ? n.cue : null;
|
||||||
|
if (!cue || !cue.cwd || cue.cwd !== cwd) continue; // cue-conditioned on cwd
|
||||||
|
const id = String(n.created_at || n.message);
|
||||||
|
if (state.surfaced_nudges.includes(id)) continue;
|
||||||
|
process.stdout.write(n.message + "\n");
|
||||||
|
state.surfaced_nudges.push(id);
|
||||||
|
if (state.surfaced_nudges.length > 50) state.surfaced_nudges.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function resetFrictionCounters(state) {
|
function resetFrictionCounters(state) {
|
||||||
state.tools_since_user = 0;
|
state.tools_since_user = 0;
|
||||||
@@ -315,6 +352,7 @@ function resetSessionLocal(state) {
|
|||||||
state.task_tool_kinds = {};
|
state.task_tool_kinds = {};
|
||||||
state.task_tool_count = 0;
|
state.task_tool_count = 0;
|
||||||
state.task_corrections = 0;
|
state.task_corrections = 0;
|
||||||
|
state.surfaced_nudges = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureStateDefaults(state) {
|
function ensureStateDefaults(state) {
|
||||||
@@ -339,6 +377,7 @@ function ensureStateDefaults(state) {
|
|||||||
if (!Array.isArray(state.recentRecoveries)) state.recentRecoveries = [];
|
if (!Array.isArray(state.recentRecoveries)) state.recentRecoveries = [];
|
||||||
if (typeof state.session_post_count !== "number") state.session_post_count = 0;
|
if (typeof state.session_post_count !== "number") state.session_post_count = 0;
|
||||||
if (!Array.isArray(state.context_ring)) state.context_ring = [];
|
if (!Array.isArray(state.context_ring)) state.context_ring = [];
|
||||||
|
if (!Array.isArray(state.surfaced_nudges)) state.surfaced_nudges = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
@@ -364,6 +403,7 @@ function main() {
|
|||||||
if (event === "UserPromptSubmit") {
|
if (event === "UserPromptSubmit") {
|
||||||
const prompt = (input.prompt || "").slice(0, 200);
|
const prompt = (input.prompt || "").slice(0, 200);
|
||||||
pushContext(state, { event: "user", prompt: excerpt(prompt, CONTEXT_EXCERPT_LEN), ts });
|
pushContext(state, { event: "user", prompt: excerpt(prompt, CONTEXT_EXCERPT_LEN), ts });
|
||||||
|
surfaceMatchingNudges(state, cwd);
|
||||||
if (isCorrection(prompt)) {
|
if (isCorrection(prompt)) {
|
||||||
const last = state.tool_window[state.tool_window.length - 1] || {};
|
const last = state.tool_window[state.tool_window.length - 1] || {};
|
||||||
appendJournal({
|
appendJournal({
|
||||||
@@ -425,12 +465,17 @@ function main() {
|
|||||||
state.subagent_dispatch_emitted[name] = true;
|
state.subagent_dispatch_emitted[name] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (event === "PostToolUse") {
|
} else if (event === "PostToolUse" || event === "PostToolUseFailure") {
|
||||||
const tool = input.tool_name || "unknown";
|
const tool = input.tool_name || "unknown";
|
||||||
const argsHash = djb2(JSON.stringify(input.tool_input || {}));
|
const argsHash = djb2(JSON.stringify(input.tool_input || {}));
|
||||||
const file = (input.tool_input && (input.tool_input.file_path || input.tool_input.path)) || null;
|
const file = (input.tool_input && (input.tool_input.file_path || input.tool_input.path)) || null;
|
||||||
|
|
||||||
const toolResponse = input.tool_response;
|
// Claude Code sends failed tool calls to PostToolUseFailure with the error
|
||||||
|
// text in `error`. Shape it like an is_error response so every PostToolUse
|
||||||
|
// detector sees real failures. An interrupt is an abort, not a tool error.
|
||||||
|
const toolResponse = event === "PostToolUseFailure"
|
||||||
|
? { is_error: input.is_interrupt !== true, content: String(input.error || "") }
|
||||||
|
: input.tool_response;
|
||||||
const respExcerpt = (() => {
|
const respExcerpt = (() => {
|
||||||
if (!toolResponse) return null;
|
if (!toolResponse) return null;
|
||||||
const text = typeof toolResponse === "string" ? toolResponse
|
const text = typeof toolResponse === "string" ? toolResponse
|
||||||
@@ -501,11 +546,11 @@ function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.tool_response && typeof input.tool_response === "object") {
|
if (toolResponse && typeof toolResponse === "object") {
|
||||||
bumpUsage("payload:tool_response_seen");
|
bumpUsage("payload:tool_response_seen");
|
||||||
}
|
}
|
||||||
|
|
||||||
const fp = errorFingerprint(input.tool_response);
|
const fp = errorFingerprint(toolResponse);
|
||||||
if (fp) {
|
if (fp) {
|
||||||
bumpUsage("payload:tool_response_error_seen");
|
bumpUsage("payload:tool_response_error_seen");
|
||||||
if (state.recentRecoveries.length) {
|
if (state.recentRecoveries.length) {
|
||||||
@@ -550,7 +595,7 @@ function main() {
|
|||||||
if (tool === "Bash") {
|
if (tool === "Bash") {
|
||||||
const cmd = (input.tool_input && input.tool_input.command) || "";
|
const cmd = (input.tool_input && input.tool_input.command) || "";
|
||||||
const isBuildCmd = BUILD_RE.test(cmd);
|
const isBuildCmd = BUILD_RE.test(cmd);
|
||||||
const hasError = (input.tool_response && input.tool_response.is_error === true) || fp !== null;
|
const hasError = (toolResponse && toolResponse.is_error === true) || fp !== null;
|
||||||
if (isBuildCmd && hasError) {
|
if (isBuildCmd && hasError) {
|
||||||
state.build_failure_count += 1;
|
state.build_failure_count += 1;
|
||||||
if (state.build_failure_count >= BUILD_LOOP_THRESHOLD && !state.build_loop_emitted) {
|
if (state.build_failure_count >= BUILD_LOOP_THRESHOLD && !state.build_loop_emitted) {
|
||||||
|
|||||||
+3
-2
@@ -120,13 +120,14 @@ copy_file "$SRC/hooks/adam-nudge.mjs" "$DEST/hook
|
|||||||
# Agent / skill / command
|
# Agent / skill / command
|
||||||
copy_file "$SRC/agents/adam.md" "$DEST/agents/adam.md"
|
copy_file "$SRC/agents/adam.md" "$DEST/agents/adam.md"
|
||||||
copy_file "$SRC/skills/adam-self-improvement/SKILL.md" "$DEST/skills/adam-self-improvement/SKILL.md"
|
copy_file "$SRC/skills/adam-self-improvement/SKILL.md" "$DEST/skills/adam-self-improvement/SKILL.md"
|
||||||
|
copy_file "$SRC/skills/adam-self-improvement/apply-by-type.md" "$DEST/skills/adam-self-improvement/apply-by-type.md"
|
||||||
copy_file "$SRC/commands/reflect.md" "$DEST/commands/reflect.md"
|
copy_file "$SRC/commands/reflect.md" "$DEST/commands/reflect.md"
|
||||||
# Adam internals
|
# Adam internals
|
||||||
copy_file "$SRC/adam/scripts/adam-archive.mjs" "$DEST/adam/scripts/adam-archive.mjs"
|
copy_file "$SRC/adam/scripts/adam-archive.mjs" "$DEST/adam/scripts/adam-archive.mjs"
|
||||||
copy_file "$SRC/adam/scripts/adam-upgrade.mjs" "$DEST/adam/scripts/adam-upgrade.mjs"
|
copy_file "$SRC/adam/scripts/adam-upgrade.mjs" "$DEST/adam/scripts/adam-upgrade.mjs"
|
||||||
# v0.3.3 helper scripts — invoked from SKILL.md / hooks / analyst flow
|
# v0.3.3 helper scripts — invoked from SKILL.md / hooks / analyst flow
|
||||||
for _adam_script in adam-utils adam-window adam-explain adam-nudge-eligibility adam-cooldown \
|
for _adam_script in adam-utils adam-window adam-explain adam-nudge-eligibility adam-cooldown \
|
||||||
adam-score adam-ab-measure adam-apply-reinforcement adam-batch adam-rollback \
|
adam-score adam-ab-measure adam-apply-reinforcement adam-apply-nudge adam-batch adam-rollback \
|
||||||
adam-skill-utility; do
|
adam-skill-utility; do
|
||||||
copy_file "$SRC/adam/scripts/${_adam_script}.mjs" \
|
copy_file "$SRC/adam/scripts/${_adam_script}.mjs" \
|
||||||
"$DEST/adam/scripts/${_adam_script}.mjs"
|
"$DEST/adam/scripts/${_adam_script}.mjs"
|
||||||
@@ -222,7 +223,7 @@ log ""
|
|||||||
log "installed:"
|
log "installed:"
|
||||||
log " hooks/adam-observe.mjs, hooks/adam-nudge.mjs"
|
log " hooks/adam-observe.mjs, hooks/adam-nudge.mjs"
|
||||||
log " agents/adam.md"
|
log " agents/adam.md"
|
||||||
log " skills/adam-self-improvement/SKILL.md"
|
log " skills/adam-self-improvement/SKILL.md, apply-by-type.md"
|
||||||
log " commands/reflect.md"
|
log " commands/reflect.md"
|
||||||
log " adam/scripts/adam-archive.mjs"
|
log " adam/scripts/adam-archive.mjs"
|
||||||
log " adam/scripts/adam-upgrade.mjs"
|
log " adam/scripts/adam-upgrade.mjs"
|
||||||
|
|||||||
@@ -34,6 +34,17 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"PostToolUseFailure": [
|
||||||
|
{
|
||||||
|
"matcher": "",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"$HOME/.claude/hooks/adam-observe.mjs\""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"SessionStart": [
|
"SessionStart": [
|
||||||
{
|
{
|
||||||
"hooks": [
|
"hooks": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: adam-self-improvement
|
name: adam-self-improvement
|
||||||
description: Use when the user types /reflect, asks "what has adam learned", asks to "review proposals", or wants to inspect the self-improvement queue. Dispatches the adam subagent to analyse the observation journal and presents proposals for approve/reject/edit.
|
description: Runs the ADAM /reflect review loop. Use when the user types /reflect or /reflect --explain, asks "what has adam learned", asks to "review proposals" or "review the queue", or acts on a SessionStart nudge about pending proposals. Pre-filters and scores the observation journal, dispatches the adam subagent in two stages, auto-applies verified high-confidence proposals, and walks the rest of the queue for approve, reject or edit.
|
||||||
---
|
---
|
||||||
|
|
||||||
# adam-self-improvement
|
# adam-self-improvement
|
||||||
@@ -8,90 +8,45 @@ description: Use when the user types /reflect, asks "what has adam learned", ask
|
|||||||
## When to invoke
|
## When to invoke
|
||||||
|
|
||||||
- User types `/reflect`
|
- User types `/reflect`
|
||||||
- User types `/reflect --explain` (same flow, but the analyst's clustering trace is shown to the user — see §2b below)
|
- User types `/reflect --explain` (same flow, but the analyst's clustering trace is shown to the user — see §2b)
|
||||||
- User asks: "what has adam learned", "any proposals", "review the queue"
|
- User asks: "what has adam learned", "any proposals", "review the queue"
|
||||||
- SessionStart nudge said proposals are pending and user wants to act on it
|
- SessionStart nudge said proposals are pending and user wants to act on it
|
||||||
|
|
||||||
|
## Supporting file
|
||||||
|
|
||||||
|
- [apply-by-type.md](apply-by-type.md): the Karpathy constraints and the per-type apply, move, A/B-tracking, archive, reject and edit procedures. **Read it before any apply in §3a or §4.** Its checks are mandatory. If no proposal reaches §3a or §4, you do not need it.
|
||||||
|
|
||||||
## Protocol
|
## Protocol
|
||||||
|
|
||||||
### 0. Parse flags
|
### 0. Parse flags
|
||||||
|
|
||||||
Check the slash-command argument string for the literal token `--explain`. Set `explain=true` when present; otherwise `explain=false`. Unknown flags: print one-line warning, continue with `explain=false`. This single flag is the only argument `/reflect` currently accepts.
|
Check the slash-command argument string for the literal token `--explain`. Set `explain=true` when present, otherwise `explain=false`. Unknown flags: print a one-line warning, continue with `explain=false`. `--explain` is the only argument `/reflect` accepts.
|
||||||
|
|
||||||
### 1. Pre-filter the journal (window + exclusion) + score
|
### 1. Pre-filter the journal (window + exclusion) + score
|
||||||
|
|
||||||
Before dispatching the analyst, run the windowed-journal filter:
|
Run these steps in order before dispatching the analyst. Each step writes stdout to the output file and stderr to the log file. On failure, show the stderr log to the user and apply the fallback. No failure stops the run.
|
||||||
|
|
||||||
```bash
|
| # | Step | Command | Fallback on non-zero exit |
|
||||||
node ~/.claude/adam/scripts/adam-window.mjs --home ~/.claude > /tmp/adam-windowed-journal.jsonl 2> /tmp/adam-windowed-journal.log
|
|---|------|---------|---------------------------|
|
||||||
```
|
| 1 | Window filter | `node ~/.claude/adam/scripts/adam-window.mjs --home ~/.claude > /tmp/adam-windowed-journal.jsonl 2> /tmp/adam-windowed-journal.log` | Pass the raw `~/.claude/adam/journal.jsonl` path to the agent. Its manual excluded-timestamps logic still filters actioned entries. Only the freshness window is lost. |
|
||||||
|
| 2 | Score | `node ~/.claude/adam/scripts/adam-score.mjs --input /tmp/adam-windowed-journal.jsonl > /tmp/adam-scores.json 2> /tmp/adam-scores.log` | Pass `{"sessions":[],"reinforcement_candidates":[]}` (dampener defaults to 1.0). |
|
||||||
|
| 3 | A/B measure | `node ~/.claude/adam/scripts/adam-ab-measure.mjs --home ~/.claude --format json > /tmp/adam-ab-regressions.json 2> /tmp/adam-ab-regressions.log` | Pass `[]`. |
|
||||||
|
| 4 | Auto-rollback (only if step 3 found `regressed`) | `node ~/.claude/adam/scripts/adam-rollback.mjs --auto --home ~/.claude > /tmp/adam-rollback-results.json 2> /tmp/adam-rollback.log` | Continue (best-effort). |
|
||||||
|
| 5 | Evidence batching | `node ~/.claude/adam/scripts/adam-batch.mjs --input /tmp/adam-windowed-journal.jsonl > /tmp/adam-batches.json 2> /tmp/adam-batch.log` | Pass `null`. The analyst falls back to raw journal clustering. |
|
||||||
|
| 6 | Skill utility | `node ~/.claude/adam/scripts/adam-skill-utility.mjs --input /tmp/adam-windowed-journal.jsonl --json > /tmp/adam-skill-utility.json 2> /tmp/adam-skill-utility.log` | Skip (best-effort). |
|
||||||
|
|
||||||
The script reads the active journal plus all rotated journal files (new
|
What each step does and how to use its output:
|
||||||
`journal/YYYY-Www.jsonl` weekly format AND legacy
|
|
||||||
`journal/YYYY-MM-DD-<ts>.jsonl` size-rotated format are both supported), applies
|
|
||||||
per-signal-type sliding windows (see `SIGNAL_WINDOWS_DAYS` in
|
|
||||||
`adam-window.mjs`), and drops entries already actioned via
|
|
||||||
`applied/*.md` / `rejected/*.md` frontmatter `source_entries`.
|
|
||||||
|
|
||||||
If `adam-window.mjs` exits non-zero: log the stderr file to the user, fall
|
1. **Window filter.** Reads the active journal and all rotated journals (weekly `journal/YYYY-Www.jsonl` and legacy size-rotated `journal/YYYY-MM-DD-<ts>.jsonl`). Applies per-signal-type sliding windows (`SIGNAL_WINDOWS_DAYS` in `adam-window.mjs`). Drops entries already actioned through the `source_entries` frontmatter of `applied/*.md` and `rejected/*.md`.
|
||||||
through to passing the raw `~/.claude/adam/journal.jsonl` path to the agent
|
2. **Score.** Produces a per-session `dampener` (0.5 / 0.75 / 1.0, from `task_completed_count`) and a `reinforcement_candidates` list (skills cited by ≥3 clean `task_completed` events). The analyst uses both (`agents/adam.md` §"Scoring: task_completed dampener").
|
||||||
(graceful degradation — the agent's manual excluded-timestamps logic still
|
3. **A/B measure.** Runs on previously auto-applied proposals (see the A/B tracking append in apply-by-type.md). Output is an array of delta objects (`pre_count`, `post_count`, `delta_pct`, `status` ∈ {`improved`,`neutral`,`regressed`,`no_baseline`,`pending`}). Filter to `status == "regressed"` and pass that as `ab_regressions`. When the list is non-empty, the analyst must put a `## Regressions` section at the top of its output (`agents/adam.md` §"A/B effectiveness").
|
||||||
filters actioned entries; only the freshness window is lost).
|
4. **Auto-rollback** (MOSS §3.5). Reverts regressed proposals before analyst dispatch. The script moves each one from `applied/` back to `proposals/` with `rolled_back: true` and creates a regression nudge. For each one, print: `adam: rolled back "<proposal_id>" — regression detected (delta: <delta_pct>%)`.
|
||||||
|
5. **Evidence batching** (MOSS §3.1). Groups entries by (signal_type, cluster_key) into coherent failure batches, with per-batch metadata such as `has_context_window` (transcript evidence attached).
|
||||||
Then run the scoring pre-step on the same windowed journal:
|
6. **Skill utility** (execution-grounded selection signal, after SkillsInjector arXiv 2605.29794: utility Δ(s), not surface match). Surfaces advisory candidates for description disambiguation or archival. Ranks skills by co-occurrence with positive (`task_completed`, `clean_recovery`, `correction_free_streak`) vs negative outcome events. **CO-OCCURRENCE, NOT CAUSATION.** Show the worst 3 below-baseline skills (`lift < 0`, not low-sample) to the *user* as a one-line advisory before listing proposals (e.g. `skill-utility: chezmoi 9% pos n=85, ghostty-config 14% pos n=50, …`). Do NOT feed this into the analyst's proposal machinery or auto-draft skill archival from it. The human decides.
|
||||||
|
|
||||||
```bash
|
|
||||||
node ~/.claude/adam/scripts/adam-score.mjs --input /tmp/adam-windowed-journal.jsonl > /tmp/adam-scores.json 2> /tmp/adam-scores.log
|
|
||||||
```
|
|
||||||
|
|
||||||
This produces a per-session `dampener` (0.5 / 0.75 / 1.0 based on
|
|
||||||
`task_completed_count`) and a `reinforcement_candidates` list (skills cited by
|
|
||||||
≥3 clean `task_completed` events). The analyst uses both — see
|
|
||||||
`agents/adam.md` §"Scoring: task_completed dampener". If the score step fails,
|
|
||||||
log stderr to the user and pass an empty `{"sessions":[],"reinforcement_candidates":[]}`
|
|
||||||
to the analyst (dampener defaults to 1.0).
|
|
||||||
|
|
||||||
Finally, run the A/B measurement pre-step on any previously auto-applied
|
|
||||||
proposals (see §3 ab-tracking write):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node ~/.claude/adam/scripts/adam-ab-measure.mjs --home ~/.claude --format json > /tmp/adam-ab-regressions.json 2> /tmp/adam-ab-regressions.log
|
|
||||||
```
|
|
||||||
|
|
||||||
The JSON output is an array of A/B delta objects (`pre_count`, `post_count`,
|
|
||||||
`delta_pct`, `status` ∈ {`improved`,`neutral`,`regressed`,`no_baseline`,`pending`}).
|
|
||||||
Filter to `status == "regressed"` before passing to the analyst as
|
|
||||||
`ab_regressions`. The analyst is required (see `agents/adam.md` §"A/B
|
|
||||||
effectiveness") to surface a `## Regressions` section at the top of its output
|
|
||||||
when this list is non-empty. If the script fails: log stderr, pass `[]`.
|
|
||||||
|
|
||||||
**Auto-rollback** (MOSS §3.5): if any entries have `status == "regressed"`, run the rollback script to auto-revert them before analyst dispatch:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node ~/.claude/adam/scripts/adam-rollback.mjs --auto --home ~/.claude > /tmp/adam-rollback-results.json 2> /tmp/adam-rollback.log
|
|
||||||
```
|
|
||||||
|
|
||||||
For each rolled-back proposal, print to user: `adam: rolled back "<proposal_id>" — regression detected (delta: <delta_pct>%)`. The rollback script moves the proposal from `applied/` back to `proposals/` with `rolled_back: true` and creates a regression nudge. If the script fails: log stderr, continue (rollback is best-effort).
|
|
||||||
|
|
||||||
**Evidence batching** (MOSS §3.1): pre-cluster the windowed journal into coherent failure batches:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node ~/.claude/adam/scripts/adam-batch.mjs --input /tmp/adam-windowed-journal.jsonl > /tmp/adam-batches.json 2> /tmp/adam-batch.log
|
|
||||||
```
|
|
||||||
|
|
||||||
This groups entries by (signal_type, cluster_key) and reports per-batch metadata including `has_context_window` (whether transcript evidence is attached). If the script fails: log stderr, pass `null` to the analyst (graceful degradation — analyst falls back to raw journal clustering).
|
|
||||||
|
|
||||||
**Skill utility** (execution-grounded selection signal, in the spirit of SkillsInjector arXiv 2605.29794 — utility Δ(s), not surface match): compute per-skill good:bad outcome ratios over the windowed journal:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node ~/.claude/adam/scripts/adam-skill-utility.mjs --input /tmp/adam-windowed-journal.jsonl --json > /tmp/adam-skill-utility.json 2> /tmp/adam-skill-utility.log
|
|
||||||
```
|
|
||||||
|
|
||||||
This ranks skills by how often they co-occur with positive (`task_completed`, `clean_recovery`, `correction_free_streak`) vs negative outcome events, surfacing skills below the baseline positive rate (with sufficient sample) — advisory candidates for description disambiguation or archival. **CO-OCCURRENCE, NOT CAUSATION**: display the worst 3 below-baseline skills (`lift < 0`, not low-sample) to the *user* as a one-line advisory before listing proposals (e.g. `skill-utility: chezmoi 9% pos n=85, ghostty-config 14% pos n=50, …`). Do NOT feed this into the analyst's proposal machinery or auto-draft skill-archival from it — the human decides. If the script fails: log stderr, skip (best-effort).
|
|
||||||
|
|
||||||
### 2. Dispatch the analyst (two-stage pipeline)
|
### 2. Dispatch the analyst (two-stage pipeline)
|
||||||
|
|
||||||
MOSS §3.3: "A single prompt asked to diagnose, plan, implement, verify, and decide overloads context and produces lower-quality output than a sequenced flow." The analyst is dispatched in two stages with a validation gate between them.
|
MOSS §3.3: "A single prompt asked to diagnose, plan, implement, verify, and decide overloads context and produces lower-quality output than a sequenced flow." Dispatch the analyst in two stages with a validation gate between them.
|
||||||
|
|
||||||
**Stage 1 — Diagnose + Plan**: Use the Agent tool with `subagent_type: "adam"` and prompt:
|
**Stage 1 — Diagnose + Plan**: Use the Agent tool with `subagent_type: "adam"` and prompt:
|
||||||
|
|
||||||
@@ -107,6 +62,7 @@ Inputs:
|
|||||||
- batches_path: /tmp/adam-batches.json # pre-clustered evidence batches
|
- batches_path: /tmp/adam-batches.json # pre-clustered evidence batches
|
||||||
- scores_path: /tmp/adam-scores.json
|
- scores_path: /tmp/adam-scores.json
|
||||||
- ab_regressions_path: /tmp/adam-ab-regressions.json
|
- ab_regressions_path: /tmp/adam-ab-regressions.json
|
||||||
|
- skill_utility_path: /tmp/adam-skill-utility.json # per-skill utility Δ (worst-first); already computed in pre-analysis
|
||||||
- journal_path: ~/.claude/adam/journal.jsonl # raw — fallback only
|
- journal_path: ~/.claude/adam/journal.jsonl # raw — fallback only
|
||||||
- state_path: ~/.claude/adam/state.json
|
- state_path: ~/.claude/adam/state.json
|
||||||
- usage_path: ~/.claude/adam/usage.json
|
- usage_path: ~/.claude/adam/usage.json
|
||||||
@@ -131,6 +87,8 @@ Wait for return.
|
|||||||
|
|
||||||
If all diagnoses are removed or the file is missing/empty, print "adam: no valid diagnoses — nothing to implement" and skip to §6.
|
If all diagnoses are removed or the file is missing/empty, print "adam: no valid diagnoses — nothing to implement" and skip to §6.
|
||||||
|
|
||||||
|
**Adaptive re-diagnose** (ReContext P5 — spend depth only where it is needed): after validation, collect validated diagnoses whose `mismatch` is `unclear` OR whose cluster severity-sum is top-tier (≥32, per `scores_path`); take at most the top 3 by severity. If any exist, re-dispatch **one** more `stage=diagnose` pass carrying `redigest: <their cluster_ids>` and `prior_diagnoses_path: /tmp/adam-diagnoses.json` (the analyst refines only those clusters in place), then re-run the inter-stage validation above on the refreshed file. Do this at most once per `/reflect`; clear clusters are never re-diagnosed. If none qualify, go straight to Stage 2.
|
||||||
|
|
||||||
**Stage 2 — Implement**: Use the Agent tool with `subagent_type: "adam"` and prompt:
|
**Stage 2 — Implement**: Use the Agent tool with `subagent_type: "adam"` and prompt:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -143,6 +101,7 @@ Inputs:
|
|||||||
- windowed_journal_path: /tmp/adam-windowed-journal.jsonl
|
- windowed_journal_path: /tmp/adam-windowed-journal.jsonl
|
||||||
- scores_path: /tmp/adam-scores.json
|
- scores_path: /tmp/adam-scores.json
|
||||||
- ab_regressions_path: /tmp/adam-ab-regressions.json
|
- ab_regressions_path: /tmp/adam-ab-regressions.json
|
||||||
|
- skill_utility_path: /tmp/adam-skill-utility.json # per-skill utility Δ — drives the utility-lift rubric penalty
|
||||||
- state_path: ~/.claude/adam/state.json
|
- state_path: ~/.claude/adam/state.json
|
||||||
- usage_path: ~/.claude/adam/usage.json
|
- usage_path: ~/.claude/adam/usage.json
|
||||||
- proposals_dir: ~/.claude/adam/proposals/
|
- proposals_dir: ~/.claude/adam/proposals/
|
||||||
@@ -159,14 +118,14 @@ Wait for return.
|
|||||||
|
|
||||||
### 2b. Persist and render the clustering trace
|
### 2b. Persist and render the clustering trace
|
||||||
|
|
||||||
The analyst's final message always contains a fenced ` ```trace ` block (per `agents/adam.md` §"Clustering trace (always emit)") immediately before its punch-list JSON line.
|
The analyst's final message always contains a fenced ` ```trace ` block (`agents/adam.md` §"Clustering trace (always emit)") immediately before its punch-list JSON line.
|
||||||
|
|
||||||
1. Extract the trace block. If it is missing, print a one-line warning to the user (`adam: trace block missing from agent output — proceeding without observability`) and continue; do not block on this.
|
1. Extract the trace block. If it is missing, print `adam: trace block missing from agent output — proceeding without observability` and continue. Do not block on this.
|
||||||
2. ALWAYS write the trace verbatim (without the surrounding fences) to `~/.claude/adam/last-trace.txt` (overwrite each run). This persists for retrospection via `node ~/.claude/adam/scripts/adam-explain.mjs`.
|
2. ALWAYS write the trace verbatim (without the fences) to `~/.claude/adam/last-trace.txt`, overwriting each run.
|
||||||
3. Extract the `SUMMARY:` line from the trace. ALWAYS display it as a one-line status to the user BEFORE the proposals are listed, e.g. `clustering: <SUMMARY line>`. This single-line status is shown in both `--explain` and default modes.
|
3. ALWAYS show the trace's `SUMMARY:` line as a one-line status BEFORE the proposals are listed, e.g. `clustering: <SUMMARY line>`, in both modes.
|
||||||
4. If `explain=true` (from §0): ALSO render the full trace block back to the user as a fenced code block (` ```text ` … ` ``` `) under a header `Clustering trace:`. If `explain=false`: SUPPRESS the cluster-line body from the user-visible output (the SUMMARY line is already shown in step 3).
|
4. If `explain=true`: ALSO render the full trace as a fenced ` ```text ` block under the header `Clustering trace:`. If `explain=false`: SUPPRESS the cluster-line body (step 3 already showed the SUMMARY line).
|
||||||
|
|
||||||
The user can re-render any past trace at any time via:
|
The user can re-render any past trace:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node ~/.claude/adam/scripts/adam-explain.mjs --mode summary # SUMMARY + per-decision counts
|
node ~/.claude/adam/scripts/adam-explain.mjs --mode summary # SUMMARY + per-decision counts
|
||||||
@@ -176,71 +135,24 @@ node ~/.claude/adam/scripts/adam-explain.mjs --mode json # machine-readabl
|
|||||||
|
|
||||||
### 3. Pre-apply verification gate (MOSS §3.4)
|
### 3. Pre-apply verification gate (MOSS §3.4)
|
||||||
|
|
||||||
MOSS §3.4: "Verification must therefore be runtime, on a production-equivalent environment, and against the same prompts that produced the failure evidence." Before auto-applying, verify each proposal deterministically:
|
MOSS §3.4: "Verification must therefore be runtime, on a production-equivalent environment, and against the same prompts that produced the failure evidence."
|
||||||
|
|
||||||
For each id in `high_confidence`:
|
For each id in `high_confidence`, read `~/.claude/adam/proposals/<id>-*.md` and run these checks. All four must pass for auto-apply. Any failure demotes the id to `queued` for manual review.
|
||||||
- Read the proposal file from `~/.claude/adam/proposals/<id>-*.md`.
|
|
||||||
- **Verification checks** (all must pass for auto-apply to proceed):
|
1. **Source entries exist**: every timestamp in `source_entries` frontmatter appears in `/tmp/adam-windowed-journal.jsonl`. A missing one means the evidence is stale or already actioned.
|
||||||
1. **Source entries exist**: every timestamp in `source_entries` frontmatter must appear in `/tmp/adam-windowed-journal.jsonl`. If any are missing, the evidence is stale or was already actioned — demote to `queued`.
|
2. **Diagnosis grounded**: the `# Diagnosis` section has all four fields (Trigger, Action, Mismatch, Outcome) with ≥1 backtick-wrapped quote.
|
||||||
2. **Diagnosis grounded**: the `# Diagnosis` section must have all four fields (Trigger, Action, Mismatch, Outcome) with ≥1 backtick-wrapped quote. If malformed, demote to `queued`.
|
3. **Type-evidence match**: the proposal `type` matches what the evidence supports:
|
||||||
3. **Type-evidence match**: the proposal `type` must match what the evidence supports:
|
|
||||||
- `correction` signals → `memory`, `skill_new`, `skill_edit` (not `nudge`)
|
- `correction` signals → `memory`, `skill_new`, `skill_edit` (not `nudge`)
|
||||||
- `dead_end` signals → `nudge`, `skill_new`, `skill_edit` (not `memory`)
|
- `dead_end` signals → `nudge`, `skill_new`, `skill_edit` (not `memory`)
|
||||||
- `tool_error_loop` signals → `memory`, `skill_new`, `skill_edit`
|
- `tool_error_loop` signals → `memory`, `skill_new`, `skill_edit`
|
||||||
- `harness_edit` → must cite harness-level evidence (false negative, scoring bias, window miscalibration)
|
- `harness_edit` → must cite harness-level evidence (false negative, scoring bias, window miscalibration)
|
||||||
If mismatch, demote to `queued`.
|
4. **No conflicting applied proposal**: grep `~/.claude/adam/applied/` for a proposal with the same `target` applied in the last 7 days. This prevents stacking rapid edits.
|
||||||
4. **No conflicting applied proposal**: grep `~/.claude/adam/applied/` for any proposal with the same `target` applied in the last 7 days. If found, demote to `queued` (prevents stacking rapid edits).
|
|
||||||
- Print verification result: `verified: <id> (4/4 checks passed)` or `demoted: <id> (failed: <check_name>)`.
|
Print `verified: <id> (4/4 checks passed)` or `demoted: <id> (failed: <check_name>)`.
|
||||||
- Demoted proposals are moved from `high_confidence` to `queued` for manual review.
|
|
||||||
|
|
||||||
### 3a. Apply verified high-confidence items
|
### 3a. Apply verified high-confidence items
|
||||||
|
|
||||||
For each id that passed verification:
|
Read [apply-by-type.md](apply-by-type.md). Enforce its Karpathy constraints, then follow its §3a sections for each verified id: the per-type apply, then move, A/B tracking append and archive. Print `auto-applied N proposals: [ids]`.
|
||||||
- Print `id`, `target`, `confidence`, `blast_radius`, `cross_session_evidence`, `auto_apply_eligible`.
|
|
||||||
- Apply the change:
|
|
||||||
- **For `skill_new`**: `mkdir -p ~/.claude/skills/<slug>/`, then `Write` the proposal's `# Proposed change` body to `~/.claude/skills/<slug>/SKILL.md`. After write, print: "skill `<slug>` written to `~/.claude/skills/<slug>/SKILL.md` — activates immediately — Claude Code v2.1.0+ auto-hot-reloads user-level skills, no restart needed."
|
|
||||||
- **For `memory`**: `Write` the proposal's `# Proposed change` body (which MUST include the auto-memory frontmatter — see "Memory drafting protocol" in `agents/adam.md`) to the path in `target`. Then update `MEMORY.md` index with a one-line pointer.
|
|
||||||
- **For `nudge`**: low-blast auto-apply path. Single-session evidence is sufficient — skip the cross-session gate. Append a new entry to `~/.claude/adam/active-nudges.json` (create the file with `[]` if absent) with shape `{kind, message, created_at: <now_ms>, expires_at_ts: <now_ms + 7*86400000>, max_displays: 3, displays_used: 0, source_session: <session_id from proposal>}`. Do NOT modify any skill, memory, agent, or CLAUDE.md. Tell user: "nudge queued — surfaces on next SessionStart in a different session (expires in 7 days)."
|
|
||||||
- **For `reinforcement`**: gated by `confidence >= 4 AND blast_radius == low` (same as memory). Apply by invoking the helper:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node ~/.claude/adam/scripts/adam-apply-reinforcement.mjs ~/.claude/adam/proposals/<id>-*.md --home ~/.claude
|
|
||||||
```
|
|
||||||
|
|
||||||
The helper reads the proposal frontmatter (`skill_slug`, `count`, `source_session`) and appends one JSON line to `~/.claude/adam/reinforcements.jsonl`. No code/memory/skill modifications. Output: `{"status":"applied"|"gated", ...}` — on `gated` leave proposal in `proposals/` (helper failed its own re-check), on `applied` continue to the archive step. Tell user: "reinforcement logged for `<skill_slug>` (count=<N>) — appended to reinforcements.jsonl."
|
|
||||||
- **For `skill_edit`**: enforce the apply-time gate before writing.
|
|
||||||
1. Verify proposal frontmatter has `auto_apply_eligible: true`. If not, abort and queue for review.
|
|
||||||
2. Read `target` SKILL.md, capture `current_bytes` from a fresh stat — do NOT trust frontmatter `bytes_before`.
|
|
||||||
3. Verify diff in `# Proposed change`:
|
|
||||||
- Unified-diff format.
|
|
||||||
- Zero `-` lines on existing SKILL.md content (additions only).
|
|
||||||
- Total `+` lines ≤ 30.
|
|
||||||
If any check fails, print one-line refusal reason, leave proposal in `proposals/`, continue.
|
|
||||||
4. Cooldown re-check: run `node ~/.claude/adam/scripts/adam-cooldown.mjs --skill <target_skill> --fingerprint <proposal_fingerprint>` (both fields come from proposal frontmatter; missing fingerprint → "legacy"). Refuse if the script returns `status: cooldown` OR `status: blacklisted`. This per-(skill, fingerprint) gate replaces the previous coarse per-skill scan — proposals for the same skill with a different fingerprint are NOT blocked by an older entry.
|
|
||||||
5. (covered by step 4 — blacklisted status is returned by `adam-cooldown.mjs` when `auto_apply_blacklist: true` is found in `rejected/` within 30 days for the same (skill, fingerprint))
|
|
||||||
6. Apply via `Edit` tool (append the new section per the diff). Never use `Write` on existing SKILL.md.
|
|
||||||
7. Re-stat target. If new size exceeds `2 * current_bytes` (captured in step 2), revert via `Edit` (remove the just-appended section) and refuse — print refusal reason.
|
|
||||||
8. Add `last_auto_edit: <iso8601 utc now>` to the proposal frontmatter before moving it.
|
|
||||||
9. Tell user: "skill `<slug>` extended (added <N> lines) — auto-applied via win-evidence gate."
|
|
||||||
- Move proposal to `~/.claude/adam/applied/<UTC-ts>-<id>.md`.
|
|
||||||
- **A/B tracking append** (skip for `reinforcement` — positive-only ledger, intentionally not A/B-tracked per `agents/adam.md` §"`reinforcement` proposals"): as a separate atomic step right after the move, append one JSON line to `~/.claude/adam/ab-tracking.jsonl` (create with empty contents if absent). Read fields from the proposal's frontmatter (`proposal_fingerprint`, `originating_signals` — both populated per `agents/adam.md`; `originating_signals` is a list of `{type, count, session_ids}` objects). Schema:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"applied_at": <unix_ms now>,
|
|
||||||
"proposal_id": "<id>",
|
|
||||||
"proposal_type": "skill_edit|skill_new|memory|nudge",
|
|
||||||
"target_skill": "<slug or target basename>",
|
|
||||||
"proposal_fingerprint": "<hash>",
|
|
||||||
"originating_signals": [{"type":"<signal>","count":<N>,"session_ids":[...]}],
|
|
||||||
"pre_window_days": 7
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This entry is consumed by `adam-ab-measure.mjs` on subsequent `/reflect` runs to compute pre/post signal-count deltas. See `agents/adam.md` §"A/B effectiveness". If the append fails (disk-full etc.) log a warning but do NOT abort the apply path — A/B is observability, not a gate.
|
|
||||||
- **Archive consumed journal entries**: `node ~/.claude/adam/scripts/adam-archive.mjs ~/.claude/adam/applied/<UTC-ts>-<id>.md` — moves entries listed in proposal's `source_entries` from `journal.jsonl` to `journal/actioned-<id>.jsonl` so subsequent `/reflect` runs do not re-cluster them.
|
|
||||||
|
|
||||||
Print: `auto-applied N proposals: [ids]`.
|
|
||||||
|
|
||||||
### 4. Walk the queue
|
### 4. Walk the queue
|
||||||
|
|
||||||
@@ -248,27 +160,13 @@ For each id in `queued`:
|
|||||||
|
|
||||||
a. Read and display the proposal in full (frontmatter + body).
|
a. Read and display the proposal in full (frontmatter + body).
|
||||||
b. Ask the user: **approve** / **reject** / **edit**.
|
b. Ask the user: **approve** / **reject** / **edit**.
|
||||||
c. On **approve**:
|
c. On **approve**: enforce the Karpathy constraints, apply per the apply-by-type.md §4 approve section, move the proposal to `~/.claude/adam/applied/<ts>-<id>.md`, and archive it.
|
||||||
- For `claude_md_edit`: backup `cp ~/.claude/CLAUDE.md ~/.claude/adam/applied/<ts>-claude-md-backup.md` first.
|
d. On **reject**: follow apply-by-type.md §4 reject (reason, `auto_apply_blacklist` for `skill_edit`, move to `rejected/`, archive).
|
||||||
- For `deletion`: `mkdir -p ~/.claude/adam/trash/<ts>` then `mv` the artifact into it. Print restoration command.
|
e. On **edit**: ask the user for the change, edit the proposal in place, then loop back to step a for that same id.
|
||||||
- For `skill_new`: `mkdir -p ~/.claude/skills/<slug>/`, then write `# Proposed change` body to `<slug>/SKILL.md`. Tell user: "skill `<slug>` written — activates immediately (CC v2.1.0+ auto-hot-reload)."
|
|
||||||
- For `skill_edit`: apply the unified diff in `# Proposed change` to the existing SKILL.md at `target` (append-only — never replace existing content).
|
|
||||||
- For `memory`: write `# Proposed change` body (must include auto-memory frontmatter) to `target` and update `MEMORY.md` index with a one-line pointer.
|
|
||||||
- For `harness_edit` (MOSS §1): apply the unified diff to the target harness file. **Before applying**:
|
|
||||||
1. Run `bash ~/.claude/adam/tests/run-tests.sh` — capture pass count.
|
|
||||||
2. Apply the diff via `Edit`.
|
|
||||||
3. Run `bash ~/.claude/adam/tests/run-tests.sh` again — verify pass count is equal or higher and 0 failures.
|
|
||||||
4. If test regression: revert the edit, print "harness_edit reverted — test regression detected", leave proposal in `proposals/`.
|
|
||||||
5. If tests pass: tell user "harness edit applied to `<target>` — tests pass (<N> passed)."
|
|
||||||
- For all others: apply via Write/Edit per the proposal's `# Proposed change`.
|
|
||||||
- Move proposal to `~/.claude/adam/applied/<ts>-<id>.md`.
|
|
||||||
- Archive: `node ~/.claude/adam/scripts/adam-archive.mjs ~/.claude/adam/applied/<ts>-<id>.md`.
|
|
||||||
d. On **reject**: ask for reason in one line. Append `# Reason\n<reason>` to proposal body. If the proposal `type` is `skill_edit`, ALSO add `auto_apply_blacklist: true` to its frontmatter (so future reflects skip auto-apply on this target for 30 days). Move to `~/.claude/adam/rejected/<id>.md`. Archive: `node ~/.claude/adam/scripts/adam-archive.mjs ~/.claude/adam/rejected/<id>.md`.
|
|
||||||
e. On **edit**: ask the user for the change, edit the proposal in place, then loop back to step 4a for that same id.
|
|
||||||
|
|
||||||
### 5. Handle failures
|
### 5. Handle failures
|
||||||
|
|
||||||
If apply fails (file write error, target missing): leave proposal in `proposals/`, append `# Apply error\n<error>` to its body. Tell the user. Do not move it.
|
If apply fails (file write error, target missing): leave the proposal in `proposals/`, append `# Apply error\n<error>` to its body, and tell the user. Do not move it.
|
||||||
|
|
||||||
### 6. Summary
|
### 6. Summary
|
||||||
|
|
||||||
@@ -288,35 +186,16 @@ adam reflect summary:
|
|||||||
failed: <N>
|
failed: <N>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Keypoint history**: after all proposals are processed, append one JSON line to `~/.claude/adam/keypoint-history.jsonl` with the aggregate keypoint scores from the diagnose stage:
|
**Keypoint history**: after all proposals are processed, append one JSON line to `~/.claude/adam/keypoint-history.jsonl` with the aggregate keypoint scores from the diagnose stage. This is the longitudinal record of which capabilities improve across `/reflect` runs.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"ts":"<iso>","session":"<session_id>","keypoints":{"tool_selection":N,"scope_discipline":N,"error_recovery":N,"first_attempt":N,"build_reliability":N},"proposals_emitted":N,"proposals_applied":N}
|
{"ts":"<iso>","session":"<session_id>","keypoints":{"tool_selection":N,"scope_discipline":N,"error_recovery":N,"first_attempt":N,"build_reliability":N},"proposals_emitted":N,"proposals_applied":N}
|
||||||
```
|
```
|
||||||
|
|
||||||
This builds a longitudinal record of which capabilities are improving across `/reflect` runs.
|
|
||||||
|
|
||||||
## Karpathy constraints (you must enforce on each apply)
|
|
||||||
|
|
||||||
Before writing any proposal:
|
|
||||||
- Confirm `# Assumptions` section is non-empty.
|
|
||||||
- Confirm `# Diagnosis` section exists and contains all four labelled lines (`Trigger:`, `Action:`, `Mismatch:`, `Outcome:`) AND at least one backtick-wrapped quote ≤80 chars in the Outcome line. Refuse if missing or malformed — agent must redraft per the "Diagnosis drafting protocol" in `agents/adam.md`.
|
|
||||||
- Confirm `# Success criterion` section is non-empty and runnable.
|
|
||||||
- Confirm change is ≤50 LOC for non-`skill_new`, or ≤80 LOC for `skill_new` body. If larger, ask the user once: "this proposal is N LOC — proceed?"
|
|
||||||
- For `claude_md_edit`: confirm 3+ distinct cwds in the `# Why` section.
|
|
||||||
- For `deletion`: confirm both criteria (a) and (b) from the agent's special handling are documented in the proposal.
|
|
||||||
- For `skill_new`: confirm the slug doesn't collide with any existing skill in `~/.claude/skills/`. If it does, refuse and ask user to rename.
|
|
||||||
- For `skill_edit`: confirm the diff is append-only (no `-` lines that remove existing content) and that target SKILL.md exists. When auto-applying, ALSO re-verify the eligibility gate steps in §3 (cooldown, blacklist, byte cap) before any `Edit` call — never trust frontmatter alone.
|
|
||||||
- For `skill_edit` with `auto_apply_eligible: true`: confirm `contradiction_flag` is absent or null in frontmatter. Refuse auto-apply if `contradiction_flag` is set with any non-empty value (treat the agent's flag as a hard veto on auto-apply; user can still manually approve in walk-the-queue if they disagree with the heuristic).
|
|
||||||
- For `memory`: confirm `# Proposed change` body starts with `---` frontmatter matching the live auto-memory schema — top-level `name` (the slug) + `description`, plus a `metadata:` block with `node_type: memory`, `type`, and `originSessionId`. Cross-check the shape against an existing file in the target memory dir. Refuse if frontmatter is flat (`type:`/`originSessionId:` at top level) or missing the `metadata:` block — agent must redraft per the Memory drafting protocol.
|
|
||||||
- For `harness_edit`: confirm `auto_apply_eligible: false` (never auto-apply). Confirm `confidence ≥ 5`. Confirm `# Test verification` section names the test command. Confirm diff is ≤30 LOC and targets a single allowed harness file (see `agents/adam.md` §"Harness self-modification"). Run test suite before AND after applying — revert on any regression.
|
|
||||||
- Confirm `source_entries` is present in proposal frontmatter as a non-empty list (used for archive). Warn (do not refuse) if missing — legacy proposals from before v0.2.0 won't have it.
|
|
||||||
|
|
||||||
If any check fails, refuse to apply and ask the user how to proceed.
|
|
||||||
|
|
||||||
## Things you MUST NOT do
|
## Things you MUST NOT do
|
||||||
|
|
||||||
- Do not auto-apply anything not in `high_confidence`.
|
- Do not auto-apply anything not in `high_confidence`.
|
||||||
|
- Do not apply any proposal before you read apply-by-type.md and enforce its Karpathy constraints.
|
||||||
- Do not invoke other skills during a `/reflect` run.
|
- Do not invoke other skills during a `/reflect` run.
|
||||||
- Do not modify `settings.json` without explicit user yes.
|
- Do not modify `settings.json` without explicit user yes.
|
||||||
- Do not hard-delete anything. Use `mv` to `~/.claude/adam/trash/<ts>/`.
|
- Do not hard-delete anything. Use `mv` to `~/.claude/adam/trash/<ts>/`.
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Apply procedures by proposal type
|
||||||
|
|
||||||
|
Read this file before any apply in SKILL.md §3a (auto-apply) or §4 (walk the queue).
|
||||||
|
Every check here is mandatory. Section numbers (§N) refer to SKILL.md.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- Karpathy constraints (every apply)
|
||||||
|
- §3a auto-apply, per type
|
||||||
|
- §3a after each auto-apply: move, A/B tracking, archive
|
||||||
|
- §4 approve, per type
|
||||||
|
- §4 reject and edit
|
||||||
|
|
||||||
|
## Karpathy constraints (you must enforce on each apply)
|
||||||
|
|
||||||
|
Before writing any proposal:
|
||||||
|
|
||||||
|
- Confirm `# Assumptions` section is non-empty.
|
||||||
|
- Confirm `# Diagnosis` section exists and contains all four labelled lines (`Trigger:`, `Action:`, `Mismatch:`, `Outcome:`) AND at least one backtick-wrapped quote ≤80 chars in the Outcome line. Refuse if missing or malformed — agent must redraft per the "Diagnosis drafting protocol" in `agents/adam.md`.
|
||||||
|
- Confirm `# Success criterion` section is non-empty and runnable.
|
||||||
|
- Confirm change is ≤50 LOC for non-`skill_new`, or ≤80 LOC for `skill_new` body. If larger, ask the user once: "this proposal is N LOC — proceed?"
|
||||||
|
- For `claude_md_edit`: confirm 3+ distinct cwds in the `# Why` section.
|
||||||
|
- For `deletion`: confirm both criteria (a) and (b) from the agent's special handling are documented in the proposal.
|
||||||
|
- For `skill_new`: confirm the slug doesn't collide with any existing skill in `~/.claude/skills/`. If it does, refuse and ask user to rename.
|
||||||
|
- For `skill_edit`: confirm the diff is append-only (no `-` lines that remove existing content) and that target SKILL.md exists. When auto-applying, ALSO re-verify the eligibility gate steps in §3 (cooldown, blacklist, byte cap) before any `Edit` call — never trust frontmatter alone.
|
||||||
|
- For `skill_edit` with `auto_apply_eligible: true`: confirm `contradiction_flag` is absent or null in frontmatter. Refuse auto-apply if `contradiction_flag` is set with any non-empty value (treat the agent's flag as a hard veto on auto-apply; user can still manually approve in walk-the-queue if they disagree with the heuristic).
|
||||||
|
- For `memory`: confirm `# Proposed change` body starts with `---` frontmatter matching the live auto-memory schema — top-level `name` (the slug) + `description`, plus a `metadata:` block with `node_type: memory`, `type`, and `originSessionId`. Cross-check the shape against an existing file in the target memory dir. Refuse if frontmatter is flat (`type:`/`originSessionId:` at top level) or missing the `metadata:` block — agent must redraft per the Memory drafting protocol.
|
||||||
|
- For `harness_edit`: confirm `auto_apply_eligible: false` (never auto-apply). Confirm `confidence ≥ 5`. Confirm `# Test verification` section names the test command. Confirm diff is ≤30 LOC and targets a single allowed harness file (see `agents/adam.md` §"Harness self-modification"). Run test suite before AND after applying — revert on any regression.
|
||||||
|
- Confirm `source_entries` is present in proposal frontmatter as a non-empty list (used for archive). Warn (do not refuse) if missing — legacy proposals from before v0.2.0 won't have it.
|
||||||
|
|
||||||
|
If any check fails, refuse to apply and ask the user how to proceed.
|
||||||
|
|
||||||
|
## §3a auto-apply, per type
|
||||||
|
|
||||||
|
For each id that passed §3 verification, print `id`, `target`, `confidence`, `blast_radius`, `cross_session_evidence`, `auto_apply_eligible`. Then apply:
|
||||||
|
|
||||||
|
- **`skill_new`**: `mkdir -p ~/.claude/skills/<slug>/`, then `Write` the proposal's `# Proposed change` body to `~/.claude/skills/<slug>/SKILL.md`. After write, print: "skill `<slug>` written to `~/.claude/skills/<slug>/SKILL.md` — activates immediately — Claude Code v2.1.0+ auto-hot-reloads user-level skills, no restart needed."
|
||||||
|
- **`memory`**: `Write` the proposal's `# Proposed change` body (which MUST include the auto-memory frontmatter — see "Memory drafting protocol" in `agents/adam.md`) to the path in `target`. Then update `MEMORY.md` index with a one-line pointer.
|
||||||
|
- **`nudge`**: low-blast auto-apply path. Single-session evidence is sufficient — skip the cross-session gate. Apply by invoking the helper (do NOT hand-write the JSON — the helper builds the message and the `cue` deterministically from frontmatter):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ~/.claude/adam/scripts/adam-apply-nudge.mjs ~/.claude/adam/proposals/<id>-*.md --home ~/.claude
|
||||||
|
```
|
||||||
|
|
||||||
|
The proposal frontmatter MUST carry `type: nudge`, `source_session`, and `cwd` (the cluster's dominant `cwd`, from `adam-batch.mjs`'s per-batch `cwds`), plus `dead_end_count`. The helper appends one entry to `~/.claude/adam/active-nudges.json` (creating it if absent) with `cue: {cwd}` populated — so the reminder is query-conditioned: it surfaces at SessionStart *and* mid-session (via `adam-observe.mjs`) only when the user is working in that codebase. Output: `{"status":"applied"|"gated", ...}` — on `gated` leave the proposal in `proposals/`, on `applied` continue to archive. Do NOT modify any skill, memory, agent, or CLAUDE.md. Tell user: "nudge queued — surfaces when you return to `<cwd basename>` (expires in 7 days)."
|
||||||
|
- **`reinforcement`**: gated by `confidence >= 4 AND blast_radius == low` (same as memory). Apply by invoking the helper:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ~/.claude/adam/scripts/adam-apply-reinforcement.mjs ~/.claude/adam/proposals/<id>-*.md --home ~/.claude
|
||||||
|
```
|
||||||
|
|
||||||
|
The helper reads the proposal frontmatter (`skill_slug`, `count`, `source_session`) and appends one JSON line to `~/.claude/adam/reinforcements.jsonl`. No code/memory/skill modifications. Output: `{"status":"applied"|"gated", ...}`. On `gated`, leave the proposal in `proposals/` (the helper failed its own re-check). On `applied`, continue to the archive step. Tell user: "reinforcement logged for `<skill_slug>` (count=<N>) — appended to reinforcements.jsonl."
|
||||||
|
- **`skill_edit`**: enforce the apply-time gate before writing.
|
||||||
|
1. Verify proposal frontmatter has `auto_apply_eligible: true`. If not, abort and queue for review.
|
||||||
|
2. Read `target` SKILL.md, capture `current_bytes` from a fresh stat — do NOT trust frontmatter `bytes_before`.
|
||||||
|
3. Verify diff in `# Proposed change`: unified-diff format, zero `-` lines on existing SKILL.md content (additions only), total `+` lines ≤ 30. If any check fails, print one-line refusal reason, leave proposal in `proposals/`, continue.
|
||||||
|
4. Cooldown re-check: run `node ~/.claude/adam/scripts/adam-cooldown.mjs --skill <target_skill> --fingerprint <proposal_fingerprint>` (both fields come from proposal frontmatter; missing fingerprint → "legacy"). Refuse if the script returns `status: cooldown` OR `status: blacklisted`. This per-(skill, fingerprint) gate replaces the previous coarse per-skill scan — proposals for the same skill with a different fingerprint are NOT blocked by an older entry. `adam-cooldown.mjs` returns `blacklisted` when `auto_apply_blacklist: true` is found in `rejected/` within 30 days for the same (skill, fingerprint).
|
||||||
|
5. Apply via `Edit` tool (append the new section per the diff). Never use `Write` on existing SKILL.md.
|
||||||
|
6. Re-stat target. If new size exceeds `2 * current_bytes` (captured in step 2), revert via `Edit` (remove the just-appended section) and refuse — print refusal reason.
|
||||||
|
7. Add `last_auto_edit: <iso8601 utc now>` to the proposal frontmatter before moving it.
|
||||||
|
8. Tell user: "skill `<slug>` extended (added <N> lines) — auto-applied via win-evidence gate."
|
||||||
|
|
||||||
|
## §3a after each auto-apply: move, A/B tracking, archive
|
||||||
|
|
||||||
|
1. Move proposal to `~/.claude/adam/applied/<UTC-ts>-<id>.md`.
|
||||||
|
2. **A/B tracking append** (skip for `reinforcement` — positive-only ledger, intentionally not A/B-tracked per `agents/adam.md` §"`reinforcement` proposals"): as a separate atomic step right after the move, append one JSON line to `~/.claude/adam/ab-tracking.jsonl` (create with empty contents if absent). Read fields from the proposal's frontmatter (`proposal_fingerprint`, `originating_signals` — both populated per `agents/adam.md`; `originating_signals` is a list of `{type, count, session_ids}` objects). Schema:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"applied_at": <unix_ms now>,
|
||||||
|
"proposal_id": "<id>",
|
||||||
|
"proposal_type": "skill_edit|skill_new|memory|nudge",
|
||||||
|
"target_skill": "<slug or target basename>",
|
||||||
|
"proposal_fingerprint": "<hash>",
|
||||||
|
"originating_signals": [{"type":"<signal>","count":<N>,"session_ids":[...]}],
|
||||||
|
"pre_window_days": 7
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`adam-ab-measure.mjs` consumes this entry on later `/reflect` runs to compute pre/post signal-count deltas. See `agents/adam.md` §"A/B effectiveness". If the append fails (disk-full etc.), log a warning but do NOT abort the apply path — A/B is observability, not a gate.
|
||||||
|
3. **Archive consumed journal entries**: `node ~/.claude/adam/scripts/adam-archive.mjs ~/.claude/adam/applied/<UTC-ts>-<id>.md`. This moves the entries listed in the proposal's `source_entries` from `journal.jsonl` to `journal/actioned-<id>.jsonl`, so later `/reflect` runs do not re-cluster them.
|
||||||
|
|
||||||
|
After all ids, print: `auto-applied N proposals: [ids]`.
|
||||||
|
|
||||||
|
## §4 approve, per type
|
||||||
|
|
||||||
|
- **`claude_md_edit`**: back up first with `cp ~/.claude/CLAUDE.md ~/.claude/adam/applied/<ts>-claude-md-backup.md`.
|
||||||
|
- **`deletion`**: `mkdir -p ~/.claude/adam/trash/<ts>`, then `mv` the artifact into it. Print the restoration command.
|
||||||
|
- **`skill_new`**: `mkdir -p ~/.claude/skills/<slug>/`, then write the `# Proposed change` body to `<slug>/SKILL.md`. Tell user: "skill `<slug>` written — activates immediately (CC v2.1.0+ auto-hot-reload)."
|
||||||
|
- **`skill_edit`**: apply the unified diff in `# Proposed change` to the existing SKILL.md at `target` (append-only — never replace existing content).
|
||||||
|
- **`memory`**: write the `# Proposed change` body (must include auto-memory frontmatter) to `target` and update the `MEMORY.md` index with a one-line pointer.
|
||||||
|
- **`harness_edit`** (MOSS §1): apply the unified diff to the target harness file. **Before applying**:
|
||||||
|
1. Run `bash ~/.claude/adam/tests/run-tests.sh` — capture pass count.
|
||||||
|
2. Apply the diff via `Edit`.
|
||||||
|
3. Run `bash ~/.claude/adam/tests/run-tests.sh` again — verify pass count is equal or higher and 0 failures.
|
||||||
|
4. If test regression: revert the edit, print "harness_edit reverted — test regression detected", leave proposal in `proposals/`.
|
||||||
|
5. If tests pass: tell user "harness edit applied to `<target>` — tests pass (<N> passed)."
|
||||||
|
- **All other types**: apply via Write/Edit per the proposal's `# Proposed change`.
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
1. Move proposal to `~/.claude/adam/applied/<ts>-<id>.md`.
|
||||||
|
2. Archive: `node ~/.claude/adam/scripts/adam-archive.mjs ~/.claude/adam/applied/<ts>-<id>.md`.
|
||||||
|
|
||||||
|
## §4 reject and edit
|
||||||
|
|
||||||
|
- **Reject**: ask for the reason in one line. Append `# Reason\n<reason>` to the proposal body. If the proposal `type` is `skill_edit`, ALSO add `auto_apply_blacklist: true` to its frontmatter (so future reflects skip auto-apply on this target for 30 days). Move to `~/.claude/adam/rejected/<id>.md`. Archive: `node ~/.claude/adam/scripts/adam-archive.mjs ~/.claude/adam/rejected/<id>.md`.
|
||||||
|
- **Edit**: ask the user for the change, edit the proposal in place, then loop back to §4 step a for that same id.
|
||||||
Reference in New Issue
Block a user