Skip to content

refactor(.agent): host-agent review protocol + structural fixes - #3

Merged
codejunkie99 merged 10 commits into
masterfrom
refactor/host-agent-review-protocol
Apr 16, 2026
Merged

refactor(.agent): host-agent review protocol + structural fixes#3
codejunkie99 merged 10 commits into
masterfrom
refactor/host-agent-review-protocol

Conversation

@codejunkie99

Copy link
Copy Markdown
Owner

Summary

  • Shifts the reasoning boundary: Python handles mechanical filing (cluster, extract, stage, prefilter, decay, archive); the host agent (Claude Code, Codex, Windsurf, ...) handles validation via CLI tools using the LLM it already has. The brain no longer needs its own ANTHROPIC_API_KEY.
  • Three verifiable bug fixes in existing code: batch-unsoundness in promotion, off-by-one in the rewrite-flag trigger, and an ornamental contradicts field on episodic writes.
  • Adds a durable candidate lifecycle + decision log, structured lessons.jsonl as the source of truth (LESSONS.md rendered from it), and a host-agent CLI (list_candidates, graduate, reject, reopen). Graduation requires --rationale so silent rubber-stamping is structurally impossible.

What changes for the user

  • auto_dream.py on the Stop hook is now safe — it only stages candidates and writes a summary; no unattended reasoning, no git commits.
  • New memory/working/REVIEW_QUEUE.md surfaces the review backlog into every session.
  • Review workflow: python .agent/tools/list_candidates.pygraduate.py <id> --rationale "..." or reject.py <id> --reason "...".
  • Existing LESSONS.md content above the ## Auto-promoted entries will be appended below sentinel is preserved across renders — the template's seed lessons and preamble survive first graduation.

Codex findings addressed

  1. graduate_validated was batch-unsound (frozen existing snapshot) — function removed; tools/graduate.py handles one candidate against a fresh LESSONS.md read.
  2. auto_dream did unattended reasoning + git commits on the Stop hook — stripped to staging-only.
  3. Provider coupling in validate.py — LLM dependency removed; heuristic prefilter only.
  4. Fragile candidate state (overwrite-by-key, no history) — durable lifecycle + decision log in review_state.py, rejection_count surfaces churn.
  5. AGENTS.md / DECISIONS.md not loaded despite stated contract — context_budget.build_context now loads them + REVIEW_QUEUE.md.
  6. Off-by-one rewrite trigger in hooks/on_failure.py:52 — fires on Nth failure instead of (N+1)th.
  7. Anecdote-as-claim extraction — unchanged here; mitigated by the required host-agent review step. Upgrading extraction (true LLM pattern synthesis or TF-IDF centroid) is Phase 4 work.

Files (19 changed)

New (11): harness/hooks/_provenance.py, harness/llm.py, harness/text.py, memory/validate.py, memory/cluster.py, memory/review_state.py, memory/render_lessons.py, tools/list_candidates.py, tools/graduate.py, tools/reject.py, tools/reopen.py

Modified (8): AGENTS.md, harness/conductor.py, harness/context_budget.py, harness/hooks/post_execution.py, harness/hooks/on_failure.py, memory/auto_dream.py, memory/promote.py, tools/memory_reflect.py

Not pushed (stays per-user)

  • memory/personal/PREFERENCES.md (customizable template)
  • memory/working/WORKSPACE.md + REVIEW_QUEUE.md (session state)
  • memory/episodic/AGENT_LEARNINGS.jsonl (per-machine log)
  • memory/candidates/ (transient)
  • memory/semantic/lessons.jsonl (grows from user graduations)

Test plan

  • Lane A bug fixes verified: batch-soundness (2nd candidate sees 1st winner's claim in existing), off-by-one fires rewrite flag on 3rd failure with FAILED 3 TIMES, contradicts field absent from episodic entries.
  • Heuristic prefilter catches claim_too_short and exact_duplicate.
  • review_state lifecycle: stage → graduate (accepted + rationale recorded), reject (rejection_count++), reopen (history preserved including rejection + reopen decisions).
  • render_lessons marker preservation: hand-curated preamble + seed bullets survive re-renders; sentinel present exactly once; fresh install creates a minimal header + sentinel.
  • render_lessons idempotent across repeated calls.
  • All 4 CLI tools compile + respond to --help.
  • context_budget.build_context loads AGENTS.md + review queue content (~2k tokens with the test query).
  • auto_dream imports cleanly with new wiring; graduate_validated removed from promote.
  • Manual: run python .agent/memory/auto_dream.py against real episodic data and confirm staging + REVIEW_QUEUE.md output.
  • Manual: full review cycle on a real candidate with a rationale.

codejunkie99 added 10 commits April 17, 2026 02:32
Shifts the reasoning boundary: Python handles mechanical filing
(cluster, extract, stage, prefilter, decay, archive). The host agent
(Claude Code, Codex, Windsurf, ...) handles validation via CLI tools
using the LLM it already has. The brain no longer needs its own
ANTHROPIC_API_KEY.

Bug fixes
- promote.graduate_validated batch-unsoundness (fixed then the function
  was removed; replaced by tools/graduate.py which handles one candidate
  at a time against a fresh LESSONS.md read).
- hooks/on_failure.py: rewrite-flag off-by-one (was firing on 4th
  failure; now 3rd, as FAILURE_THRESHOLD=3 implies).
- hooks/post_execution.py + on_failure.py: ornamental `contradicts`
  field removed from episodic writes (never populated on raw events).

Restructure
- memory/auto_dream.py: staging-only. No validation, no graduation,
  no git commit. Safe to run on Stop hooks or cron.
- memory/validate.py: gutted to heuristic prefilter (length + exact
  duplicate). No LLM calls. No more Anthropic SDK coupling.
- memory/review_state.py (new): candidate lifecycle (staged /
  provisional / accepted / rejected / superseded) + append-only
  decision log. rejection_count + reopen history surface recurring
  churn instead of looking fresh.
- memory/render_lessons.py (new): semantic/lessons.jsonl is the
  source of truth; LESSONS.md is rendered. User content above the
  `## Auto-promoted entries will be appended below` sentinel is
  preserved across renders.
- memory/cluster.py (new): Jaccard content clustering + deterministic
  extractive pattern (canonical highest-salience episode as claim).
  Replaces action-prefix clustering.
- harness/context_budget.py: query-aware retrieval (salience x
  relevance). Loads AGENTS.md, DECISIONS.md, REVIEW_QUEUE.md per
  the stated read order (previously silently skipped).
- harness/llm.py (new): _call_model factored out of conductor.
  Used only by the standalone conductor path; memory/ no longer
  imports it.
- harness/text.py (new): shared STOPWORDS, word_set, jaccard.
- harness/hooks/_provenance.py (new): source metadata for
  episodic entries (confidence, source{skill, run_id, commit_sha},
  evidence_ids).
- tools/list_candidates.py, graduate.py, reject.py, reopen.py (new):
  host-agent CLI. Graduation requires --rationale so silent
  rubber-stamp is structurally impossible.

Host-agent review protocol
- auto_dream stages candidates + writes memory/working/REVIEW_QUEUE.md
- Host agent checks the queue at session start
- list_candidates.py to see pending (priority = cluster_size *
  canonical_salience * age_factor)
- graduate.py <id> --rationale "..." or reject.py <id> --reason "..."
- Review is batched so cross-candidate contradictions surface
- Heuristic prefilter catches obvious junk; subjective judgment is
  the host agent's, not the brain's

Addresses Codex review of the earlier draft
- Batch-unsound graduate_validated: removed; per-call graduate.py reads
  fresh LESSONS.md each time.
- Unattended reasoning + git commits on Stop hook: stripped to
  staging-only.
- Provider coupling in validate.py: removed.
- Fragile candidate state: durable lifecycle + decision log.
- AGENTS.md / DECISIONS.md not loaded despite stated contract: now
  loaded.
- Off-by-one rewrite trigger: fixed.
- Anecdote-as-claim extraction: unchanged; mitigated by host-agent
  review. (Upgrading extraction is Phase 4 work.)

Not pushed (stays per-user)
- memory/personal/PREFERENCES.md
- memory/working/WORKSPACE.md
- memory/working/REVIEW_QUEUE.md (regenerated by auto_dream)
- memory/episodic/AGENT_LEARNINGS.jsonl
- memory/candidates/
- memory/semantic/lessons.jsonl (grows from graduations)
- [P1] write_candidates now looks up prior state across all lifecycle
  subdirs (staged/rejected/graduated), not just the staged location.
  A rejected candidate re-detected by clustering is moved back to
  staged with rejection_count and decision log preserved; the stale
  rejected/<slug>.json is removed so the id never exists in two
  states at once. A graduated candidate is skipped entirely — the
  lesson already lives in lessons.jsonl.

- [P2] staged_at is preserved across re-staging. candidate_priority()
  and the "oldest staged" backlog signal in REVIEW_QUEUE.md both
  depend on staged_at representing the true first-staged time;
  overwriting it to now() made recurring candidates never age and
  stop surfacing as stale work.

- [P2] run_dream_cycle now refreshes REVIEW_QUEUE.md even when the
  episodic log is empty. build_context loads the queue into every
  host session, so a stale/missing file silently hides pending
  review items. The early-return path now writes the queue first
  and reports pending count in its log line.

Smoke tests verify:
- rejected → re-detected: rejection_count=1, staged_at preserved,
  decisions log shows [staged, rejected, staged], old rejected/
  copy cleared.
- graduated → re-detected: write_candidates returns 0 (skipped);
  graduated/ file intact.
- empty episodic with prior pending: REVIEW_QUEUE.md regenerated
  with correct Pending count + Oldest staged timestamp.
- [P1] render_lessons now migrates legacy auto-promoted bullets below
  the sentinel into lessons.jsonl on first run. Without this, an
  installation upgrading from the old markdown-only format would have
  its previously-promoted lessons silently erased the first time
  graduate.py (or any path that calls render_lessons) ran with an
  empty lessons.jsonl. Migrated entries land with status='legacy'.
  Idempotent: subsequent renders don't re-import the same bullet.

- [P2] extract_pattern now boosts canonical_salience by cluster_size
  via salience_score's existing recurrence multiplier (capped at 3).
  Previously canonical_salience was computed from a single episode
  and ignored recurrence, so a cluster of 2 entries with pain=7
  importance=7 scored 4.9 and failed the 7.0 promotion threshold.
  Now scores 9.8 — matching the old recurrence-aware behavior.
  Repetition is the learning signal the pipeline is designed around.

- [P2] _extract_lesson_lines now strips the `[PROVISIONAL]` prefix
  (and `~~strikethrough~~` markers for superseded lessons) before
  returning claim text for duplicate detection. Previously a
  candidate with claim "foo" would not match an existing
  "[PROVISIONAL] foo" line, so the same provisional lesson could
  be graduated repeatedly under new candidate ids.
- [P2] write_candidates now distinguishes accepted vs provisional in
  the graduated/ subdir. Fully accepted lessons stay terminal (the
  pattern will never be re-surfaced as a candidate), but provisional
  ones are probationary by design — if the same pattern recurs, the
  host agent should see it again for evidence accumulation or
  supersession. Previously any slug in graduated/ was skipped
  regardless of status, making the provisional workflow terminal.

- [P2] review_state.mark_graduated / mark_rejected / mark_reopened
  now call write_review_queue_summary on the standard working/
  path after every transition. build_context loads the queue into
  every host session; without this refresh, a manual graduate.py
  would leave the reviewed item showing as pending until the next
  dream cycle. Uses a convention-based default path derived from
  candidates_dir, so callers don't need to pass it explicitly.

- [P3] write_candidates now skips re-staging of rejected candidates
  whose last decision was by reviewer "heuristic_prefilter". Those
  rejections are deterministic (length / exact duplicate vs
  LESSONS.md), so re-staging them every dream cycle just to have
  the prefilter reject them again does nothing but churn
  rejection_count. Human rejections still re-surface — the
  recurrence IS the signal the reviewer wants to see.
The round-2 "strip [PROVISIONAL] prefix before duplicate check" fix
was defensive against a different-slug-same-claim risk. Round 4
pointed out the root cause: slugs were md5(name), and name depends
on cluster membership, so the same pattern could get a new slug
when one supporting episode joined or left. That's the actual bug
to fix; once ids are stable, the marker-stripping hack becomes
harmful (it blocks provisional lessons from ever re-entering review).

- [P2] extract_pattern now returns a stable `id` derived from
  md5(normalize(claim)). Adding or removing a supporting episode
  no longer changes the id, so lifecycle history (rejection_count,
  graduated/provisional status, decision log) carries forward.
  promote._slug prefers pattern["id"]; falls back to md5(key) for
  safety on older data.

- [P1] _extract_lesson_lines now SKIPS provisional and superseded
  lessons in the duplicate check rather than stripping their
  markers. Provisional lessons are probationary by design: a
  recurring provisional pattern must reach the host agent so
  evidence can accumulate. Superseded ("~~...~~") lessons are
  historical, not current; a new claim matching one is a
  legitimate revival.

With stable ids, the round-2 scenario (same claim under a new
slug creating a duplicate provisional) is structurally impossible,
so the marker-strip hack is no longer needed.
- [P1] _extract_lesson_lines now reads the status= field from each
  rendered bullet's HTML annotation. Only status="accepted" counts
  as terminal; legacy (migrated from pre-restructure LESSONS.md)
  and provisional lessons can now recur through the duplicate gate
  for re-review or supersession. Visual-marker check is kept as a
  fallback for unannotated bullets.

- [P1] render_lessons now dedupes lessons.jsonl by id before
  rendering (keeps the latest entry per id). Without this, a
  provisional→accepted state transition would render both rows as
  separate bullets with the same claim. The jsonl stays
  append-only for audit; the rendered markdown shows the current
  state.

- [P2] write_candidates now cleans up the graduated/ copy when it
  re-stages a provisional lesson. Previously the same slug could
  live in both staged and graduated at once, making
  list_candidates --status graduated show stale data.
- [P1] Candidate re-stage gate now keys off actual change signals,
  not reviewer identity. mark_rejected and mark_graduated stamp the
  most recent decision with {evidence_snapshot, lessons_sha}.
  write_candidates compares the current cluster's evidence_ids +
  current LESSONS.md hash against the stamp and re-stages only if
  something material shifted. This fixes two failure modes:
  (a) human-rejected candidates no longer churn every dream cycle
  when the same evidence keeps producing the same cluster, and
  (b) heuristic-rejected candidates are no longer permanently
  suppressed — if the accepted lesson that triggered the duplicate
  rejection is later superseded or removed, the pattern comes back
  for review.

- [P2] context_budget._top_lessons now filters by status, so only
  accepted lessons reach the host agent's system prompt.
  Previously superseded (~~...~~), [PROVISIONAL], and legacy
  bullets could be injected as active guidance, letting the agent
  act on stale or probationary memory. Uses the same status=
  annotation parsing as validate._extract_lesson_lines (kept in
  sync by comment; duplication preferred over cross-layer import
  between harness/ and memory/).
- [P1] Pattern id now derives from normalized claim + conditions
  (sorted shared tokens). Claim-only id collided for generic
  canonical text ("the test failed" in db context vs api context
  would overwrite). Conditions usually stay stable across cluster
  membership changes, so lifecycle continuity is preserved in the
  common case while genuinely-different clusters get distinct ids.

- [P1] graduate.py is now atomic in the right direction: append to
  lessons.jsonl + render LESSONS.md FIRST, then move the candidate
  file to graduated/. Previously the move happened before the
  semantic writes; an interruption in the middle left the
  candidate terminal-filed but the lesson unlogged, which was a
  silent data-loss path. Append-only jsonl + dedupe-by-id at
  render time mean a retry after crash produces one rendered
  bullet, not two.

- [P2] Re-stage gate for heuristic-rejected candidates now
  compares the SPECIFIC lesson(s) that blocked them, not the
  whole-file LESSONS.md hash. heuristic_prefilter stamps
  duplicate_claims on the rejection decision; write_candidates
  checks whether any of those claims still exist as terminal
  lessons. Unrelated graduations no longer trigger re-staging +
  auto-rejection + rejection_count inflation. validate's
  _extract_lesson_lines is renamed extract_lesson_lines so
  promote can reuse the status-aware terminal filter.
- [P1] graduate._lesson_id now keys off candidate.id (claim+
  conditions, stable) instead of md5(claim). Two distinct
  candidates with the same canonical claim (e.g., "the test
  failed" in db vs api context) used to produce the same
  lesson_id; dedupe_by_id would silently drop the earlier
  accepted lesson from LESSONS.md.

- [P2] context_budget._top_lessons now returns "" when no
  accepted lessons exist, instead of falling back to raw
  markdown. The fallback leaked exactly the provisional/
  legacy/superseded content the status filter is designed to
  block — agent could act on stale memory right after
  migration or during pure-provisional states.

- [P2] content_cluster is now actually single-linkage. The old
  loop appended to the FIRST matching cluster but never merged
  other clusters the new entry also connected to, so input
  order [A, C, B] where A~B and B~C but A⊄C produced
  [[A,B], [C]] instead of one 3-item cluster. Recurrence counts
  and promotion thresholds were order-dependent as a result.
  Fix: find ALL clusters the new entry connects to, absorb
  them into one.
- [P2] Provisional supersessions no longer blank the active lesson.
  _build_auto_section now applies the superseded_by map only when
  the superseder is status="accepted". A provisional --supersedes
  was silently making the old accepted lesson render as
  ~~strikethrough~~ while the replacement was still probationary,
  so retrieval skipped both — the topic went dark.

- [P2] graduate.py --supersedes now excludes the superseded
  lesson from the exact-duplicate check. Replacing a lesson with
  structurally better content (updated conditions, fresh
  evidence, revised status) but the same claim wording is the
  whole point of supersession; the heuristic was blocking it with
  exact_duplicate_of_*.

- [P2] Re-stage gate now only counts NEW evidence (new_evidence
  minus prev_evidence), not set inequality. Decay archives old
  low-salience episodes every dream cycle; without this fix,
  routine evidence shrinkage would flip evidence_changed=True
  even when nothing new arrived, recreating churn for patterns
  whose only change was decay.
@codejunkie99
codejunkie99 merged commit af2b32f into master Apr 16, 2026
@codejunkie99
codejunkie99 deleted the refactor/host-agent-review-protocol branch April 16, 2026 23:06
codejunkie99 pushed a commit to hovhannest/agentic-stack that referenced this pull request Apr 23, 2026
Cross-model review (Claude + Codex adversarial) flagged 4 issues that
would either lose user data on Windows or silently degrade episodic
memory quality.

1. install.ps1:157-159 — DATA LOSS on Windows re-install.
   `Remove-Item -LiteralPath $skillsDst -Recurse -Force` on PowerShell
   5.1 (default Windows shell) traverses INTO a symlink target and
   deletes its contents before removing the link. A second
   `install.ps1 pi` run would wipe .agent/skills/. Detect ReparsePoint
   via Get-Item.Attributes BEFORE Remove-Item; use
   .NET Directory.Delete($path, false) on links so only the link is
   removed, never the target.

2. adapters/pi/memory-hook.ts — no subprocess timeout, hangs Pi.
   `await runHook(payload)` is awaited inside Pi's tool_result handler;
   a stuck Python child blocks Pi's event loop forever. Add 3s default
   timeout (overridable via $AGENT_HOOK_TIMEOUT_MS), kill the child on
   timeout, surface `timeout` as a separate result kind.

3. adapters/pi/memory-hook.ts — stderr was dropped, failures
   undiagnosable. Switch stdio to capture stderr (bounded to 4KB to
   avoid memory blowup on a wedged hook) and surface the first line
   in the failure notification.

4. .agent/harness/hooks/pi_post_tool.py — Pi sends tool_input with
   camelCase keys (filePath, oldString, newString), but the shared
   cc.* helpers (action_label, reflection, importance) expect Claude
   Code's snake_case keys. Without normalization, every Edit/Write
   logged by Pi degraded to "edit: ?" / "Edited ?" with empty detail.
   Add a Pi→canonical input key map applied in _normalize_input().

5. .agent/harness/hooks/pi_post_tool.py — fail-open on malformed
   payload was logging bogus "Unknown success" entries (Codex's High
   codejunkie99#1). If Pi ever changes the event shape or sends invalid JSON,
   episodic memory got polluted with noise instead of a real signal.
   Add a _emit_malformed() path that records an explicit
   `hook:malformed_payload` failure entry with a 200-char excerpt of
   the offending payload — visible in AGENT_LEARNINGS.jsonl as a real
   error, not noise.

Smoke-tested:
- empty payload → `hook:malformed_payload | empty payload`
- malformed JSON → `hook:malformed_payload | json decode error: ...`
- Pi camelCase Edit (filePath/oldString/newString) → produces correct
  `edit: /tmp/x.txt` action label and `Edited /tmp/x.txt: replaced 'a'
  with 'b'` reflection (was: `edit: ?` / `Edited ?`)
- well-formed bash success → unchanged behavior

Codex's concurrent-write concern (Codex codejunkie99#3) and the rsync-style sync
for .pi/skills (Codex codejunkie99#5) are NOT addressed here — they apply to
pre-existing infrastructure (post_execution.py write semantics, pi's
existing symlink path) and are scoped as separate follow-ups.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant