Praxis
Skill indigokarasu/praxis
Bounded behavioral refinement loop. Records outcomes, extracts micro-lessons from repeated patterns, consolidates them into capped active behavior shifts, applies shifts at runtime, and generates plain-language debriefs. Use for recording task outcomes, extracting lessons from repeated patterns, managing active behavior shifts, generating runtime briefs, or producing debriefs. Not for: general memory (use Chronicle), preference tracking (use Taste), real-time task execution, content generation, system health monitoring (use Custodian), or skill evaluation scoring (use Mentor).From its SKILL.md
npx -y skills add indigokarasu/praxisAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its file declares
Copied from the file, not written here
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
76.2 KB, ~18.6k tokens by cl100k_base, as published. Nobody here has run it
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 <operator>". 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 returnno_signal. If the event was already written by the production script, remove it fromevents.jsonlbyevent_id(post-hoc manual cleanup — established pattern for already-written false positives). Confirmed 2026-07-07:dispatch-20260707T103730Z.jsonproduced anescalationevent (event_idevt-20260707104141463939-0947) from anescalations[]entry whose source waspraxis-cron-20260707T084657Z.json(already evaluated); removed during cleanup, leaving 0 genuine behavioral events for the run. -
Production ingest script may record events from phantom (non-existent on disk) source journals — verify
os.path.exists()and remove — The productionpraxis_ingest_run.pycan emit an event whosesource_journalpath resolves to a file that does NOT exist on disk, even though thatjournal_idis present injournals_evaluated.jsonl(marked evaluated). Root cause: the script's file-discovery or journal-list reference includes a journal that was deleted, rotated, or never materialized, yet it still reads/derives a signal from it and records an event. Confirmed 2026-07-07: the script recorded anescalationevent (event_idevt-20260707131835467668-14860) attributed toocas-custodian/light-scan-2026-07-07T131135.json;test -f+search_filesconfirmed the file is MISSING on disk, thoughjournals_evaluated.jsonlcarried 1 entry for it. The event was a false positive — there was no real custodian escalation journal at that timestamp (the actual custodian light-scan at 12:07 hadescalation_needed: true, but it is a tracked user-gated fault and NOT the source of this event). Detection: after the production run, for any non-no_signalevent (escalation/failure_keyword/execution_error), resolvesource_journalto{agent_root}/commons/journals/<source_journal>and checkos.path.exists(). Cleanup (post-hoc, established pattern): if the source file is missing, remove the event fromevents.jsonlbyevent_idusing a Python heredoc — NOTcat file | python3, which trips the pipe-to-interpreter security scanner in cron mode. Removing the phantom leaves the run's genuine behavioral event count at 0, which then triggers the Bug-2--new-genuine-events 0fast-path to clear ALL extracted lessons as full-history noise. Recommended script fix:praxis_ingest_run.pyshouldos.path.exists()-guard each resolvedsource_journalpath immediately before recording an event and skip events whose source file is absent (prevents the phantom from ever enteringevents.jsonl). This is distinct from the dispatch-wave escalation echo (which references an already-evaluated journal that DOES exist on disk) — here the referenced journal file is simply absent. -
Custodian
actionjournals with error mentions in summary are routine operational reports — filter at extraction time — Custodian light-scan/action journals (type:"action") routinely contain summary text like "All other error jobs are either transient (429), no-op exits, disabled, or already tracked" when reporting on known cron job states. The generic summary scanner picks up "error" and emits afailure_keywordsignal — but the journal is reporting on known/tracked issues, not a new behavioral failure. When a custodian journal hastype: "action"and the summary contains "error" butescalation_neededis absent or the journal is a routine scan (no newfindingswithseverity: "critical"), skip generic signal extraction and returnno_signal. The existingobservationtype filter only coverstype == "observation"— theactiontype with error mentions is a distinct false-positive source. Discovered 2026-06-21: custodian light-scan action journal produced afailure_keywordevent from summary text about known error jobs. A single event won't produce a lesson (needs ≥2), but it pollutes the event stream. Seereferences/session-20260621-dispatch-9.md. -
Custodian
type: "observation"is a routine scan — emitno_signal— Custodian journals withtype: "observation"are routine platform scans that check gateway status, disk usage, and job health. They do not represent behavioral signals. When a custodian journal hastype: "observation", emitno_signaland skip signal extraction. This is distinct from custodiandeep-scanorlight-scantypes which may contain genuine signals. Confirmed 2026-06-21: custodian observation journal produced no actionable signals. -
Dispatch-triage journals are email triage records, not behavioral signals — filter at extraction time — ocas-dispatch journals with
triagein the filename (e.g.,dispatch-triage-*.json) are records of email inbox triage decisions (action: none, informational). They don't represent behavioral failures or system issues. When signal extraction encounters a journal fromocas-dispatch/withtriagein the filename, emitno_signaland skip. Confirmed 2026-06-28: dispatch-triage journal was incorrectly falling through to generic signal extraction in inline scripts. -
Custodian journals without a
typefield are routine operational reports — filter at extraction time — Some custodian light-scan journals (post-2026-06-22) lack atypekey entirely. The existingtype: "action"filter doesn't catch these. Check byrun_idpattern (light-scanordeep-scanin run_id) + noescalation_needed+ nopersistent_failuresincron_registry. When all three conditions are met, emitno_signaland skip signal extraction. Theis_false_positive_journal()function inpraxis_ingest_run.pywas patched on 2026-06-22 to handle this variant. Confirmed 2026-06-22: custodian light-scan journal with notypefield produced a false-positivefailure_keywordevent from summary text about "19 error jobs" that were all first-occurrence/known patterns. -
Custodian light-scan with UUID-style
run_idandnot_activity_reasoncontaining transient/stale errors is a no-op — filter at extraction time — Custodian light-scans with 10-character hexrun_id(e.g.,01e29333-454) use the post-2026-05 schema wherenot_activity_reasonexplains the verdict. Whennot_activity_reasoncontains "all transient" or "all...are transient or stale" ANDtier1_fixes_applied: 0ANDissues_escalated: 0AND notypefield, emitno_signaland skip signal extraction — even if the summary orobservations.transient_errorscontains the word "error". Theis_false_positive_journal()pre-filter must checknot_activity_reasonfor phrases like "all error jobs are transient", "all transient or stale", "all...are transient" combined with zero fixes/escalations; if matched, returnno_signalbefore keyword scanning. Without this filter, journals reporting all-transient error states produce false-positivefailure_keywordevents (the keyword "error" appears innot_activity_reasonandobservations.transient_errors[].fingerprintdescriptions). Confirmed 2026-06-26:01e29333-454.jsonproduced afailure_keywordevent from "4 error jobs, all transient or stale" in summary; theis_false_positive_journal()handler only checked fortypefield presence/custodian action sub-type, not the typeless light-scan + all-transient verdict pattern. This event was caught and removed by post-ingest review, but it should never have been recorded. -
Custodian
esc-loop/escalation-execution-loopjournals withescalation_needed: trueare GENUINE escalation signals — do NOT filter — Custodian's scheduled escalation loop (scan_type: "escalation-execution-loop",run_idlikeesc-loop-*.json) reviews tracked user-gated issues, pauses burning cron jobs, and setsescalation_needed: truewhen issues remain that require <operator>'s action (billing / API-key rotation / skill-internal model / interactive Google re-auth). This is a real behavioral signal, NOT one of the routine/healthy custodian false-positive variants (observation / action / typeless light-scan / all-transient light-scan). When a custodian journal hasescalation_needed: true, record a genuineescalationevent (do not skip). Theis_false_positive_journal()pre-filter already excludes esc-loop (it only matchesobservation,action, and typeless/all-transientlight-scan), so it passes through correctly — but this is intentional, not an omission; do not "fix" it by adding esc-loop to the filter. Always still apply the phantom-file guard: verifyos.path.exists()on the resolvedsource_journalbefore trusting the event. Confirmed 2026-07-07:esc-loop-20260707T173625Z.jsonproduced a verified genuine escalation event (source file present on disk,escalation_needed: true, 4 user-gated issues confirmed). -
Lesson content dedup required — The
lesson_idincludes a random/timestamp component, so dedup bylesson_idalone does NOT prevent semantic duplicates. Each ingest run generates different IDs for the same(signal_type, phase)group. Always dedup by(signal_type, failure_phase)content fingerprint before writing lessons. Seeingest-script-pattern.md§Lesson Content Dedup. Without this,lessons.jsonlgrows by ~9-49 duplicate entries per run. -
Lesson dedup key must normalize
failure_phaseto lowercase — The(signal_type, failure_phase)dedup key is case-sensitive. Existing lessons may havefailure_phase: "Planning"(capitalized) while new lessons producefailure_phase: "planning"(lowercase), causing the dedup to miss the match and create a semantic duplicate. In the 2026-06-22 ingest, acoverage_gaplesson was duplicated because"Planning" != "planning". Fix: Normalize both sides to lowercase before comparison — seereferences/inline-examples.md§Lesson dedup key for thelesson_dedup_keyfunction andreferences/support-file-map.mdfor the When-to-read signal. Apply this to both the new lesson AND when building the existing-lesson dedup set. Also apply in Pass 1 event grouping — normalizefailure_phasebefore grouping to prevent split groups across case variants. Seereferences/session_20260622_ingest_followup.md. -
Active shift cap is hard — 12-shift cap enforced on every activation
-
Lessons require causal grounding — "do X because Y" not just "do X"
-
Forge
resultfield has multiple no-op variants — Forge scan journals useresult: "no_op",result: "clean","no-op"(hyphenated),"NO_UNPROCESSED_FILES"(uppercase), and longer strings like"clean — no pending VariantProposal or VariantDecision files"or"clean — no unprocessed VariantProposal or VariantDecision files found"to indicate routine success (nothing to process). All are healthy system states. Fix: DefineFORGE_NO_OP_RESULTS = {"no_op", "clean", "no-op", "no_unprocessed_files"}and checkresult.lower().strip() in FORGE_NO_OP_RESULTS. Do NOT rely on exact string matching against"no_op"alone. The status-less forge schema variant (noresultkey, nostatuskey, or empty summary string) is also a no_signal — treat as routine no-op when no other failure indicators are present. Seereferences/session_20260616_ingest_cron_afternoon2.mdfor the full variant catalog. -
Forge no-op filter must use
startswith, not exact match — Forge result strings routinely include trailing detail text after the no-op keyword (e.g.,"clean \u2014 no unprocessed VariantProposal or VariantDecision files found"). TheFORGE_NO_OP_RESULTSset check withresult.lower().strip() in FORGE_NO_OP_RESULTSdoes NOT match these longer strings, causing false-positiveforge_errorevents. In the 2026-06-16 18:26 ingest, 2 false-positive events and 1 false lesson were produced before manual cleanup. Fix: Replace the exact-match check with astartswithloop:def is_forge_no_op(result_val): if not result_val: return False r = str(result_val).lower().strip() return any(r.startswith(prefix) for prefix in FORGE_NO_OP_RESULTS)Or split on the em-dash/whitespace and check only the first token:
r.split('\u2014')[0].strip().split()[0] in FORGE_NO_OP_RESULTS. -
Forge no-op
FORGE_NO_OP_RESULTSmust include"no unprocessed"(with spaces) — The set typically includes"no_unprocessed_files"(underscores), but forge journals write natural language with spaces:"No unprocessed VariantProposal...". Thestartswithcheck against"no_unprocessed_files"does NOT match"no unprocessed variant...". Fix: Add"no unprocessed"to the prefix set:FORGE_NO_OP_RESULTS = {"no_op", "clean", "no-op", "no_unprocessed_files", "no unprocessed"}. -
Forge
actions_takencan be an empty list[]with noresultfield — Some forge journal variants (post-2026-06-19) haveactions_taken: [](empty list), noresultkey, nostatuskey, andfindings: {unprocessed_proposals: 0, ...}. The forge no-op filter that only checksresultandstatusfields misses this variant, causing it to fall through to signal extraction (where it produces no signals but is classified asno_signalinstead offorge_no_op). Fix: Inis_forge_no_op(), also check: (1)actions_takenas a string withstartswithagainstFORGE_NO_OP_RESULTS, and (2)actions_takenas an empty list combined with zero findings. Seereferences/session_20260619_ingest_cron_c.md. -
Finch
action.resultinstead of top-levelresult— Newer forge journals (post-2026-06-16) nest the result underaction.result(e.g.,{"action": {"result": "no_new_files"}}). The forge no-op pre-filter only checksdata.get("result", "")at the top level. Fix: Also checkdata.get("action", {}).get("result", "")inis_forge_no_op(). But note:actioncan also be a string (e.g.,"No unprocessed VariantProposal or VariantDecision files found...") — always guard withisinstance(action, dict)before.get("result"). -
finch_actionable_emailis a legitimate signal type — NOT noise — Finch scan journals produceactionableemail counts when new emails require attention (job opportunities, application updates, etc.). This is a genuine positive signal, not a no-op. Do NOT addfinch_actionable_emailtoNOISE_SIGNAL_TYPES. The signal should produce events and, when ≥2 events accumulate, lessons. The only filter: ifactionable == 0, skip (no new emails to act on). Discovered 2026-06-20: 12 finch_actionable_email events from 10 scans produced the first finch lesson. -
Finch
new_tasks_addedis a list, not an int — Finch scan journals storenew_tasks_addedas a list of task dicts, NOT as an integer. Checkingdata.get("new_tasks_added", 0) > 0crashes withTypeError. Fix: Uselen(new_tasks) if isinstance(new_tasks, list) else (new_tasks if isinstance(new_tasks, int) else 0). -
Initialize ALL accumulator variables before any loop or conditional —
truly_new,remaining_proposals, and any accumulator must be initialized before theif/forblock that might define it. A variable assigned only inside afor/elsebody does not exist when the loop iterates 0 times, causingNameErrorafter data writes have already completed. -
Inline Python heredoc variable shadowing in
terminal()— When writing dispatch ingest logic as inline Python insideterminal(), a variable likenew_journalsbuilt in one scope (e.g., a set-difference loop) can be silently shadowed by a same-named variable in a later block (e.g., the eval-write loop that rebuilds it from scratch). The result: diagnostic counters report 0 even though writes succeeded (the file grows correctly). The fix: use distinct variable names for each logical stage (discovered_journals,eval_written_count) and always verify writes withwc -lpost-run rather than trusting inline counters. Confirmed 2026-06-28 dispatch: eval file grew from 42,241 to 42,248 (+7) but script reportedjournals_evaluated: 0. -
Eval entry
sourcefield must be set explicitly — When writing tojournals_evaluated.jsonlfrom inline Python, thesourcefield defaults to empty/missing if not explicitly included in the entry dict. Future gap analysis grep checks (grep "source" eval_file) then show?or empty. Always include'source': 'dispatch-mtime-discovery'(or appropriate source tag) in every eval entry dict. Confirmed 2026-06-28: 7 entries written with missing source field. -
Cap enforcement must use a separate counter, not the mutable list — When activating shifts in a loop, do NOT check
len(active_shifts)if you're appending toactive_shiftsinside the same loop. The list grows on every iteration and the cap is never enforced. Use a separateactive_countvariable computed once before the loop, and increment it manually on each activation. -
Cap enforcement must use full file rewrite, not append-only — When the cap is exceeded and a shift is expired in-memory to make room for a new one, the expired shift's status change is NOT persisted if you only append new shifts to
shifts.jsonl. The expired shift remainsactiveon disk. Fix: After all in-memory modifications (reinforce, expire, activate), do a FULL rewrite ofshifts.jsonlfrom the canonical in-memory state. Track new shifts separately and only append those if you must use append-only — but prefer full rewrite. Seereferences/session_20260618_ingest.mdfor the repair procedure. -
Shift file rewrite must not double-write — When rewriting
shifts.jsonlafter modifying active shifts in memory, do NOT writeexisting_shifts(which includes all old shifts, already expired ones and all) AND then appendnew_shifts. Either: (a) rewrite the entire file from the merged in-memory list, or (b) append only new shifts to the existing file (don't re-write existing entries). In the 2026-06-18 repair, the rewrite wroteexisting_shifts(78 entries) +new_shifts(12 entries) = 90 entries total. A subsequent re-read showed 78 + 12 = 90, but the old active shifts were already expired in-memory so the file was correct by accident. Be explicit: either full rewrite from canonical in-memory state, or append-only for new entries. -
Domain must be the skill name, not the signal_type — When proposing shifts from events,
domainmust be set to the skill that produced the events (e.g.,ocas-mentor), NOT to thesignal_type(e.g.,gap_detected). Settingdomain = signal_typeproduces meaningless shifts like "In gap_detected during Planning: gap_detected recurs" instead of "In ocas-mentor during Planning: gap_detected recurs". Fix: Use theskillfield from the events that contributed to the lesson, or use the most common skill in the event group as the domain. Only fall back tosignal_typeas domain if no skill information is available. -
Lesson extraction must filter events with null/None/empty failure_phase — Before grouping events for lesson extraction, filter out events where
failure_phaseisNone,null,"", or"MISSING". These produce meaningless lessons like "Monitor and address X during None phase". In the 2026-06-16 ingest, 90 events with invalid phases produced 26 bad lessons. Fix: Addvalid_events = [e for e in all_events if e.get('failure_phase') and str(e.get('failure_phase')).lower() not in ('none', 'null', '', 'missing')]before the grouping loop. -
Writing complex Python scripts — use heredoc, not write_file —
write_file()and inlinepython3 -csilently corrupt multi-line Python (merged lines, mangled quotes, unterminated strings). For scripts >20 lines, usecat > /tmp/script.py << 'EOF'interminal(), then run withpython3 /tmp/script.py. Confirmed 2026-06-29: 3 consecutive write_file attempts all produced SyntaxError; heredoc worked on first try. Seereferences/ingest-script-pattern.md§Writing complex Python scripts. -
patchcorrupts multi-line JSON replacements iningest_state.json(2026-07-01) — Thepatchtool's fuzzy matching can mangle JSON structure when replacing multi-line blocks. During this cron ingest, apatchcall targeting lines 50-52 ofingest_state.jsonsuccessfully replaced the targeted fields but dropped thestale_script_cleanupsub-object that immediately followed, producing invalid JSON that wouldn't parse. Root cause: fuzzy matching matched and replaced a block boundary that included context from the next object, and thenew_stringdidn't re-declare it. Fix: For multi-line edits toingest_state.json(or any nested JSON state file), prefer full file rewrite viawrite_file()overpatch(). Ifpatchmust be used, ensure theold_stringincludes ALL content between the target lines and the start of the next top-level key — or better, verify JSON validity withpython3 -c "import json; json.load(open(...))"immediately after applying. Confirmed 2026-07-01: 2-step patch (journal path + decay timestamp) broke the file; had to recover via fullwrite_filerewrite. -
ingest_state.jsonhas two gap-backfill counters — readeval_gaps_backfilled, notgaps_backfilled— Aftergap_backfill.pyruns, its stdout printsgaps_backfilled=N, but the field it actually writes iseval_gaps_backfilled. The separategaps_backfilledkey is a stale duplicate that stays at0and is NOT updated by the script. When you read state and seegaps_backfilled: 0immediately after a backfill that printedgaps_backfilled=26, that is NOT corruption —eval_gaps_backfilledholds the real cumulative count. Always readeval_gaps_backfilledfor the authoritative backfill total; treat the baregaps_backfilledkey as dead. Confirmed 2026-07-07: gap_backfill printedgaps_backfilled=26; on-diskeval_gaps_backfilledbecame 26 whilegaps_backfilledstayed 0 — the discrepancy looked like state clobbering until the two-field split was identified. -
Rewrite
ingest_state.jsonvia Python load→modify→dump, not hand-typed JSON — The patch-corruption pitfall above says prefer a full file rewrite overpatch(); correct, BUT hand-authoring the entire 58-field JSON intowrite_file()is itself error-prone — it is trivial to omit a nested sub-object (e.g.,stale_script_cleanup) and silently lose a counter or produce invalid JSON. Safest pattern: read current state withjson.load, mutate only the fields you need (s['last_ingest_run'] = ...,s['journals_processed'] = s.get('journals_processed',0)+N, etc.), thenjson.dump(s, open(f,'w'), indent=2). This preserves every other field automatically. Use aterminal()Python heredoc for the multi-line logic (notwrite_filefor the JSON body). Confirmed 2026-07-07: full ingest-state update done this way — all 58 keys preserved, no field dropped. -
os.walkcan return phantom files that don't exist (2026-06-29) — During gap backfill,os.walkmay list files deleted by concurrent processes between the directory listing and youros.stat()call. Always guard withos.path.exists(fpath)before stat or gap classification. A phantom gap entry that can't be opened is a race artifact, not a real gap — skip silently. -
Bug 2 noise lessons can have
signal_typekey MISSING entirely — not just"?"(2026-06-29) — The production script's Pass 2 grounding produces lessons that lack asignal_typekey altogether when source events have no signal_type field. Cleanup filters that only checksignal_type == "?"orsignal_type == ""miss this variant. Any noise cleanup MUST also checkles.get("signal_type") is None. Confirmed 2026-06-29: 13 Bug-2 lessons bypassed the existing filter because the key was absent, not set to"?". -
Decay age computation: use
last_reinforced_at, NOTactivated_at(2026-07-01) — When computing shift age for decay analysis, the clock resets on every reinforcement. A shift activated 12 days ago that was last reinforced 2 days ago has ~12 days remaining (at 14-day TTL), NOT ~2 days. Usingactivated_atas the decay baseline produces false "approaching decay" warnings and pollutes the debrief with incorrect findings. Always uselast_reinforced_atas the primary age field; fallback toactivated_atonly iflast_reinforced_atis missing entirely. The decay-risk flag (reinforcement_count == 0 AND age > 10 days) is already correct inscripts/praxis_debrief.py. This pitfall is for anyone writing inline debrief logic (cron mode, dispatch) that computes ages manually. Confirmed 2026-07-01: inline debrief incorrectly flagged all 3 shifts as approaching decay when they had been reinforced 2 days prior with ~12 days remaining. -
Shift records carry DUPLICATE reinforcement fields — read the canonical one (confirmed 2026-07-11) —
shifts.jsonlentries contain BOTHreinforced_count(appears early in the dict, frequently 0) ANDreinforcement_count(appears later, authoritative), and BOTHlast_reinforcedandlast_reinforced_at. A decay scan that readss.get('reinforced_count', 0)instead ofs.get('reinforcement_count', 0)will wrongly treat a reinforced shift asreinforcement_count == 0, flagging it as decay-risk and risking a force-expire of a live shift. Confirmed 2026-07-11: active shiftshf-rebuild-20260618T063158-0009hasreinforced_count: 0(early) butreinforcement_count: 1(late) withlast_reinforced_at: 2026-06-28— correctly NOT in decay_risk becausereinforcement_count(notreinforced_count) is canonical. Always readreinforcement_countandlast_reinforced_at; treatreinforced_count/last_reinforcedas legacy aliases to ignore.
Double-Z timestamp bug in praxis-cron journals (STILL ACTIVE 2026-06-30) — The praxis_ingest_run.py script occasionally produces journal filenames with double-Z suffixes (e.g., praxis-cron-20260630T092758ZZ.json). Root cause: timestamp composition applies .rstrip('Z') + 'Z' to a value already ending in Z, OR two ISO timestamp components get concatenated. Mitigation: gap backfill and dispatch pipeline treat these filename as-is for eval registration — no rename needed at eval time. Fix needed: audit praxis_ingest_run.py journal output section to check ts.endswith('Z') before appending Z. Confirmed recurring: 2026-06-26, 2026-06-28, 2026-06-30.
-
Shell heredoc journal writing also produces double-Z (2026-07-01) — When using shell heredoc (
cat > file << EOF) to write journal files in cron, the timestamp shell variable typically ends inZ(e.g.,TS="20260701T101349Z"). If the filename template appends anotherZ—${TS}Z.json— the result is...ZZ.json. This is a DIFFERENT source from Bug 4 (production script double-Z). Fix: Strip trailingZfrom the timestamp variable before using it in the filename template:TS_SHORT="${TS%Z}"then use${TS_SHORT}Z.json. Or, always check and fix with post-write rename. Confirmed 2026-07-01: shell heredoc journal producedpraxis-cron-20260701T101349ZZ.json, fixed withmv. -
JSON journal writing in cron: prefer shell heredoc over inline Python — When writing JSON journal files via
python3 << 'PYEOF'heredoc, dict literals with double-quotes get corrupted: smart-quote conversion, variable name truncation after closing quotes, andSyntaxError: invalid decimal literalfrom mangled dicts. Fix: Use shell heredoc (cat > file << EOFwith$TSand$NOWvariables) to write JSON journal files. Reserve Python heredocs for eval file reads/writes with programmatic content. Confirmed 2026-06-30T11:25Z: 6 consecutive inline Python heredoc failures before switching to shell heredoc worked. -
Proposed shifts are invisible to standard decay checks — they accumulate indefinitely — The decay check only scans
status: "active"shifts for reinforcement TTL. Proposed shifts that never get activated sit inshifts.jsonlforever, bloating the file with dead entries. Confirmed 2026-06-30: 15 proposed shifts from the 2026-06-18 rebuild sat idle for 11 days. Fix: Every decay check must also scanstatus: "proposed"entries and expire any ≥10 days old. Seereferences/session-20260630-decay-check-stale-proposed.md. -
Follow explicit script paths — When a user provides an explicit script path for running ingest or other Praxis scripts, use that exact path. Do not substitute or assume alternative locations, even if they seem equivalent. Failure to use the provided path can result in script-not-found errors and failed runs. Always verify the path before execution.
-
Gap backfill MUST run BEFORE overwriting
last_ingest_run(cron checklist ordering trap) —gap_backfill.pythresholds its scan onmtime > last_ingest_runread fromingest_state.json. The Cron Execution Checklist lists "Updateingest_state.json(setlast_ingest_runto current timestamp)" as step 1 and "Gap journal backfill" as step 2 — taken literally, step 1 setslast_ingest_run = now, so step 2's scan (mtime > now) catches ZERO journals silently (no error, just 0 backfilled). The correct order: rungap_backfill.pyFIRST (whilelast_ingest_runstill holds the PRIOR run's timestamp), THEN updateingest_state.jsonincludinglast_ingest_run = now.gap_backfill.pyonly mutates backfill counters, neverlast_ingest_run, so running it early is safe and is the only way it can catch post-ingest / date-filter-missed journals. Confirmed 2026-07-07: running gap backfill with the priorlast_ingest_run(10:37:30) correctly reported 0 missed journals; updatinglast_ingest_runfirst would have made it a silent no-op. -
Pipe-to-interpreter commands hang autonomous cron jobs (security scan →
approval_pending) — Commands that pipe file contents into an interpreter (cat file | python3 -c ...,tail -1 file | python3 ...,grep x file | python3 ...) trip the environment'stirith:pipe_to_interpretersecurity scanner, which routes them toapproval_pendingstatus. A scheduled cron job has no user present to approve, so the command silently blocks and the run stalls (no error, no completion). Fix (cron mode): never pipe files intopython3/jq/etc. Use (a) the dedicatedread_file/search_filestools, (b) a plainwc -l fileorgrepterminal call with no pipe to an interpreter, or (c)execute_codewithfrom hermes_tools import read_file, terminalwhen you must parse/transform content programmatically. Confirmed 2026-07-07: twocat file | python3andtail file | python3calls in this ingest both hitapproval_pendingand had to be rerouted toread_file+execute_code. -
Re-read
ingest_state.jsonAFTERgap_backfill.pybefore your state update —gap_backfill.pywrites its own fields back to the state file (eval_lines, and incrementseval_gaps_backfilled). If you snapshot the state once at the start of the run and later apply your load→modify→dump update, you will clobber gap backfill's writes. Fix: re-readingest_state.jsonimmediately before the final state update so your changes compose on top of the backfill's counters. Confirmed 2026-07-07: gap backfill advancedjournals_evaluated_count49232→49240 between the initial read and the update step; re-reading preserved it. (Pairs with the load→modify→dump pattern — never hand-type the JSON body, which risks dropping nested keys likestale_script_cleanup.)
OKRs
See references/okrs-praxis.md for full OKR definitions and targets.
Key OKRs: event_coverage (≥0.90), lesson_extraction_precision (≥0.80), shift_activation_accuracy (≥0.75), shift_decay_compliance (≥0.95), cap_efficiency (≥0.80).
Self-Update
See references/self-update-praxis.md.
Support File Map
references/praxis-ingest-cli-pitfall.md— Before invokingpraxis_ingest_run.pyin dispatch/cron:--helpis not guaranteed non-mutating; inspect source for interface details and treat any invocation as a real ingest with side effects.
See references/support-file-map.md for the full file registry with "When to read" column.
| scripts/praxis_debrief.py | Manual debrief generation; use when running praxis.debracket.generate outside cron. NOTE: Any new fields added to debrief JSON schema must also be added to references/debrief_workflow.md for consistency. |
| scripts/cleanup_noise_lessons.py | Noise lesson cleanup script; removes Bug 2 noise lessons using fast pre-filter + per-lesson criteria (missing signal_type, low confidence, noise signal types). Rewritten 2026-07-01 to fix write_file corruption and add full Bug 2 logic. Patched 2026-07-01 to fix path resolution bug: ROOT computed as ../.. from script dir (landed at {profile}/skills/); fixed to ../../.. (lands at {profile}/). Patched 2026-07-07: added --new-genuine-events N fast-path — when N<2 (run recorded 0–1 genuine non-no_signal events), clears all lessons as Bug 2 full-history noise even though the all-no_signal pre-filter didn't fire. See references/noise_lesson_cleanup.md for details. |
| references/debrief_workflow.md | Debrief generation steps including shift-population collapse audit, lessons pipeline health check, and JSON schema. Read before modifying debrief logic. |
| references/session-20260629-cron-ingest-0207.md | 2026-06-29 cron ingest: 5,817 gap backfill catchup, 14 noise lessons cleaned |
| references/session-20260630-dispatch-0103Z-praxis.md | Dispatch 2026-06-30 Praxis pipeline: Multi-skill dispatch, routine no-op. 9 journals ingested, 5 no_signal events, 13 Bug-2 noise lessons with missing signal_type key cleaned. Fast pre-filter confirmed. |
| references/session-20260630-dispatch-1125Z-praxis.md | Dispatch 2026-06-30T11:25Z: Pure eval-registration dispatch. All new_files already in praxis prior-wave artifacts — Praxis NOT loaded. JSON journal writing pitfall (shell heredoc vs inline Python, 6 failures). New mixed_genuine_no_op shortcut. |
| references/session-20260629-dispatch-1030Z-praxis-second-wave-gap-backfill.md | Second-wave no-op + 83 gap backfill. Cron pipelines write 50-80 journals between dispatch waves. Expected steady-state rate. Backfill procedure for second-wave dispatches. |
| references/session-20260629-cron-ingest-0308.md | 2026-06-29 cron ingest: phantom gap journal detected (os.walk race), 14 Bug-2 noise lessons cleaned |
| references/session-20260629-cron-ingest-1231.md | 2026-06-29 cron ingest: 13 Bug-2 noise lessons with MISSING signal_type key — cleanup filter bug discovered and fixed |
| references/session-20260629-dispatch-1221Z.md | Genuine dispatch with gap_backfill.py path resolution fix. Concurrent cron gap pattern confirmed. Script lives at skills/ocas-praxis/scripts/ not commons/data/ocas-praxis/scripts/. |
| references/session-20260629-cron-ingest-1404.md | Cron ingest: Bug-2 filter confirmation. All 13 lessons had signal_type=None (key missing). Fast pre-filter ("all events no_signal → all lessons noise") validated. |
| references/session-20260630-cron-ingest-0140.md | Cron ingest 2026-06-30 0140Z: Another fast pre-filter confirmation. 7 journals, 5 no_signal events, 13 Bug-2 lessons with signal_type=None (key missing), 2 gap backfill. |
| references/session-20260630-decay-check-stale-proposed.md | Decay check 2026-06-30: 15 stale proposed shifts expired (11d, never activated). Proposed-shift TTL pattern — decay check should scan proposed status, not just active. |
| references/noise_lesson_cleanup.md | Guide for cleaning up noise lessons in the Praxis behavioral refinement loop. |
| references/session-20260701-cron-ingest-0735Z.md | Cron ingest 2026-07-01 0735Z: Confirmed last_lesson_extraction_event_id: "" is as broken as null. Phantom finch journal produced unverifiable event. Fast pre-filter vs per-lesson comparison. |
| references/session-20260701-cron-ingest-1012Z.md | Cron ingest 2026-07-01 1012Z: cleanup_noise_lessons.py restored (write_file corruption fixed). Shell heredoc double-Z pitfall documented. |
| references/session-20260701-cron-ingest-1134Z.md | Cron ingest 2026-07-01 1134Z: Routine steady-state. patch corrupts multi-line JSON in ingest_state.json — prefer write_file rewrite. 14 Bug-2 noise lessons cleaned. Decay scan: 3 healthy. |
| references/session-20260712-cron-ingest-0402.md | Cron ingest 2026-07-12 0402Z: Exactly 1 genuine custodian escalation + 2 no_signal mentor events; --new-genuine-events 1 correctly cleared 14 Bug-2 historical lessons because <2 new genuine events cannot ground a lesson. Verified gap-backfill-before-state-update and post-write closure checks. |
What ships with it: 270 files
1062.5 KB alongside SKILL.md, 13 of them executable
assets/
- readme/hero.jpg209.8 KB
evals/
- evals.json1.4 KB
references/
- cron-execution-checklist.md22.0 KB
- data_model.md9.3 KB
- debrief_templates.md2.3 KB
- debrief_workflow.md6.0 KB
- dispatch-ingest.md7.3 KB
- dispatch-quick-path.md7.9 KB
- finch_journal_schema.md2.0 KB
- gotcha_cross_skill_corroboration.md2.1 KB
- gotcha_custodian_findings_schema.md337 B
- gotcha_escalation_fingerprint.md2.3 KB
- gotcha_evidence_field_schema.md1.1 KB
- gotcha_failure_phase_tagging.md552 B
- gotcha_ingest_state_shift_count.md1.0 KB
- gotcha_oauth_corruption.md1.8 KB
- gotchas-praxis.md96.2 KB
- gotcha_unknown_signal_type.md1.7 KB
- ingest-script-pattern.md70.6 KB
- inline-examples.md3.0 KB
- journal_ingestion.md10.1 KB
- journal.md1.9 KB
- journal_sources.md1.8 KB
- lesson_rules.md4.3 KB
- mentor-light-noise-filters.md3.3 KB
- noise_lesson_cleanup.md1.8 KB
- okrs-praxis.md933 B
- praxis-cron-journal-template.md2.0 KB
- praxis-ingest-cli-pitfall.md1.2 KB
- praxis-ingest-directory-filter.md2.6 KB
- production-script-fix-lesson-scoping.md1.5 KB
- recurring-noise-lesson-cleanup.md5.2 KB
- runtime_rules.md2.3 KB
- CHANGELOG.md2.0 KB
- config.json88 B
- config.yaml24 B
- evals.json1.4 KB
- .gitignore77 B
- LICENSE1.0 KB
- README.md6.2 KB
230 more files not listed here. See all 270 in the repository.