Praxis
Praxis is the system's behavioral self-improvement loop — it records real task outcomes, waits for patterns to emerge across multiple events, and consolidates validated lessons into a small capped set of active behavior shifts that influence every future run. The cap of 12 active shifts is a hard constraint that prevents unbounded rule accumulation, and every shift must trace back to recorded events so nothing changes without an auditable reason.
When to Use
- Recording outcomes from skill executions
- Extracting lessons from repeated patterns
- Reviewing or managing active behavior shifts
- Generating the current runtime brief (active shifts only)
- Producing a debrief explaining what changed and why
- Running scheduled cron ingest (praxis:journal_ingest) — use the production-proven pattern in
scripts/ingest_cron_YYYYMMDD.pyandreferences/ingest-script-pattern.md. After running the production script, runskills/ocas-praxis/scripts/gap_backfill.pyto catch journals the date filter missed (typically ~25% miss rate). Script path: Bothpraxis_ingest_run.pyandgap_backfill.pylive atskills/ocas-praxis/scripts/, NOT atcommons/data/ocas-praxis/scripts/. Always use the skill directory path. IMPORTANT: The production script has three known bugs (narrow date filter, full-history lesson reprocessing, eval ID format mismatch). The post-ingest checklist (gap backfill, noise lesson cleanup, state update, journal write, decay-risk scan) is MANDATORY — not optional. Seereferences/cron-execution-checklist.md. - Running shift cleanup/consolidation — use
scripts/shift_cleanup_YYYYMMDD.pypattern - Running lesson noise cleanup — use
scripts/lesson_cleanup_YYYYMMDD.pypattern - Running praxis review pass — use
skills/ocas-praxis/scripts/praxis_review.pyto review behavioral patterns over a time period (e.g.,--since-hours 24). Script path:praxis_review.pylives atskills/ocas-praxis/scripts/, NOT atcommons/data/ocas-praxis/scripts/. Always use the skill directory path. - Generating daily debrief — use
scripts/debrief_YYYYMMDD.pytemplate
When NOT to Use
- General knowledge storage — use memory tool
- Preference tracking — use Taste
- One-off trivia or domain facts
- Broad autobiographical summaries
- Silent personality mutation
Workflow
The praxis workflow operates as a continuous loop: Record → Extract → Consolidate → Apply → Debrief. This workflow exists because behavioral refinement requires systematic repetition, not ad-hoc adjustments.
- Record — Capture task outcomes as evidence records
- Extract — Identify micro-lessons from repeated patterns
- Consolidate — Merge lessons into active behavior shifts (capped)
- Apply — Apply shifts at runtime
- Debrief — Generate plain-language summary Example: a task repeatedly fails due to timeout → Praxis extracts "increase timeout for this endpoint" → consolidates into active shift → applies on future runs → debriefs the improvement.
Responsibility Boundary
Praxis owns bounded behavioral refinement: events, lessons, shifts, and debriefs. Error handling follows the recovery contract — see Recovery Behavior section below.
Praxis does not own: general memory (use memory tool), preference persistence (Taste), pattern discovery (Finch), communications (Dispatch), skill evaluation (Mentor).
Praxis reads journals from all skills to extract behavioral signals. Praxis decides whether to act on each signal found in any skill's journal output.
Ontology Types
- Concept/Event — recorded outcomes, task completions, failures, corrections, and behavioral signals
- Concept/Idea — extracted lessons, behavior shifts, and refinements
Praxis does not extract or emit Chronicle signals. Lessons remain isolated to the bounded refinement loop.
Commands
praxis.event.record— record a completed event or outcome with evidencepraxis.lesson.extract— derive micro-lessons from recorded eventspraxis.shift.propose— propose a new behavior shift from lessonspraxis.shift.list— list all shifts with statuspraxis.shift.activate— activate a proposed shift (enforces cap)praxis.shift.expire— expire or reject a shift with reasonpraxis.runtime.brief— generate runtime brief with active shifts onlypraxis.debrief.generate— produce a plain-language debriefpraxis.status— event count, active shifts, cap usage, last debriefpraxis.journal— write journal for the current run; called at end of every runpraxis.update— pull latest from GitHub source; journals and data preserved
Core Loop
- Record event → 2. Extract lessons (if pattern detected) → 3. Upgrade lessons — mandatory second pass to add causal grounding (what/why/when) and set
confidence: high→ 4. Dedup lessons against active shifts — before writing new lessons, check if an active shift already covers the same(signal_type, failure_phase)key; if yes, skip lesson creation (the shift already encodes it) → 5. Propose shift (check domain+phase overlap, handle mixed schemas) → 6. Activate (if cap allows) → 7. Generate debrief
Two-pass lesson extraction is mandatory. Pass 1 groups events by signal_type+phase and produces lesson stubs. Pass 2 adds full causal grounding (what/why/when) and upgrades confidence to high. Without Pass 2, no lessons can produce shifts. See references/ingest-script-pattern.md for the production-proven script.
Lesson extraction scope: NEW EVENTS ONLY. Pass 1 must group only events added in the current ingest run (or since the last lesson extraction), NOT the entire events.jsonl history. Re-processing all 2,500+ events every run causes: (a) stale lessons re-created for patterns that are no longer active, (b) unknown-domain lessons from legacy events that lack a skill field, (c) noise lessons (no_active_watches, system_memory_drop) that pass the ≥2 event threshold from historical accumulation. Track last_lesson_extraction_event_id in the ingest state and filter all_events to only events with event_id greater than that marker before grouping. See references/session_20260618_ingest_cron_d.md.
Ingest state file (ingest_state.json) — create if missing. The state file at {agent_root}/commons/data/ocas-praxis/ingest_state.json tracks last_lesson_extraction_event_id for scoped lesson extraction. If the file doesn't exist, create it with all required fields on first run (see references/inline-examples.md §Ingest state bootstrap for the canonical dict and references/support-file-map.md for the When-to-read signal). If the file exists but is missing fields (e.g., last_lesson_extraction_event_id), populate them from defaults before using. Confirmed 2026-06-25: state file had only last_ingest_run and last_dispatch_run, causing the scoping mechanism to be non-functional until fields were added.
Fixing last_lesson_extraction_event_id after sessions with no events: If the ingest state shows last_lesson_extraction_event_id: null or "" (empty string) but events.jsonl has entries, the scoping mechanism is broken — lesson extraction will re-process the full history every run, producing stale lessons. The empty string variant ("") is equally broken as null — both fail the event_id > marker comparison in the lesson extraction scope filter. PITFALL — empty string vs null: After any run that produces 0 events (all no_signal), the post-ingest script MUST explicitly set last_lesson_extraction_event_id to the last existing event in events.jsonl — NOT leave it as "". Fix by setting it to the last event's ID — see references/inline-examples.md §Repair last_lesson_extraction_event_id for the exact bash snippet and references/support-file-map.md for the When-to-read signal.
After 2026-06-21 dispatch (0 events from 4 mentor-light journals), the state should be set to the last existing event in events.jsonl (e.g., evt-20260621...).
Run Completion
After every Praxis command:
- Scan all skill journals at
{agent_root}/commons/journals/*/YYYY-MM-DD/for new journal entries (not injournals_evaluated.jsonl). Track consumedjournal_idvalues. - Persist events, lessons, shifts, and debriefs to local JSONL files
- Shift merge pass — Before checking cap, scan active shifts for semantic overlap. Merge overlapping shifts before proposing any new shift.
- Log material decisions to
decisions.jsonl - Write journal via
praxis.journal - Update
ingest_state.json— Updatelast_ingest_runto current timestamp, incrementjournals_processedby new journal count, setlast_ingest_events_added,last_ingest_journals_evaluated,last_evaluated_count(incremented),last_ingest_file_count,last_event_id(if events recorded), incrementtotal_ingests. The production script does NOT do this — it must be done by the caller.
Cron Execution Checklist
After running praxis_ingest_run.py in cron mode, the caller must complete these steps (the script does NOT update state, write journals, or do gap backfill):
Update
ingest_state.json— Setlast_ingest_runto current timestamp, incrementjournals_processedandtotal_ingests, setlast_ingest_events_added,last_ingest_journals_evaluated,last_inget_file_count, andnote.Gap journal backfill — Run
skills/ocas-praxis/scripts/gap_backfill.pyto scan for journals NOT injournals_evaluated.jsonlwith mtime >last_ingest_run. The script filters dispatch-wave meta-artifacts and phantom.jsonfiles automatically. This catches: (a) journals the date filter missed, (b) concurrent-cron collisions, (c) post-ingest gaps. ⚠️ Path: The script is atskills/ocas-praxis/scripts/gap_backfill.py, NOTcommons/data/ocas-praxis/scripts/gap_backfill.py.Update ingest_state.json with backfill count — Ingest_state.json: increment
journals_processedby the number of journals backfilled (as reported by gap_backfill.py output or viastate['eval_gaps_backfilled']).Noise lesson cleanup — Remove all lessons produced by Bug 2. The production script's lesson-scoping bug produces 13-15 noise lessons every run from stale historical patterns. Cleanup criteria (expanded 2026-06-28): Remove lessons where ANY of these are true: (a)
confidence: "low", (b)signal_typeis"?"/""/null, (c) ALL events from the current run areno_signal(making ALL co-produced lessons noise regardless of individual fields). Seereferences/recurring-noise-lesson-cleanup.mdfor the cleanup procedure.Write Praxis journal — Write to
{agent_root}/commons/journals/ocas-praxis/YYYY-MM-DD/praxis-cron-{timestamp}Z.jsonwithrun_type: "cron_ingest", metrics, andnot_activity_reasonexplaining the run.- Shell heredoc double-Z pitfall: When using shell heredoc, the timestamp shell variable already ends in
Z. Template${TS}Z.jsonproduces double-Z. Fix: Strip trailing Z:TS_SHORT="${TS%Z}"then use${TS_SHORT}Z.json, or fix with post-writemv.
- Shell heredoc double-Z pitfall: When using shell heredoc, the timestamp shell variable already ends in
Decay-risk scan — Check active shifts for those with
reinforcement_count == 0and age > 7 days. Flag in journal. 6a. Stale proposed-shift cleanup — After checking active shifts, scan forstatus: "proposed"shifts that have been in limbo ≥10 days without activation. These are typically artifacts from rebuilds or bulk proposals that never got activated. Expire them with reasondecay_check: proposed shift never activated after Nd. Use full file rewrite from canonical in-memory state (see Cap enforcement must use full file rewrite). Confirmed 2026-06-30: 15 proposed shifts from the 2026-06-18 rebuild sat idle for 11 days — none had >5 source events, many had 0. Seereferences/session-20260630-decay-check-stale-proposed.md.Stale script cleanup — If >10
.pyfiles exist in data root (outsidescripts/), remove them. Never delete fromscripts/subdirectory.Verify post-write (mandatory closure) — Before declaring the run done, validate the three artifacts the pipeline just wrote. Silent corruption here is invisible to gap backfill and only surfaces as a broken state file next run:
- State JSON parses —
json.load(open('ingest_state.json'))succeeds; confirmjournals_processed,total_ingests,last_ingest_run, andlast_lesson_extraction_event_idadvanced to expected values. A non-parsing state file means the load→modify→dump update failed silently. - Journal JSON valid — The new
praxis-cron-*.jsonparses;run_idmatches the filename and ends in a singleZ(no double-Z). A double-Z filename still works via gap backfill but is a known cosmetic bug (Bug 4) — fix withmvif caught here. Verification-script pitfall: when asserting single-Z programmatically, check therun_id(or the timestamp substring before.json), NOT the full filename —filename.endswith('Z')is ALWAYS False because the filename ends in.json. Userun_id.endswith('Z') and not run_id.endswith('ZZ')(or'ZZ' not in run_id). Confirmed 2026-07-13: a closure-verification assert onfilename.endswith('Z')falsely failed a valid single-Z journal (praxis-cron-20260713T043913Z.json); the journal was correct, the check was wrong. - lessons.jsonl byte count —
os.path.getsize(lessons.jsonl) == 0after cleanup. A non-zero size means cleanup did not truncate; re-runcleanup_noise_lessons.py. NOTE: findinglessons.jsonlNON-ZERO at the start of a future run is expected steady-state carryover (a prior cleanup truncation didn't persist, or the production script re-read historical lessons) — it is NOT an error; that run's cleanup step will re-archive and re-truncate. Do not treat start-of-run non-zero as a failure.
- State JSON parses —
See references/cron-execution-checklist.md for the production-proven script pattern with all steps including third-wave mitigation and noise cleanup. See references/session-20260627-cron-ingest-2032.md for the inline mtime-based alternative when the production script's bugs cause misses.
Gap journal backfill (mandatory post-run step): After running the production script, run skills/ocas-praxis/scripts/gap_backfill.py to catch journals the date filter missed. The script walks the profile journals directory, finds unevaluated journals with mtime > last_ingest_run, filters out dispatch-wave meta-artifacts and phantom .json files (empty filename from shell write bugs), classifies remaining journals, and appends them to the eval file. The script syncs the state counter to the actual eval file line count after backfill. See references/session-20260627-cron-ingest-1804.md for the production-proven gap backfill script.
Large one-time gap backfill (expected after eval file backlog): If the eval file has a significant backlog of unevaluated journals (e.g., from before Praxis was fully integrated, or from runs where eval writes failed), the first gap backfill after fixing the eval file can produce a large batch of backfill entries (5,000–10,000+). This is a one-time catchup, not a recurring pattern. After the initial catchup, subsequent runs should see near-zero gap journals. Log the backfill count in ingest_state.json:gap_journals_backfilled and the journal not_activity_reason for audit trail. Confirmed 2026-06-29: 5,817 gap journals backfilled in a single run (eval file grew from 42,357 to 48,176 entries).
Known Production Script Bugs (ACTIVE)
Four confirmed bugs remain in scripts/praxis_ingest_run.py and scripts/praxis_common.py as of 2026-06-30. The cron checklist workarounds prevent them from causing failures, but they waste compute and occasionally miss journals.
Bug 1: Date filter too narrow (praxis_ingest_run.py §Step 2)
Script only scans today/yesterday date directories (if today in cid or yesterday in cid). Journals in other date dirs are invisible. Workaround: Gap backfill step catches these post-run.
Bug 2: Lesson extraction processes full event history (praxis_ingest_run.py §Step 5)
Script loads ALL events from events.jsonl (3,300+) every run. last_lesson_extraction_event_id in state file is NOT used. Produces noise lessons from stale events. Impact: Low — dedup prevents duplicate lessons, but wastes compute. Operational note: When script output shows "NEW LESSONS" with high event counts (n=9, n=10, etc.) or events from dates before today, these are historical noise — not genuine new patterns. The post-ingest noise lesson cleanup (Step 5 of cron checklist) removes these. Do not propose shifts for confidence: low lessons.
Bug 2 cleanup scope expansion (2026-06-28, updated 2026-06-29): The cleanup step must remove ALL lessons produced by Bug 2, not just confidence: low ones. Bug 2's full-history reprocessing produces lessons with three identifying traits:
signal_typeis"?","",null, or missing entirely (key not present in lesson dict —.get("signal_type")returnsNone). This is the most dangerous variant becauseles.get("signal_type", "")returnsNone(not""), andNone != "?"passes the filter.confidence: "high"(Pass 2 grounding always upgrades to high — doesn't indicate genuine signal)- High event counts (n=9, n=11, n=18, n=51) from historical accumulation
Decision rule for cleanup: If ALL events recorded in the current run are no_signal (no genuine behavioral events), then ALL lessons produced in the same run are Bug 2 noise — remove them entirely. Do not rely on confidence or signal_type presence alone.
Bug 2 noise when exactly 1 genuine single-instance event is recorded (2026-07-07): The fast pre-filter (--all-no-signal) only fires when EVERY event is no_signal. When the run records exactly 1 genuine event (e.g., a single failure_keyword) alongside no_signal heartbeats, the pre-filter does NOT fire — and the per-lesson criteria will KEEP high-confidence lessons with real signal_type values (failure_keyword, escalation, execution_error) — but these are STILL Bug 2 full-history noise. Why: lesson extraction requires ≥2 events of a (signal_type, phase) group within the NEW-event scope; a single genuine new event cannot ground any lesson, so every lesson in lessons.jsonl came from Bug 2's full-history reprocessing (n-counts like n=53, n=20 are historical accumulation, not new patterns). Action: archive lessons.jsonl to commons/data/ocas-praxis/lessons_noise_archive_<UTC_DATE>.jsonl, then truncate lessons.jsonl to 0 lines. Sanity check: at steady state lessons.jsonl is 0 bytes before each run (prior cleanup removes all); if it was empty, clearing all extracted lessons is correct. Do NOT leave the high-confidence historical lessons in lessons.jsonl — they re-accumulate every run and are not new learnings. The durable behavioral store is shifts.jsonl, which persists across runs regardless of lessons.jsonl being emptied. The cleanup script now supports --new-genuine-events N (N = genuine non-no_signal events recorded this run); pass N<2 to auto-clear all lessons. As of 2026-07-07 both fast-paths (--all-no-signal and --new-genuine-events) and the per-lesson path archive removed lessons to lessons_noise_archive_<UTC_DATE>.jsonl automatically before clearing — the manual fallback below is only needed if the script is unavailable. Manual fallback if not using the flag: python3 -c "import json,datetime,os; p='commons/data/ocas-praxis/lessons.jsonl'; ls=[json.loads(l) for l in open(p) if l.strip()]; open('commons/data/ocas-praxis/lessons_noise_archive_'+datetime.date.today().isoformat()+'.jsonl','a').writelines(json.dumps(x)+'\n' for x in ls); open(p,'w').close()".
Fast pre-filter (dispatch + cron): Before iterating lessons individually, check the event stream: if every event recorded in the current run has signal_type matching no_signal/empty/null/?, skip per-lesson inspection entirely and clear all lessons produced in the same run. This is the most common steady-state outcome (confirmed 2026-06-30 dispatch: 5 events all no_signal → 13 lessons removed in one operation).
Critical filter fix (2026-06-29): The signal_type key may be entirely absent from Bug-2 noise lessons — not set to "?" but simply not present in the dict. Any cleanup filter MUST check all four conditions (see references/inline-examples.md §Bug 2 noise-lesson classifier for the exact is_bug2_noise_lesson function and references/support-file-map.md for the When-to-read signal). Do NOT use les.get("signal_type", "") == "?" alone — it misses the missing-key variant. Confirmed 2026-06-29: 13 Bug-2 lessons produced with NO signal_type key at all (Pass 2 grounding didn't add it when source events had no signal_type field), bypassed the existing == "?" filter.
Bug 3: Eval file ID format mismatch (praxis_common.py §dedup_eval_file)
Eval file stores IDs as skill/YYYY-MM-DD/filename.json (with .json), but legacy entries may lack the extension. The dedup normalizes to journal_id field but doesn't generate both forms for comparison. Impact: Occasional re-scanning of evaluated journals; gap backfill catches these.
Bug 4: Double-Z timestamp in journal filenames (praxis_ingest_run.py §journal output)
Journal filenames occasionally get double-Z suffixes (e.g., praxis-cron-20260630T092758ZZ.json). Root cause: timestamp composition applies .rstrip('Z') + 'Z' to a value already ending in Z. Impact: Cosmetic — journal is still written and discoverable by gap backfill. No data loss. Confirmed recurring: 2026-06-26, 2026-06-28, 2026-06-30. Fix: Check ts.endswith('Z') before appending Z in the journal output section.
- Dispatcher's
new_filesmay list phantom files — The dispatcher's file scan may capture files that are deleted or never materialize on disk by the time the dispatch runs. These appear indetails.new_filesbutos.path.exists()returns False. This is expected and must be handled silently.
Hard Constraints
- No autonomous identity rewriting
- No silent safety boundary changes
- No unlimited behavior rule accumulation
- Only active shifts influence runtime
- Maximum 12 active shifts (configurable)
- Every shift must trace to recorded events
- Every lesson must include causal grounding (the "why" — not just "what")
- Shifts without decay review expire automatically (configurable, default 14 days)
Capping and Consolidation
Default cap: 12 active shifts. When at cap and a new shift is proposed: merge overlapping shifts, replace a weaker shift, or reject the new shift.
Shift activation dedup and merge (mandatory before cap check):
- Domain+phase overlap — Does an active shift already target the same skill/domain AND failure phase? If yes, merge.
- Text similarity — If two shifts have nearly identical
shift_text, consolidate into a single cross-skill shift. - Only after merge — check if cap is exceeded. If still at cap, expire the oldest/lowest-reinforced-count shift.
Shift decay: Active shifts not reinforced in 14+ days auto-expire. Reinforcement extends half-life. Debriefs should flag shifts at 10+ days without reinforcement as "approaching decay" — on 2026-06-14, all 11 active shifts were 12-13 days old with 0 reinforcements, one day from mass expiry, but the debrief reported no action needed.
Elaborative interrogation: Lessons must capture WHAT happened, WHY, and WHEN. Format: [LESSON] What: <pattern>. Why: <cause>. When: <conditions>
Failure-phase tagging: Tag each event with the task phase (Planning, Execution, Response). See references/gotcha_failure_phase_tagging.md.
Data Model and Storage
See references/data_model.md for full storage layout, JSON schemas, default config, and OKRs.
Key storage paths:
- Data:
{agent_root}/commons/data/ocas-praxis/ - Journals:
{agent_root}/commons/journals/ocas-praxis/YYYY-MM-DD/{run_id}.json
Inter-skill Interfaces
All skills → Praxis (cooperative read): Praxis scans journal output from every skill. Consumed journal_id values tracked in journals_evaluated.jsonl.
Known journal-producing skills: ocas-spot, ocas-rally, ocas-taste, ocas-finch, ocas-fellow, ocas-scout, ocas-bones, ocas-bower, ocas-vibes, ocas-voyage, ocas-imagine, ocas-weave, ocas-vesper, ocas-dispatch, ocas-mentor, ocas-lucid, ocas-sands, ocas-sift, ocas-reach, ocas-look, ocas-multipass, ocas-forge, ocas-haiku, ocas-custodian.
See references/journal_ingestion.md for journal schema and ingestion rules.
Recovery Behavior
Implements the recovery contract from spec-ocas-recovery.md.
- Evidence: Every run writes an evidence record including no-op runs.
not_activity_reasonmandatory. - Gap detection: If gap exceeds expected cadence, logs
gap_detected. - Degraded mode: When journal directories unavailable, logs
degraded: journals. - Log compaction: 30 days (no-op) / 90 days (error/gap). Last 7 days retained.
Initialization
On first invocation, run praxis.init:
- Create
{agent_root}/commons/data/ocas-praxis/and subdirectories - Write default
config.jsonif absent - Create empty JSONL files
- Create journal directory
- Register cron jobs:
praxis:journal_ingest(every 30min),praxis:decay_check(noon daily),praxis:debrief(6am daily),praxis:update(midnight daily) - Log initialization as DecisionRecord
Second-Wave Detection (Already Evaluated)
When triggered by the dispatcher, always check journals_evaluated.jsonl for the journal filename before running mtime-based discovery. If the journal is already present (regardless of action_taken), skip silently — it was already evaluated by a prior Praxis run in the same or previous dispatch wave. This is the correct no-op and prevents duplicate re-ingestion, unnecessary gap backfill, and evidence log bloat.
grep -q "mentor-light-20260624T044239Z" <hermes-home>/profiles/indigo/commons/data/ocas-praxis/journals_evaluated.jsonl
# If exit code 0: already evaluated, write no-op journal and exit silently
Dispatch / Cron Integration
When triggered by the dispatcher (dispatcher.py) as part of a multi-skill dispatch, Praxis owns:
journals_evaluated.jsonl— append-only log of all evaluated journalsingest_state.json—last_ingest_runtimestamp and counters
See references/dispatch-ingest.md for the full ingest procedure, decision table (genuine vs second-wave), and pitfalls.
Single-skill dispatch (Praxis only): Follow the standard journal ingest workflow. Use templates/dispatch_ingest_template.py with CAPTURED_TS — never write inline scripts.
Multi-skill dispatch (Forge + Mentor + Praxis): Read for the full cross-pipeline procedure including second/third/fourth-wave mitigation, concurrent cron gap handling, and cold-start initialization.
Key rules:
- Capture
last_ingest_runBEFORE Mentor runs (Mentor heartbeat advances it) - Third-wave mitigation is mandatory: add ALL dispatch-output journals to eval file and advance state
- Gap journal backfill after every run (catches concurrency gaps + date filter misses)
execute_codeis blocked in cron mode — useterminal()with scripts written viawrite_file()- Never do
ts.isoformat() + "+00:00"— double suffix breaksfromisoformat() - Large gap backfill (80+ entries) is normal at steady-state — cron pipelines write ~10 journals/minute. Between dispatch waves (7-8 min apart), expect 50-80 gap entries. This is expected, not a failure. See
references/session-20260629-dispatch-1030Z-praxis-second-wave-gap-backfill.md - Cold-start: initialize state with CURRENT timestamp, not epoch
- Pure eval-registration dispatch (confirmed 2026-06-30T11:25Z): When ALL
new_filesare already in praxis eval (just missing from dispatch eval) or are prior-wave artifacts, the Praxis pipeline does NOT need to run. Register directly from the dispatch pipeline, advancelast_ingest_run, do NOT incrementjournals_evaluated_count. Seereferences/session-20260630-dispatch-1125Z-praxis.md. - CAPTURED_TS calibration (verified 2026-07-10, Mentor 2.8.23): The light heartbeat did NOT advance
ingest_state.json:last_ingest_runin this deployment. Before applying the CAPTURED_TS override, checklast_ingest_runAFTER Mentor runs. If it is unchanged from the pre-Mentor value, run the ingest WITHOUT CAPTURED_TS — mtime discovery still finds the new journals (the override is only needed when the state timestamp actually moved forward). Applying CAPTURED_TS unnecessarily is harmless but adds an avoidable env-var step and a date-format footgun. - No
praxis-dispatchjournal from the template (verified 2026-07-10):templates/dispatch_ingest_template.pydoes not write apraxis-dispatch-*.jsonjournal (unlike older production pipelines). The dispatch-output journals to bridge into the DISPATCH eval during third-wave mitigation are therefore: every journal the ingest just evaluated (all of them — the forge-scan output, the mentor-light heartbeat output, and any other cross-skill journals it registered) PLUS thedispatch-wave-*journal you write. Do NOT look for or fabricate apraxis-dispatchjournal; bridge the full set of ingest-evaluated journal_ids instead.
Journal Outputs
Action Journal — every event recording, lesson extraction, shift change, and debrief generation. Include entities_observed, relationships_observed, preferences_observed with user_relevance field.
Debrief Generation
When running praxis.debrief.generate outside the scheduled cron:
- Load active shifts from
shifts.jsonl— filterstatus == "active", count reinforcement, compute age fromactivated_atorlast_reinforced_at - Scan for decay risk — shifts with
reinforcement_count == 0and age > 10 days are "approaching decay" (flag in debrief) - Scan for overlap — group active shifts by domain+phase; flag shifts sharing >3 words as potential consolidation candidates
- Count recent events — last 200 events by signal_type to identify emerging patterns
- Check cap headroom — if active shifts ≥ 10, flag "approaching cap" with weakest shift identified for potential manual expiry
- Write structured debrief to
debriefs.jsonlwith fields:debrief_id,generated_at,period,active_shift_count,cap_usage,new_shifts,expired_shifts,new_lessons,findings,recommendations - NEVER use
write_fileon JSONL files — it overwrites. Useterminal("python3 -c ...")or append viaopen(..., 'a')
Debrief JSON structure:
{
"debrief_id": "debrief-YYYYMMDDTHHMMSS",
"generated_at": "ISO timestamp",
"period": "YYYY-MM-DD to YYYY-MM-DD",
"active_shift_count": 12,
"cap_usage": "12/12 (at cap)",
"new_shifts": 0,
"expired_shifts": 0,
"new_lessons": 1,
"findings": ["finding 1", "finding 2"],
"recommendations": ["rec 1", "rec 2"],
"events_ingested": 0,
"lessons_extracted": 0,
"shifts_proposed": 0,
"shifts_activated": 0,
"shifts_expired": 0
}
Gotchas — Critical
Key gotchas (see references/gotchas-praxis.md for the full catalog):
Dedup key must be
(source_journal, signal_type)— Usingsource_journalalone as the dedup key inevents.jsonlpost-write dedup collapses multiple distinct signals from the same journal into one event. In ingest_20260606_v3, finch scan-1800 produced bothcron_errorsandauth_failuresignals, but only the first survived dedup — the second had to be recovered manually. This matches the known limitation documented iningest-script-pattern.md§Post-Write Dedup. Always dedup by(source_journal, signal_type), not justsource_journal.Shift cap enforcement requires proactive merge-before-cap, not just reject-at-cap — When proposing shifts, the merge-overlap check (domain+phase) MUST happen BEFORE the cap check. In the 2026-06-17 ingest, 5 new shifts were proposed and activated before the cap was hit, but 2 were duplicates of existing active shifts (same signal_type+phase). The merge logic caught them during cleanup, but the original ingestion didn't merge at proposal time — it just let them fill the cap. Fix: The shift proposal loop must check domain+phase overlap against ALL active shifts and merge/reinforce instead of proposing new shifts when overlap exists. This prevents cap saturation with duplicates.
Noise signal types must be filtered at lesson creation, not just shift proposal — The 2026-06-17 ingest produced 5 noise lessons (
routine,no_signal,cron_error,forge_activity,no_op,success) withconfidence: highthat then produced shifts. The NOISE_SIGNAL_TYPES filter exists in the ingest script but wasn't applied during lesson extraction Pass 2. Fix: ApplyNOISE_SIGNAL_TYPES = {"", "unknown", "?", "no_op", "forge_activity", "routine", "no_signal", "cron_error", "cron_errors", "observation", "success", "mentor_light"}filter immediately after Pass 2 grounding, BEFORE writing tolessons.jsonl. This prevents noise from ever entering the lesson pool.Mentor-light
low_coverageis a measurement artifact — filter at extraction time — Theevaluation_coveragemetric in mentor-light heartbeats (0.14–0.30) only counts skills with new journal entries in the scan window, NOT total active skills (which is 20+). The mentor correctly reportsactive_skills_30d: 20alongsideevaluation_coverage: 0.3because only ~6 of 20 skills had new files. This is expected scan-yield behavior, NOT a system failure. When mentor-light journals producelow_coverageas their only non-success signal, emitno_signalinstead of recording alow_coverageevent. Do NOT addlow_coverageglobally to NOISE_SIGNAL_TYPES — it may be legitimate from other sources. Filter specifically: ifsource_journalmatchesmentor-light-*andsignal_type == "low_coverage"andoutcome == "success", skip event recording. Discovered 2026-06-18: mentor-lightlow_coveragereached 11 events and produced a lesson + shift that is semantically meaningless. Seereferences/session_20260618_ingest_cron_z.md.Mentor-light
gap_detectedwithoutcome: "success"is a routine measurement — filter at extraction time — Thegap_detectedflag in mentor-light heartbeats fires when the time since the last scan exceeds a threshold (typically 25-30 minutes). This is normal cron cadence behavior, NOT a system failure. Thegap_minutesfield (e.g., 27.2) is within expected range for 30-minute cron intervals. When mentor-light journals producegap_detected: truewithoutcome: "success"and no other failure signals, emitno_signalinstead of recording agap_detectedevent. Filter specifically: ifsource_journalmatchesmentor-light-*andsignal_type == "gap_detected"andoutcome == "success", skip event recording. Addinggap_detectedglobally to NOISE_SIGNAL_TYPES would hide genuine gap detections from other sources (e.g., custodian). The existing active shiftgap_detected | ocas-mentor | Executionalready covers gap detection behavior; adding routine cron-cadence events only creates duplicate noise. Discovered 2026-06-20: mentor-lightgap_detectedproduced an event from a 27.2-minute gap that was pure cron cadence.Mentor-light
failure_keywordfrom generic summary scanner is a false positive — filter at extraction time — Mentor-light heartbeat journals withoutcome: "success"(or nooutcomefield) contain summary text like "0 errors detected", "2 historical error records in evidence", or "0 active anomalies". The generic summary scanner picks up the word "error" and emits afailure_keywordsignal — but the journal is reporting SUCCESS, not failure. When mentor-light journals haveoutcome in ("success", "", None)and no explicit failure indicators (gap_detected: trueormetrics.errors > 0), skip ALL generic signal extraction and returnno_signal. Do NOT rely on thesignal_typefield alone — these journals may not have one, and the generic path assignsfailure_keywordfrom summary text. Filter at the journal level, not the signal level. Discovered 2026-06-20: 8 false-positivefailure_keywordevents from mentor-light journals in a single ingest run. Seereferences/session_20260620_ingest.md.Mentor-light
correctionfrom routine data updates is a false positive — filter at extraction time — Mentor-light heartbeat journals withoutcome: "success"may contain summary text like "active_skills_30d corrected 14→18" or "Script succeeded on all 3 writes". The signal extraction emits acorrectionsignal — but this is a routine data correction (count update), not a behavioral failure. When mentor-light journals haveoutcome in ("success", "", None)and the only non-success signal iscorrection, skip event recording and returnno_signal. This is a distinct false-positive source fromfailure_keyword— the same filter gate (mentor-light + success outcome) catches both. Confirmed 2026-06-22: mentor-light journal producedcorrectionevent from routine active_skills count update.Dispatch-wave
correctionfrom routine count updates is a false positive — filter at extraction time — Dispatch-wave journals (source matchingdispatch-wave-*) with summary text like "Mentor corrected 8→22" or "eval gaps corrected" emit acorrectionsignal — but this reports that a downstream skill (Mentor, Forge) updated a count during its run, not that a behavioral correction occurred. The dispatch wave is orchestrating; the counts it reports are routine operational results from child skills, not system corrections. When a dispatch-wave journal hastype: "dispatch.wave"and its only non-success signal iscorrection, skip event recording and returnno_signal. This applies the same logic as the mentor-lightcorrectionfalse-positive filter. Confirmed 2026-06-29: dispatch-wave journal producedcorrectionevent from "Mentor corrected 8→22" in summary. Seereferences/session_20260622_ingest_cron_0409.md.Dispatch-wave
mixed_genuine_no_opis a routine orchestration outcome — filter at extraction time — Dispatch-wave journals withoutcome: "mixed_genuine_no_op"describe a dispatch that processed routine cron output with no actionable signals. The term "genuine" refers to the eval registration being genuinely needed (not second-wave re-detection), not to a behavioral event being detected. When a dispatch-wave journal hastype: "dispatch.wave"andoutcomecontainsno_op(e.g.,mixed_genuine_no_op,second_wave_no_op), skip event recording and returnno_signal. The dispatch pipeline completed successfully with no behavioral signals — this is the expected steady-state for routine cron output. Confirmed 2026-06-30: dispatch-wave journal withoutcome: "mixed_genuine_no_op"was incorrectly recorded as amixed_genuine_no_opevent by Praxis ingest, then required manual cleanup.Dispatch-wave
escalationechoing an already-evaluated Praxis-internal signal is a false positive — filter at extraction time — Dispatch-wave journals (schemadispatch-wave-v1) may carry anescalations[]array whose entrysourcepoints at a Praxis cron journal (or any journal already processed by a prior Praxis run) with astatuslike "tier1 fix applied; already evaluated by praxis ingest; no personal input required from ". This is a second-wave echo of a signal already handled by an earlier Praxis run — NOT a new behavioral event. The generic signal scanner keys off the word "escalation" in theescalations[]array and emits a weakescalationevent (summary "Unknown —"), which pollutesevents.jsonland double-counts the underlying issue. When a dispatch-wave journal'sescalations[]entry hassourcematchingpraxis-cron-*(or any already-evaluated journal) ANDstatusindicates already-handled/no-personal-input, skip event recording and return `no_sig
…(truncated)