Migrate 203 test files (356 call sites) from fs.rm/fs.rmSync to
removeWithRetries/removeSyncWithRetries to reduce EBUSY test failures
on Windows. removeWithRetries is now exported from @oh-my-pi/pi-utils.
The migration uses a regex-based approach that:
- Replaces fs.rm(path, { recursive, force }) → removeWithRetries(path)
- Replaces fs.rmSync(path, { recursive, force }) → removeSyncWithRetries(path)
- Replaces fs.rm(path) → removeWithRetries(path) (no options)
- Skips fs.rm/fs.rmSync inside template literals (bun --eval scripts)
- Adds imports to existing @oh-my-pi/pi-utils import or creates new one
- Removes unused fs imports where fs.rm was the only fs usage (4 files)
- agent-loop: raise repetition-detection floor to 180 chars and clear thinking
replay anchors when collapsing a detected loop.
- providers/google: ignore empty text parts, retain terminal thoughtSignatures,
and stop function-call signatures clobbering the prior block.
- autolearn: capture goal-mode at the turn boundary; harden managed-skill writes
against hard-links/symlinks (O_NOFOLLOW + nlink); refuse minting managed skills
whose name an authored skill already claims.
- eager tasks: thread agentKind through the session so a custom top-level agentId
still gets always-mode delegation; split Eager Tasks prompt into hard vs soft.
- title-generator: race the online title model against a local tiny-model fallback.
- eager-todo: keep the soft reminder aligned with the todo init schema.
- mcp/stdio: keep close() detaching the read loop instead of awaiting it.
- stream loop: fix collapsing and tool-call thought-signature handling.
Grill findings on the auto-learn change-set:
- Reject a symlinked managed-skills ROOT (not just the per-skill dir): lstat on a
child follows intermediate components, so a symlinked root would let an
otherwise-valid name write/delete outside the isolated directory. Added
`assertManagedRootSafe()` to create, update, and delete.
- Reject empty content: an all-whitespace/control description sanitizes to "" and
the `requireDescription` discovery scan then silently drops the skill, so the
tool would report success for a skill that never appears. Reject empty
description/body before writing.
- Serialize create/update/delete on the same skill name (`serializeSkillMutation`):
both tools are non-exclusive, so a parallel batch could interleave (e.g. update
observing the file mid-delete). O_EXCL only covered duplicate-create.
Tests: symlinked root/dir/file rejection (create + update + delete), empty
description/body, concurrent create+update ordering, concurrent-create one-winner.
`writeManagedSkill` wrote `<managed-dir>/<name>/SKILL.md` via `Bun.write`,
which follows symlinks. A planted symlink at the `<name>` directory (or the
`SKILL.md` itself) could redirect the write outside the isolated managed root
and clobber a user-authored skill, breaking the isolation guarantee. lstat the
parent directory and the file and reject symlinks before writing; the existence
check now derives from the same lstat instead of a separate `Bun.file().exists()`.
Addresses review thread on PR #2542 (thread 11).
Add a default-off "auto-learn" loop. When `autolearn.enabled` is set, after the
agent stops a session controller nudges it to capture reusable lessons: durable
facts go to long-term memory and repeatable procedures become "managed skills" —
SKILL.md files written to an isolated ~/.omp/agent/managed-skills directory that is
discovered and surfaced like authored skills but never overwrites them.
Two tools back this:
- `manage_skill` — create/update/delete managed skills.
- `learn` — record a lesson, optionally minting/enhancing a managed skill in the
same call (requires a hindsight/mnemopi memory backend).
The nudge is passive by default (a hidden reminder rides the next turn);
`autolearn.autoContinue` instead auto-runs one capture turn at stop, and
`autolearn.minToolCalls` (default 5) gates trivial turns. Plan/goal-mode turns and
subagents are never nudged, and the controller re-checks the live setting at fire
time so a mid-session opt-out takes effect.
Isolation & precedence: managed skills are a separate lowest-priority discovery
provider, so an authored skill of the same name wins across every provider and
custom directory regardless of third-party toggles; a disabled higher-priority
authored skill can never hide a managed one, and managed never masks an enabled
authored skill. Managed names and descriptions are sanitized on both write and
read (control/format chars, angle brackets, and Markdown fences) before they render
into the system prompt, and the SKILL.md byte cap is enforced on the final
serialized file.
Default off → zero footprint when disabled.