Skip to content

fix(data-layer): redact loop event/status/decision outside their allowed sets - #64

Closed
diazMelgarejo wants to merge 176 commits into
codejunkie99:masterfrom
diazMelgarejo:fix/loop-event-export-allowlist
Closed

diazMelgarejo wants to merge 176 commits into
codejunkie99:masterfrom
diazMelgarejo:fix/loop-event-export-allowlist

Conversation

@diazMelgarejo

@diazMelgarejo diazMelgarejo commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

The design promise this closes a gap in

docs/specs/v0.20-agentic-turn.md (this repo's own current spec, currently at master HEAD) states the loop event journal is meant to be content-free by construction: task, prompt, command, and output are excluded from runtime/loops/events.jsonl for exactly this reason, on the theory that two append-only journals under .agent/ with opposite privacy guarantees means the weaker one becomes the leak. .agent/tools/data_layer_export.py already honors that promise for the fields it was built to exclude — test_exports_privacy_safe_loop_events_and_quality_counts asserts a "task" value never makes it into the export.

But the journal's remaining three fields — event, status, decision — were never given the same treatment. They are supposed to be a closed, small vocabulary: harness_manager/loops/runner.py writes exactly 10 distinct event names, 10 status names, and 3 decision names, full stop — there's no code path that emits anything else. normalize_loop_event(), though, treats them as open text: str(entry.get("event") or "loop_event") copies whatever string is there straight into the exported action field, and the status/decision union does the same for result. The write side is a closed enum; the read side trusted it as free-form input. That gap is the one place the content-free-journal guarantee this repo explicitly designed for was left unenforced.

Concretely, this means any row with an event/status/decision value outside the real set — from a future bug in the supervisor, a hand-edited events.jsonl, or a loop kind that doesn't exist yet — flows verbatim into the dashboard/analytics export with zero validation. Nothing catastrophic on its own, but it's exactly the kind of small, structural gap that turns into a real leak the moment something upstream of it goes wrong, which is the whole reason the journal was designed to be content-free in the first place.

The fix

Mirror the actual literals runner.py writes into three module-level sets — VALID_LOOP_EVENTS, VALID_LOOP_STATUSES, VALID_LOOP_DECISIONS — and a small _allowed_or_unknown() helper that redacts anything outside the allowed set to "unknown". That's not a new convention: "unknown" is already the fallback this file uses everywhere else for missing or unrecognized values (pii_level, result, harness, and half a dozen other fields all do the same thing already). This fix just extends the pattern to the two fields that had skipped it.

Tests

Added test_redacts_loop_event_status_and_decision_outside_the_allowed_sets next to the existing loop-event privacy test. Verified in both directions before opening this PR: it fails against the pre-fix code (a <script> tag and a free-text "leaked prompt" string both land in the exported JSONL verbatim), and passes with the fix applied. Full suite: pytest tests/test_data_layer_export.py — 8 passed.

Scope

One file, one function, plus its test. No schema changes, no new dependencies, no behavior change for any well-formed event.

Note

Redact unrecognized loop event, status, and decision values in data layer export

  • Adds allowlists (VALID_LOOP_EVENTS, VALID_LOOP_STATUSES, VALID_LOOP_DECISIONS) in data_layer_export.py to enumerate permissible values for loop fields.
  • normalize_loop_event now passes event, status, and decision values through a new _allowed_or_unknown helper, replacing unrecognized values with 'unknown' instead of copying arbitrary text into the export.
  • Behavioral Change: any loop event/status/decision value not in the allowlists will now appear as 'unknown' in agent-events.jsonl rather than the original string.

Macroscope summarized 5a2724e.

codejunkie99 and others added 30 commits April 16, 2026 16:24
Added a Twitter handle for updates and collaborations.
onboard_ui.py imported tty and termios unconditionally. Both are Unix-only, so the wizard crashed on import under Windows Python before any logic ran. Branch the raw key reader on sys.platform: use msvcrt.getwch on Windows (handling the 0x00 / 0xe0 arrow-key prefix), keep the existing tty/termios path on POSIX.

Add .gitattributes forcing LF on .sh and .py so Git core.autocrlf=true on Windows clones does not rewrite shell script shebangs to CRLF (previously caused /usr/bin/env: 'bash\r': No such file or directory when invoking install.sh from Git Bash or WSL).
fix: Windows compatibility for install.sh and onboarding wizard
Currently the memory system can only surface top-k entries by salience
score, but cannot search by topic or keyword. This adds a lightweight
SQLite FTS5 search tool that indexes all .md and .jsonl files under
.agent/memory/ and returns ranked results with context snippets.

- memory_search.py: FTS5 index with auto-rebuild and grep fallback
- .gitignore: exclude derived .index/ directory
- memory-manager SKILL.md: document the search command

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Written by Minimax-M2.7 in Claude Code Harness.

Shifts the reasoning boundary: Python handles mechanical filing
(cluster, extract, stage, heuristic prefilter, decay, archive).
The host agent (Claude Code, Codex, Cursor, Windsurf) handles
validation via CLI tools using the LLM it already has. The brain
is no longer coupled to a specific provider SDK.

Key modules
- memory/auto_dream.py: staging-only dream cycle. No validation,
  no graduation, no git commit. Safe on Stop hooks or cron.
- memory/validate.py: heuristic prefilter (length + exact
  duplicate against accepted lessons). Deterministic, no LLM.
- memory/review_state.py: candidate lifecycle (staged/provisional/
  accepted/rejected/superseded) + append-only decision log.
  Stamps evidence + lessons hash at decision time so re-stage
  only fires on real change, not churn.
- memory/render_lessons.py: lessons.jsonl as source of truth;
  LESSONS.md rendered from it with sentinel-preserved user
  content. Auto-migrates legacy bullets on first run; dedupes
  by lesson id (handles provisional -> accepted transitions).
- memory/cluster.py: proper single-linkage Jaccard clustering
  with bridge-merge; claim+conditions-stable pattern ids.
- memory/promote.py: re-stage gate keyed on NEW evidence and
  specific-blocker presence, not reviewer identity or whole-file
  hashes. Decay-only evidence shrinkage doesn't churn. Full
  lifecycle respects all three subdirs (staged/rejected/graduated).
- harness/context_budget.py: query-aware retrieval filtered to
  status=accepted only; loads AGENTS.md, DECISIONS.md,
  REVIEW_QUEUE.md per the stated read order.
- harness/llm.py: used only by standalone conductor path; the
  memory layer does not import it.
- harness/text.py: shared word_set + jaccard.
- harness/hooks/_provenance.py: source metadata on every
  episodic entry (confidence, source{skill, run_id, commit_sha},
  evidence_ids).
- tools/{list_candidates,graduate,reject,reopen}.py: host-agent
  CLI. graduate.py requires --rationale and is atomic: lessons
  write first, candidate move last. --supersedes exempts the
  target lesson from the duplicate check.

Workflow notes
- Same pattern across cluster membership changes keeps the same
  id and lifecycle history.
- Accepted lessons are terminal. Provisional acceptances re-enter
  review when new evidence arrives.
- Rejected candidates re-enter review only when evidence grows
  OR the specific blocking lesson disappears.
- Provisional supersessions do not strike the active lesson
  until the superseder is itself accepted.
- Retrieval filters to accepted lessons only; empty filter
  returns empty rather than leaking raw markdown.

Not tracked (stays per-user)
- memory/personal/PREFERENCES.md
- memory/working/* (WORKSPACE.md, REVIEW_QUEUE.md)
- memory/episodic/AGENT_LEARNINGS.jsonl
- memory/candidates/ (transient)
- memory/semantic/lessons.jsonl (grows from graduations)
Written by Minimax-M2.7 in Claude Code Harness.

- [P1] write_candidates now does a claim-level terminal check
  against accepted lessons before the slug-based lifecycle
  lookup. Cluster-membership shifts can change `conditions`
  (intersection of shared tokens) and therefore the pattern id,
  so accepted patterns could re-stage under a new slug and bypass
  the "status=accepted = terminal" guard. Checking by claim text
  is stable regardless of id drift. Provisional and legacy
  lessons remain absent from the terminal set, so they still
  allow re-review.

- [P2] graduate.py now detects a partial-completion retry and
  completes the candidate move without re-appending to
  lessons.jsonl. Previously, if semantic writes succeeded but
  mark_graduated crashed, the next retry saw its own prior
  lesson in LESSONS.md and heuristic_check rejected it as a
  self-duplicate, leaving the candidate stuck. The retry path
  now recognizes the lesson_id already exists and skips
  duplicate-check + append, just finishing the move.
feat: add FTS5 full-text search for memory retrieval
Written by Minimax-M2.7 in Claude Code Harness.

Integrates sergi-rz's FTS5 memory search (#2) behind a beta feature
flag. Default OFF; users opt in through the onboarding wizard or by
editing .agent/memory/.features.json directly. Addresses the three
codex review findings on that PR at the same time.

Beta feature framework
- onboard_features.py (new): read/write .agent/memory/.features.json
  with a simple {key: {enabled: bool, beta: bool}} schema.
- onboard.py: adds a final "Optional features" step to the wizard.
  Default answer is No — beta features stay off unless the user
  explicitly opts in. --yes (non-interactive) path writes the
  features file too, all off.
- memory_search.py gates search/rebuild behind feature_enabled().
  --status still works regardless so users can see the toggle.

Codex review fixes on PR #2
- [P1] needs_rebuild now compares the set of memory files currently
  on disk with the set already in the index; any previously-indexed
  file that no longer exists flags the index stale. Without this,
  deleted/renamed files kept appearing in results until some
  unrelated file bumped the index.
- [P2] search_grep now passes explicit .md/.jsonl target paths to
  grep instead of scanning the whole .agent/memory/ tree. Source
  files (archive.py, auto_dream.py, etc.) no longer pollute
  fallback search results.
- [P3] .gitignore now ignores .agent/memory/.index/ correctly.
  The previous rule was cancelled by the !.agent/memory/**
  negation placed below it; moved the .index/ exclusion AFTER
  the negation so the later rule wins. Verified with
  git status --ignored.
…ETA]

Written by Minimax-M2.7 in Claude Code Harness.

- Bumps VERSION to 0.5.0 in onboard_render.py and the banner.
- README rewritten to cover the host-agent review protocol (CLI
  tools graduate / reject / reopen / list_candidates), the new
  structured lessons.jsonl source of truth, BETA FTS5 memory
  search, and Windows install path.
- Formula bumped to v0.5.0 tarball URL; sha256 placeholder gets
  updated in the follow-up commit once GitHub cuts the archive.
  install list now includes onboard_features.py; brew test
  asserts .features.json is written alongside PREFERENCES.md.
- install.ps1 (new): PowerShell installer parallel to install.sh,
  so Windows users don't need Git Bash / WSL. Same adapters,
  same wizard invocation, same flags (--yes / --reconfigure /
  --force via PowerShell switches).
Written by Minimax-M2.7 in Claude Code Harness.

When SQLite FTS5 is unavailable, memory_search now uses ripgrep (`rg`)
if it's on PATH, falling back to grep only when rg is missing. ripgrep
is faster, UTF-8 clean by default, and respects gitignore. The
.md/.jsonl target restriction from the previous fix is preserved for
both tools — implementation files never reach the search path.

- Adds _fallback_command() that picks rg vs grep based on PATH,
  returns (cmd, tool_name).
- Adds fallback_tool() surfacing the selected tool in --status.
- Renames search_grep to search_fallback; keeps search_grep as a
  backwards-compat alias.
- --status now reports "Mode: FALLBACK (ripgrep|grep|unavailable)"
  and "Fallback available:" in FTS5 mode too.
- README memory_search section mentions ripgrep preference.
Written by Minimax-M2.7 in Claude Code Harness.

Replaces the placeholder with the real SHA256 of the v0.5.0 tarball:
6bec75321979828243fbc437843393d85f068affaed8c3370f0531c6086c19bd

Computed from the GitHub-generated archive. `brew update && brew
upgrade agentic-stack` will now fetch the correct artifact.
Written by Minimax-M2.7 in Claude Code Harness.

Embeds the star-history.com SVG at the end of the README so repo
growth is visible at a glance. Clickable through to the interactive
chart on star-history.com.
Added information about the coding environment and PR review process.
Breaking: .openclient-system.md -> .openclaw-system.md.
Existing OpenClient users need to re-run ./install.sh openclaw.
The portable brain is designed to be mounted by any harness, and many
harnesses (Hermes Agent with `--profile`, others in the list that
support multi-identity) run several isolated agents against a single
brain. Without a profile field on each episodic entry, the dream cycle
clusters across all of them as one stream: a lesson graduated from a
quant-focused agent's trial-and-error can end up rendered as
"agent learned X" alongside a leisure-focused agent's unrelated
discovery. Per-agent retrospectives are impossible.

This adds a `profile` field to the episodic `source` block:

    "source": {
        "skill":      "<what skill fired>",
        "profile":    "<harness profile name>",   # new
        "run_id":     "<run identity>",
        "commit_sha": "<agent git hash>"
    }

Resolution order:
  1. `AGENT_PROFILE` env var (canonical; any harness can export this).
  2. `HERMES_HOME` basename under `/profiles/<name>` (Hermes-specific
     fallback so existing Hermes installs Just Work without touching
     adapter code).
  3. "default" when neither applies.

No breaking changes: consumers that ignore `source["profile"]` see the
same behaviour as before. Cluster.py and promote.py can now partition
by profile when that sharpens the review-queue signal; existing code
that reads `source` as an opaque blob keeps working.

Tested four cases:
- default (no env): "default"
- AGENT_PROFILE set: value used verbatim
- HERMES_HOME under /profiles/fin: "fin"
- HERMES_HOME at base: "default"
harness: tag episodic entries with active profile name

approved by avid live
Three new host-agent tools ship: learn.py (one-shot lesson teaching),
recall.py (proactive lesson retrieval with per-entry source labels), and
show.py (colorful brain-state dashboard). All 8 adapter configs wire
recall into their prompts so every harness can consult graduated lessons
before deploy/migration/timestamp/debug/refactor work.

Reliability pass:
- Unify pattern_id across cluster.py and learn.py paths; canonicalize
  conditions (casefold, unicode whitespace, zero-width strip, dedupe).
- heuristic_check: require ≥3 content words (blocks raw-length-gate
  bypass like "!!!abc").
- graduate.py retry: idempotent re-render, honor original metadata,
  refuse on legacy rows missing reviewer/rationale.
- render_lessons + append_lesson now hold fcntl.LOCK_EX on lessons.jsonl;
  LESSONS.md rewrite is atomic via temp+rename.

Seed UTC lesson ships pre-graduated so proactive-recall returns a hit
on first install.
Updates LICENSE file to canonical Apache 2.0 text, bumps the README
badge + license section, and updates the Homebrew Formula license
identifier to Apache-2.0. Copyright holder unchanged (Avid, 2026).
codejunkie99 and others added 23 commits July 18, 2026 05:47
…kie99#59)

Add a MiniMax provider branch to .agent/harness/llm.py so the portable
harness can call MiniMax directly via its OpenAI- and Anthropic-compatible
endpoints instead of relying on undocumented external SDK behavior.

- Configure global (api.minimax.io) and CN (api.minimaxi.com) regional
  endpoints with both OpenAI and Anthropic base URLs.
- Register current MiniMax-M3 (1,000,000-token context) and MiniMax-M2.7
  (204,800-token context) text models; MiniMax-M3 is the default.
- Select region via AGENT_MINIMAX_REGION and wire via AGENT_MINIMAX_WIRE.
- Document the new env vars in .env.example and the standalone-python
  adapter README.
- Add tests/test_llm_provider.py covering region/model config, provider
  availability, and both OpenAI/Anthropic wires for each region.

Co-authored-by: octo-patch <266937838+octo-patch@users.noreply.github.com>
codejunkie99#57)

* fix(memory): stage() mirrors manual lessons into AGENT_LEARNINGS.jsonl

The manual-stage path in .agent/tools/learn.py writes a candidate with
evidence_ids: [now], promising an episodic record exists at that timestamp —
but never writes one. The auto-derived candidate path mirrors to
AGENT_LEARNINGS.jsonl correctly; the manual path does not. The result is a
referential-integrity gap: a graduated lesson's evidence_ids points at a
timestamp with no matching episodic record.

Fix: add _append_episodic_mirror(), called right after the candidate file is
written, using the same 'now' timestamp so evidence_ids[0] always resolves.
Fail-open on OSError so a mirror-write failure never blocks staging.

Minimal and additive — stdlib only, no new dependencies, no change to the
auto-derived path.

Found and fixed downstream in Perpetua-Tools (which vendors this project);
contributing back per that project's dogfooding practice.

* test(memory): cover the manual-stage episodic mirror

Standalone stdlib unittest (no third-party test dependency) since this
project currently ships no test harness — intended as the first test file.

Kept as a SEPARATE commit from the fix so it can be cherry-picked out if the
maintainer prefers to merge the fix alone. Adapted from the downstream
Perpetua-Tools suite that proved this fix.

Covers: stage() writes exactly one matching episodic mirror; the candidate's
evidence_ids[0] resolves to exactly one episodic record (the regression this
targets); and _append_episodic_mirror fails open on write error.

Run: python3 -m unittest .agent/tools/test_learn_episodic_mirror.py
…e99#61)

Two related Windows-compatibility fixes:

- Reconfigure stdout/stderr to UTF-8 (errors=replace) at import, so a lesson
  claim containing non-ASCII (arrows, em-dashes, accented text) doesn't raise
  UnicodeEncodeError under a cp1252 console on Windows.
- Write the candidate JSON file with explicit encoding="utf-8" rather than
  the platform default, so candidates round-trip identically across OSes.

Both are additive and platform-safe (the reconfigure is guarded by hasattr,
a no-op where unavailable). Found and applied downstream in Perpetua-Tools;
contributing back.

Stacked on top of the episodic-mirror fix (atomic-01).
…e99#62)

* fix(io): force UTF-8 on stdout/stderr and candidate writes

Two related Windows-compatibility fixes:

- Reconfigure stdout/stderr to UTF-8 (errors=replace) at import, so a lesson
  claim containing non-ASCII (arrows, em-dashes, accented text) doesn't raise
  UnicodeEncodeError under a cp1252 console on Windows.
- Write the candidate JSON file with explicit encoding="utf-8" rather than
  the platform default, so candidates round-trip identically across OSes.

Both are additive and platform-safe (the reconfigure is guarded by hasattr,
a no-op where unavailable). Found and applied downstream in Perpetua-Tools;
contributing back.

Stacked on top of the episodic-mirror fix (atomic-01).

* fix(io): use a context manager in _lesson_already_appended

The read-only probe opened lessons.jsonl with a bare open() and relied on
GC to close the handle. Wrap it in a with-statement (and add explicit
encoding="utf-8" for cross-platform consistency) so the file descriptor is
released deterministically. Behavior is otherwise unchanged.

Found and applied downstream in Perpetua-Tools; contributing back.

Stacked on top of atomic-02 (UTF-8 fixes).
…odejunkie99#60)

recall.py's _load_structured() already deduped lessons.jsonl by latest
row per id (handling retraction, which appends a same-id row rather
than editing in place) but had no supersession awareness: supersession
creates a NEW id whose supersedes field points at the old one, and the
old row's own status stays "accepted" forever since supersession never
edits it. Proactive recall could return both the stale and replacement
guidance for the same topic.

render_lessons.py's _build_auto_section already computed exactly this
-- an old-id -> new-id map, accepted-supersessions-only (a provisional
--supersedes must not retire the old lesson before its replacement is
itself accepted). Extracted that computation into a public
superseded_by_map(lessons) function and import it from recall.py,
rather than re-deriving the same rule a second time -- retrieval and
rendering now share one source of truth for "which lessons are
currently retired" instead of two copies that can drift apart.

Tests: tests/test_recall_supersession.py (superseded lesson excluded
from _load_structured, recall() ranks the replacement not the
superseded lesson, provisional supersession does not retire the old
lesson) + a direct unit test on superseded_by_map itself. Full suite:
179 passed, 1 pre-existing unrelated failure (confirmed via git stash
against a clean checkout: test_unknown_executable_is_a_structured_start_failure,
a macOS PermissionError-vs-FileNotFoundError platform quirk, not
touched by this change).
…ejunkie99#63)

_new_loop_assets() computed each new loop-* skill's destination as
dst_skills / src.relative_to(src_agent) -- but src.relative_to(src_agent)
already starts with "skills/" (src_agent is .agent/, not .agent/skills/),
and dst_skills is already .agent/skills/. Result: every genuinely-new
loop-* skill lands at .agent/skills/skills/loop-x/SKILL.md instead of
.agent/skills/loop-x/SKILL.md -- both the CLI's own --dry-run report and
the real copy were wrong, silently, since no existing test exercised a
fresh (non-pre-seeded) loop-* skill destination.

Fix: dst_agent / src.relative_to(src_agent), matching the sibling
non-loop skill-copy block earlier in the same function (line ~84), which
already gets this right.

New test: test_upgrade_copies_missing_loop_skills_to_the_correct_path.
Confirmed it fails on the old code (asserts the doubled path is absent)
and passes on the fix.
Patch release covering codejunkie99#60, codejunkie99#61, codejunkie99#62, codejunkie99#63: superseded-lesson filtering in
recall, the doubled skills/ path in upgrade's loop-skill copy, UTF-8 I/O in
learn.py, and a leaked file handle.

Collapses the stacked per-version README history (v0.9.0 through v0.18.0)
into a single pointer at CHANGELOG.md, and relaxes the onboarding docs test
from an exact version pin to the 0.19.x series so patch releases keep
asserting the loop docs and sandbox caveat without editing the assertion
each time.
Verified sha256 a128f83f... across two independent fetches of the published
tag tarball. The url assertion is now derived from __version__ so a future
bump that forgets the formula fails the suite instead of serving brew users
the previous release.
Retargets the draft from v0.19 to v0.20 and reconciles it with what v0.19.0
actually shipped. The original was written against a v0.18 baseline; the loop
supervisor has since landed several primitives the draft proposed to build
from scratch.

Adds section 0.1 mapping every shipped primitive (owned worktrees, budgets,
deny-path gates, approval gates, stagnation breaker, atomic checkpoints,
content-free event journal, deterministic verification, autonomy tiers) to the
subsystem that must now build on it rather than duplicate it.

Substantive changes beyond renumbering:

- Bus messages drop the inline 4KB payload for a payload_ref path. v0.19
  shipped a field whitelist for the loop event journal that excludes task,
  prompt, command, and output by construction; two append-only journals under
  .agent/ with opposite privacy guarantees means the weaker one becomes the
  leak. Both now share one whitelist helper.
- Speculative execution becomes N loop runs plus a scoreboard instead of a
  second worktree/budget/gate implementation. Renamed .agent/spec/ to
  .agent/trials/ — 'spec' already means the loop contract schema, and
  .agent/spec/ reads as 'the spec for .agent'.
- Background actions become L1 report-only loop contracts; act.py is a
  scheduler and proposal store, not a second supervisor. Carries over the
  v0.19 caveat that the supervisor is not an operating-system sandbox.
- Eval quarantine reuses the recall exclusion path v0.19.1 added for
  superseded lessons, so retrieval keeps one filter chain.
- Migration section is v0.19.x to v0.20; the Python floor is no longer
  restated, since the spec must not silently raise it.

Moves the document from the repo root to docs/specs/ so the root stays
README/CHANGELOG/LICENSE plus entry points.
…wed sets

normalize_loop_event() in .agent/tools/data_layer_export.py copies
entry["event"], entry["status"], and entry["decision"] straight into
the exported "action" and "result" fields with no validation. Those
three keys are supposed to come from a small, supervisor-controlled
finite set -- harness_manager/loops/runner.py only ever writes 10
distinct event names, 10 status names, and 3 decision names to
runtime/loops/events.jsonl -- but nothing enforced that on the export
path, so any row with an unexpected value for those fields (a bug
upstream, a hand-edited events.jsonl, or a future loop kind that
doesn't yet exist) would flow the raw string straight through into
the dashboard/analytics surface.

That matters more here than it would in a generic ETL script: this
repo's own design intent for the loop event journal is that it's
content-free by construction. task/prompt/command/output are excluded
from events.jsonl entirely for exactly this reason -- see the existing
test_exports_privacy_safe_loop_events_and_quality_counts coverage,
which already asserts a "task" field never makes it into the export.
event/status/decision were the one gap in that whitelist discipline:
a closed, small vocabulary in the writer, treated as open text on the
read side.

Add VALID_LOOP_EVENTS/VALID_LOOP_STATUSES/VALID_LOOP_DECISIONS (mirrored
directly from runner.py's own literals) and a small _allowed_or_unknown()
helper. Anything outside the allowed set redacts to "unknown" -- the
same fallback already used everywhere else in this file for missing or
unrecognized values, so this isn't introducing a new convention, just
applying the existing one to a field that was skipped.

Adds one regression test alongside the existing loop-event privacy
test, verified to fail against the pre-fix code (a script tag and a
free-text string both land in the exported JSONL verbatim without the
fix) and pass with it.
@diazMelgarejo

Copy link
Copy Markdown
Contributor Author

Related: #65 (two smaller, unrelated fixes found in the same repo while working on this one).

codejunkie99 pushed a commit that referenced this pull request Sep 26, 2026
README: list all 15 seed skills (brain + four loop skills), add loops/,
Copilot/Pi hooks and tests/ to the repo layout, correct the Windsurf rule
path, move install notes out of the Brain section, and drop the duplicate
changelog section. CHANGELOG: add an Unreleased entry for #64, #65, #67.
@codejunkie99

Copy link
Copy Markdown
Owner

Thanks @diazMelgarejo! master was rewritten after this was opened, so the branch could no longer merge. Your commit landed on current master via #73 (cherry-picked, authorship kept: 74e9fb6). #73 also adds exhausted, failed_to_start, timed_out, phase_started and running to the allowlist; the supervisor writes these, and they were being exported as unknown. Closing as superseded.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.