<!-- 工具 description 单源（分节加载：registry key = tools#工具名）。
     启动静态类：md 缺失时 registry 抛错（无运行时兜底——缺文件=仓库损坏）。
     内容由迁移前 .ts 字面量程序化提取，基线见 .golden/tools.json；改这里即改工具说明。 -->

## load_memory_rules

Load the L1 record contract (what to record, type enum, priority, call shape) into context. Call this when you decide to record something, BEFORE calling l1_commit. Params: mode="once" (default, cleared after one l1_commit) or mode="always" (permanent for this session).

## skill_manage

Manage skills (create, edit, delete, list). Skills are your procedural memory — reusable approaches for recurring task types. General memory (MEMORY.md / USER.md) is broad and declarative; skills are narrow and actionable.

Create when: complex task succeeded (5+ calls), errors overcome, user-corrected approach worked, non-trivial workflow discovered.
Update when: instructions stale/wrong, missing steps or pitfalls found during use. If you used a skill and hit issues not covered by it, patch it immediately.

New skills go to profiles/<active>/skills/<category>/<name>/SKILL.md. Existing skills can be modified wherever they live.

Pinned skills are protected from deletion only — edit and write_file still work. Unpin first to delete. Bundled skills (system-level) cannot be modified at all.

Good skills include: concrete trigger conditions (When to Use), numbered steps with exact commands, a pitfalls section, and verification steps. Use skill_view() to see format examples.

## skill_view

View the content of a skill's SKILL.md or support files. Use this to inspect a skill before deciding whether to update it.

## l1_commit

Commit L1 memory records. Two modes via 'source':
  - 'subagent' (default): the l1-extractor subagent after extraction/dedup; requires 'records' + 'cursor' (serialized v2 per-shard byte-offset cursor returned by `read_l0_increment`). Cursor advances only after this commit succeeds. Scene attribution goes through the scene pool: put `scene_id` (picked from the ScenePool block in the task message) on records that continue an existing scene, or `new_scene_name` + `new_scene_reason` on records that start a new one. Server-side replaces `scene_name` with the pool's canonical name, registers new scenes, and strips the two intermediate fields before writing.
  - 'main-agent': the main agent proactively records a durable fact mid-session; requires only 'records' (each item needs just {content, type, priority?}). id / timestamps / sessionKey / sessionId / cwd are filled server-side; the L1 cursor is NOT advanced and L2 is NOT enqueued. Prefer a scene_id from list_l1_scenes. An optional scene_name is matched against the pool by name/alias but never registered as a new scene; if both are omitted, the record is unclassified under main-agent-live. Neither mode guesses a scene from cwd. In subagent mode, empty records still commit the supplied v2 cursor without registering a scene.

Project attribution: `cwd` stays the source session's workbench. `metadata.scope` is one of `self|global|unknown`. For an explicit `self` project, pass `metadata.project` as the candidate root and `metadata.projectEvidence: [{message_id, path}]`; each id must be in this record's `source_message_ids`, and each absolute path must occur in that session's L0 message body and lie within the candidate root. Verified per-record evidence takes precedence over the session project (`projectSource: l0-evidence`). If neither candidate nor evidence is supplied, a usable session judgment is retained for compatibility. Invalid explicit evidence never falls back to the session: the record becomes `unknown` with an empty project, and `projectWarnings` reports why. A `self` record without a usable project also becomes `unknown`. `global` and `unknown` always have an empty project. KnownProjects is advisory, not a complete registry.

## list_l1_scenes

List the L1 scene pool — the canonical set of work scenes — so a record can be attributed to an existing scene **by id** instead of an invented name. Call this BEFORE `l1_commit` when recording: pick the `scene_id` whose topic matches the fact you are about to write. If nothing fits, omit the id and pass a stable `scene_name` of the form `{project}｜{topic}` (never encode what you are currently doing into it). Read-only. Returns the same window/limit the extractor sees: `{scenes: [{id, name, cwd, lastUsedAt, useCount}], returned, note}`. Entries absent from the list are either merged away or unused for over 60 days. Omitting both the id and the name is allowed but marks the record as unclassified. NOTE on `cwd`: it is the **session workbench** (the directory pi was launched from), never the project. Project identity lives in the separate `metadata.project` tag on the record, not here.

## l1_project_get

Look up the session's candidate project. For the l1-extractor, query once per distinct L0 sessionKey; never derive it from the subagent context (which reads as "session"). Returns {project, flag, confidence, decidedBy, note?}. Only flag=true makes this candidate usable; confidence measures evidence concentration, not calibrated probability. Judge each memory independently: self means project-specific; global means a general practice or habit with no project; unknown means attribution cannot be established. global/unknown require an empty project. Explicit per-record project attribution must include metadata.projectEvidence for l1_commit to verify against the source L0 messages; valid evidence overrides this session candidate, invalid evidence becomes unknown instead of being assigned here. Do not use cwd to guess a project. Missing/invalid scope is unknown, never self. There are exactly three scope values.

## l1_fix

<!-- vars: fixMinEvidenceChars, fixDebounceHours, fixHighValueHits -->
Fix or remove an L1 memory record found to be wrong (agent-initiated correction). Executes the same in-place semantics as the l1-admin CLI (rewrite = new id + old row tombstoned in place; tombstone = mark deleted). Guards: evidence (>={{fixMinEvidenceChars}} chars, required), audit annotation auto-appended, {{fixDebounceHours}}h debounce per record, high-value protection (hits>={{fixHighValueHits}} or referenced by L2 scene members -> rejected, use '/my-memory:l1:fix --force'). Params: recordId (required), action ('rewrite'|'tombstone'), newContent (required for rewrite), evidence (required), reason (optional), priority (optional 0-100 reassessment for rewrite).

## l1_task_state

Set the lifecycle state of L1 `work_task` records — mark a task completed or abandoned, or reopen it. Call this when a pending task injected in the recall block (`[待办]`) is actually finished (state="done"), or when the user decided not to do it (state="dropped"); use state="open" to reopen. Params: ids (required string[] of L1 record ids — take them from the recall block or from recall_l1_records results), state (required: open|done|dropped), reason (optional short note, stored in metadata.taskStateChange for later review). Semantics: the record is edited IN PLACE — id, content and vector stay unchanged, so the task keeps its recall history and hit stats; it is NOT deleted (deleted means "this record should not exist" — use l1_fix tombstone for that, not this tool). Idempotent: re-marking the same state is a no-op. Partial success: per-id failures (unknown id, already deleted, type != work_task) are reported and the other ids are still applied. The whole call only fails when the L1 write lock is busy (10s timeout) — retry then. Closed tasks stay recallable but are injected with a `[已完成]`/`[已废弃]` tag instead of `[待办]`; set recall.hideClosedTasks to keep them out of recall entirely.

## read_l0_increment

Read L0 conversation increment after a cursor in one call. Called by the l1-extractor subagent. Params: l0Files (comma-separated absolute paths of L0 shards), cursor (serialized v2 per-shard byte-offset cursor; empty for first scan; legacy lastRecordedAt ISO is accepted once and migrates by replaying the equal-timestamp boundary). Returns { messages (up to 25 eligible user/assistant messages in shard/line order, with sourceFile and sourceOffset), background (up to 5 most recent pre-cursor user/assistant rows), nextCursor (v2 cursor covering exactly all scanned rows through the returned batch), truncated }. Each L0 timestamp is event time in epoch milliseconds or null; recordedAt is archive time and must not replace event time. Message IDs are unique within sessionKey, not across sessions.

## read_l1_increment

Read L1 records newer than the L2 cursor in one call (replaces being pointed to a .l2-staging file). Called by the l2-scene-organizer subagent. Params: l1Dir (absolute path of L1 records directory, from RecordsDir in task message), cursor (optional L2 lastRecordId, default read from l2/cursor.json), maxRecords (optional, default 50, hard cap 200). Returns { records (new L1 records with key fields, deleted=true entries preserved for history), nextCursor (last id of returned), totalNew (unfiltered count), returned (this call's count), truncated }.

## l2_commit

Commit scene block writes and advance the L2 cursor. Called by the l2-scene-organizer subagent. Params: sceneWrites (array of {action: create|update|merge|delete|consolidate|archive|restore, filename, sources, reason, meta: {created,updated,summary,heat}, content, members}), cursor (lastRecordId string), personaSignal (optional boolean), personaReason (optional string). CONSOLIDATE is the preferred action for merging blocks: pass sources=[blockA.md,blockB.md,...] plus the merged content — the host backs up the sources, verifies no entry was dropped (rejects the batch when the target body is drastically shorter than the sources, or when an explicit members list misses source members), then writes the target and deletes the sources atomically. ARCHIVE moves ONE block out of the scene directory into the archive layer (action=archive, filename, reason REQUIRED) — it stops counting against the cap and stops appearing in IndexPath, its content and member list are kept intact, and {action:"restore"} brings it back; at most one archive per batch. RESTORE puts an archived block back into the scene directory (it counts against the cap again). NOTE: the total scene-block count has a hard cap (config l2.maxScenes, passed as the MaxScenes line in the task message). If this batch would push the count above the cap, the whole batch is REJECTED and you must first free slots — by consolidating same-project blocks, or by archiving one block whose project is no longer evolving (call l2_archive_guide for the rules and the current facts) — then retry.

## l2_archive_guide

Fetch the L2 archive guide AND the facts for every live scene block, in one call. Call this when the scene directory is at/near the cap (SceneCountWarning = orange/red) and you need to free a slot, or whenever you are considering archiving a block — it is NOT injected by default, so nothing about archiving costs context until you ask for it. Returns: the archive rules (archive vs consolidate, what makes a block a good candidate, the required reason), a fact table per live block (heat, member count, members that appear in the recall history, last recall date, active-days since the block was last written, summary), and — if you pass `filename` — whether that specific block is archivable and, if not, why. The host deliberately does NOT compute a "coldness score" (recall here is automatic injection, so "recently recalled" does not mean "still relevant"); it only hard-blocks blocks younger than the configured age and name conflicts, and YOU decide from the facts, with a written reason. Archiving is reversible.

## l3_edit

Change exactly ONE rule line in the L3 proposal file `USER-temp.md` — NOT the live `USER.md` (that one is the user profile block injected into every turn; the user merges your proposal into it by hand, so nothing you write here is live until they do). One call = one entry: `add` a new rule, `update` an existing one, or `delete` one. The proposal is yours to manage, but NEVER rewrite it wholesale: `content` must be a single line, and the host rejects any text containing a newline, `§`, or an HTML comment. Make one call per rule you want to change, as many calls as needed. The first write of a run seeds the proposal from the live `USER.md` (plus whatever the file already held), so `update`/`delete` resolve against the live rules as well. Params: action ('add'|'update'|'delete', required); content (for add/update: the single rule text); target (for update/delete: a distinctive fragment of the existing entry — the host resolves it against the file and REFUSES the call when 0 or more than 1 entry matches, returning the candidate list so you can pass a longer fragment). After every call the host re-checks the whole-file budget (the CharLimit line in the task message, priced as if merged into `USER.md`) and rejects a change that would push it over — free space by deleting or compressing other entries, then retry. Returns {success, action, position, entryCount, usage}.

## l3_commit

Finish an L3 persona update and advance the L3 cursor. Called by l3-persona-generator as the LAST action of every run — including runs where you decided nothing needed changing (the cursor must still advance, otherwise the same scenes are re-processed next turn). Params: cursor (the newest `updated` ISO timestamp among the scenes you processed). The persona text is NOT passed here, and the live `USER.md` is never written by this tool: every rule change goes through l3_edit into `USER-temp.md`, one entry per call.

## recall_l1_records

<!-- vars: autoInjectTopK -->
Search your L1 atomic memories (durable facts, work methods, tasks, and artifacts extracted from past sessions) by semantic similarity. Use when: (a) no <l1-records> block is present this turn (it is inserted only once recall is ready; a turn that finishes in a single LLM call never gets it), or the block does not cover what you need; (b) the user references something from earlier sessions — past decisions, project conventions, preferences, or previously fixed issues; (c) you need more candidates than the injected top-{{autoInjectTopK}}. Returns top-K records as {recordId, score, content, type, priority, scene_name, createdAt, taskState}. `taskState` (open|done|dropped) is set on work_task records only — when a recalled task is already finished or was abandoned, record that with l1_task_state instead of leaving it pending. Call limit: max 3 times per turn total. If 3 attempts return nothing relevant, the memory is not stored — answer from existing context. Independent of the legacy memory system (jieba inverted index / sub-memories).

## clear_invalid_l1_records

Mark L1 atomic memories as INVALID (agent judged them irrelevant after recall). For each recordId, increments invalid_hits and recomputes effective_hits = hits - invalid_hits in the L1 hit-stats DB (auto-rewrites l1/hit-stats.json consumed by records-viewer). Only records that were actually recalled (hits > 0) are counted; never-recalled ids are skipped. This is a feedback signal ONLY — the record is NOT deleted, its quality drops and recall may still return it. Call this when you recalled a memory via recall_l1_records but found it useless for the current task.

## embed_and_search

Embed query text(s) into vectors, then cosine search top-K similar L1 records. Returns record_id + content + type + priority + score. Used by l1-extractor to find dedup candidates. Prefer queries (non-empty string[], at most 50 per call); split larger batches into chunks instead of querying one memory at a time. Returns results: [{index, queryText, queryVector, candidates}], where index is local to the current chunk (original index = chunk start + index). Keep successful chunks if others fail; use the text fallback only for failed/missing queries. Successful results with empty candidates mean no match. Batch candidates may carry a ~200-character content preview with contentTruncated=true: read the full live record before update/merge/skip decisions. Only copy queryVector into embedding when final content exactly equals queryText; rewritten/merged content must be embedded again by l1_commit. Single queryText is supported for one-off lookups.

## read_scene_blocks

Read several scene blocks in ONE call (the batch replacement for reading scene files one by one). Params: filenames (array of bare .md names taken from the scene index). Returns { blocks: [{filename, ok, meta, content, truncated?, omitted?, error?}], returned, omitted, truncated, totalChars }. Limits: one block is truncated at 24000 chars; a single call returns at most 96000 chars and marks the rest omitted (call again with fewer names, or read that block alone). Called by the l2-scene-organizer subagent — it must use this instead of per-block reads, because every extra tool call costs a full model turn and the whole L2 run has a wall-clock budget.

## read_skill_feed_increment

Read skill-feed (5-role sanitized conversation with tool_call/tool_result) increment after a cursor in one call. Called by the skill-reviewer subagent. Params: feedFiles (comma-separated absolute paths of skill-feed files), cursor (lastRecordedAt ISO string, empty for full scan). Returns { messages (new entries with role/content/tool_name/tool_call_id/is_error, time ascending), background (first 5 as context only, never extract from them), nextCursor, truncated }.
