@staix/agent-mem 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/.claude-plugin/plugin.json +15 -0
  3. package/README.md +186 -21
  4. package/bin/amem.js +26 -3
  5. package/hooks/hooks.json +81 -0
  6. package/package.json +3 -1
  7. package/src/cli.ts +492 -39
  8. package/src/commands-table.ts +56 -15
  9. package/src/hook-contract.ts +3 -0
  10. package/src/hook-git.ts +42 -0
  11. package/src/hook-native.ts +92 -0
  12. package/src/hook-project.ts +206 -0
  13. package/src/hook-resources.ts +15 -0
  14. package/src/hook-spool.ts +566 -0
  15. package/src/hooks.ts +253 -0
  16. package/src/index.ts +18 -0
  17. package/src/ingest.ts +351 -0
  18. package/src/mcp-memory.ts +141 -33
  19. package/src/mcp.ts +42 -7
  20. package/src/memory/aliases.ts +87 -0
  21. package/src/memory/bridge.ts +8 -2
  22. package/src/memory/candidate-pending.ts +119 -0
  23. package/src/memory/candidates.ts +144 -47
  24. package/src/memory/capacity-split.ts +76 -0
  25. package/src/memory/capacity.ts +162 -59
  26. package/src/memory/capture-input.ts +36 -0
  27. package/src/memory/capture.ts +153 -39
  28. package/src/memory/catalog.ts +43 -10
  29. package/src/memory/commands.ts +98 -44
  30. package/src/memory/config.ts +167 -0
  31. package/src/memory/cross-project.ts +75 -0
  32. package/src/memory/curated-read.ts +29 -0
  33. package/src/memory/decay-review.ts +136 -0
  34. package/src/memory/doctor.ts +412 -0
  35. package/src/memory/editor.ts +58 -0
  36. package/src/memory/eval-fixture.ts +137 -0
  37. package/src/memory/eval-set.json +443 -0
  38. package/src/memory/eval.ts +150 -0
  39. package/src/memory/gc.ts +97 -0
  40. package/src/memory/generation-index.ts +22 -17
  41. package/src/memory/generation-sync.ts +3 -0
  42. package/src/memory/goal-promotion.ts +71 -0
  43. package/src/memory/home.ts +15 -4
  44. package/src/memory/intent-brief.ts +89 -0
  45. package/src/memory/lifecycle-log.ts +54 -0
  46. package/src/memory/lifecycle.ts +61 -0
  47. package/src/memory/machine.ts +31 -0
  48. package/src/memory/metrics.ts +63 -0
  49. package/src/memory/mutation.ts +6 -4
  50. package/src/memory/note-add.ts +100 -0
  51. package/src/memory/path-glob.ts +94 -0
  52. package/src/memory/plan-guard.ts +47 -0
  53. package/src/memory/platform.ts +9 -4
  54. package/src/memory/policy.ts +44 -13
  55. package/src/memory/private-spans.ts +32 -0
  56. package/src/memory/project-export.ts +122 -0
  57. package/src/memory/project.ts +129 -20
  58. package/src/memory/reader-lease.ts +22 -10
  59. package/src/memory/recall-staleness.ts +56 -0
  60. package/src/memory/recall.ts +74 -0
  61. package/src/memory/repo-scope.ts +404 -0
  62. package/src/memory/shard-sync-format.ts +116 -0
  63. package/src/memory/shard-sync.ts +421 -0
  64. package/src/memory/shard.ts +3 -80
  65. package/src/memory/store.ts +286 -56
  66. package/src/memory/sync-cadence.ts +139 -0
  67. package/src/memory/sync-screen.ts +86 -0
  68. package/src/memory/sync.ts +79 -11
  69. package/src/memory/usage.ts +35 -0
  70. package/src/memory/use-telemetry.ts +51 -0
  71. package/src/memory/work-lists.ts +115 -38
  72. package/src/session-brief-worker.ts +82 -0
  73. package/src/session-brief.ts +261 -0
  74. package/src/setup-owned.ts +120 -0
  75. package/src/setup-toml.ts +291 -0
  76. package/src/setup.ts +327 -0
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "agent-mem",
3
+ "owner": {
4
+ "name": "STAIxBWLB"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "agent-mem",
9
+ "source": "./",
10
+ "description": "Bounded project memory tools and seven deterministic capture hooks, without a resident worker."
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "agent-mem",
3
+ "version": "0.7.0",
4
+ "description": "Bounded project memory tools and seven deterministic capture hooks, without a resident worker.",
5
+ "mcpServers": {
6
+ "agent-mem": {
7
+ "command": "bun",
8
+ "args": [
9
+ "--smol",
10
+ "${CLAUDE_PLUGIN_ROOT}/bin/amem.js",
11
+ "mcp"
12
+ ]
13
+ }
14
+ }
15
+ }
package/README.md CHANGED
@@ -2,23 +2,63 @@
2
2
 
3
3
  Curated memory with append-only JSONL logs, a derived SQLite index, short- and long-term tiers, a typed graph, goals with leases, and git sync between machines. Extracted from [agent-hub](https://github.com/STAIxBWLB/agent-hub), with compatible storage and entry IDs.
4
4
 
5
- Version: 0.5.0. Requires Bun 1.4.2 or later; no runtime dependencies.
5
+ Version: 0.7.0. Requires Bun 1.4.2 or later; no runtime dependencies.
6
6
 
7
- ## Install
7
+ ## Quick start
8
8
 
9
- Install the public package:
9
+ Install the package, preview setup, then apply it for your client in a person's terminal:
10
10
 
11
11
  ```sh
12
12
  bun add -g @staix/agent-mem
13
13
  amem --version
14
- amem --help
14
+ amem setup all
15
+ amem setup claude --apply
15
16
  ```
16
17
 
18
+ Replace `claude` with `codex`, `kimi` or `pi` for that client. Setup shows its changes and asks for confirmation. Complete the client's own hook-trust review when prompted.
19
+
20
+ Agents submit pending memory candidates through MCP. Inspect them after a session and accept or reject each one:
21
+
22
+ ```sh
23
+ amem review list
24
+ amem review accept CANDIDATE_ID # or: amem review reject CANDIDATE_ID
25
+ ```
26
+
27
+ Replace `CANDIDATE_ID` with an id from the list. Acceptance is person-only and applies the configured privacy policy.
28
+
29
+ Claude's registered hooks capture events during the session. The configured Codex/Kimi SessionEnd hooks and Pi shutdown extension ingest transcript tails incrementally at session end. If those integrations are unavailable, run `amem ingest codex` (or `kimi` / `pi`) after each session, then repeat the review loop. The [setup section](#setup--plugin) describes the registration and native trust steps.
30
+
17
31
  From a checkout, install development dependencies with `bun install` and run `bun src/cli.ts --help`.
18
32
 
33
+ ## Setup / plugin
34
+
35
+ Preview the exact configuration files, native registration commands and private undo receipts before changing anything:
36
+
37
+ ```sh
38
+ amem setup all
39
+ amem setup claude --apply
40
+ amem setup codex --apply
41
+ amem setup kimi --apply
42
+ amem setup pi --apply
43
+ amem setup all --undo
44
+ ```
45
+
46
+ `--apply` and `--undo` require a person's terminal and an explicit `yes`; agent markers refuse them. The default is a read-only plan. Native Claude, Codex and Pi MCP registration commands run in a private temporary HOME before the listed files are published; current Kimi Code uses its MCP JSON file. No AGENTS.md, CLAUDE.md or rule files are edited. Repeated apply is a no-op even after routine CLI metadata writes. Registrations use the absolute Bun path found on PATH, preserving its stable symlink rather than a versioned Cellar target; temporary package entries (such as bunx caches) are refused; reapply refreshes moved package/runtime locations automatically. Private receipts restore original bytes, permissions and absent-file state when files are untouched. When a CLI updates its own metadata, undo restores only the managed MCP entry and hook rows while preserving those updates; edits to those managed entries refuse rather than overwrite. Config files have an 8 MiB bound and generated TOML is validated before publication.
47
+
48
+ Claude receives its seven bounded hooks. Codex and Kimi receive SessionEnd incremental ingest; Pi receives a session_shutdown extension. [Codex's native `/hooks` review](https://learn.chatgpt.com/docs/hooks) remains a pending person step: setup never grants or bypasses hook trust. Native registration requires a CLI exposing `mcp` administration (Claude, Codex and Pi are checked through their help before any registration command); current Kimi Code uses its documented config files. `setup all` skips and reports missing CLIs; an individual missing target refuses. Setup does not download tools or start a resident worker.
49
+
50
+ For a Claude Code plugin installation instead of direct setup registration:
51
+
52
+ ```sh
53
+ claude plugin marketplace add STAIxBWLB/agent-mem
54
+ claude plugin install agent-mem@agent-mem
55
+ ```
56
+
57
+ The plugin packages MCP plus the existing `hooks/hooks.json` once, using Claude's default hook discovery. It needs no `--plugin-dir`. Choose either plugin installation or direct Claude setup to avoid registering the same hooks twice. The direct setup dry run names an installed agent-mem plugin and warns about duplicate MCP and hook registrations.
58
+
19
59
  ## CLI
20
60
 
21
- The flat `amem` commands mirror `ahub memory` arguments; goals use `amem goal`:
61
+ The flat `amem` commands are the memory commands of the library (a host such as agent-hub exposes the same ones under its own name); goals use `amem goal`:
22
62
 
23
63
  ```sh
24
64
  amem list --json
@@ -30,21 +70,45 @@ amem goal list --all --json
30
70
  amem goal new "Ship a release" --milestone "Review changes"
31
71
  ```
32
72
 
33
- `amem goal link` requires the host task boundary; use `ahub goal link` for board links.
73
+ `amem goal link` needs a host that has a task board; amem alone has none, so it refuses.
34
74
 
35
- Memory commands: `list`, `search`, `show`, `graph`, `suggestions`, `confirm`, `reject`, `promote`, `demote`, `forget`, `supersede`, `rebuild`, `dedupe`, `sync`, `machine`, and `import`. Use `--json` where supported by the corresponding `ahub memory` command. `dedupe` previews candidates; `dedupe --apply` appends reconciliation operations. `amem alias add|list` manages person-reviewed project key aliases; `amem capacity` reports and proposes capture bounds (see below).
75
+ Memory commands: `list`, `search`, `show`, `graph`, `suggestions`, `confirm`, `reject`, `promote`, `demote`, `forget`, `supersede`, `rebuild`, `dedupe`, `sync`, `machine`, and `import`. Use `--json` where the command supports it. `dedupe` previews candidates; `dedupe --apply` appends reconciliation operations. `amem alias add|list` manages person-reviewed project key aliases; `amem capacity` reports and proposes capture bounds (see below).
36
76
 
37
- Ordinary `amem` commands are for the person and refuse agent-session markers, including `AGENTHUB_PEER_ID`, `CLAUDECODE`, and `CODEX_THREAD_ID`. `amem mcp` is the sole intentional session agent entrypoint; it supplies scoped reads and bounded candidate/work-list writes, never direct curated-note writes. Hosted agents can also use the hub's `hub_memory_*` read tools. Environment-marker detection is an honest-session guard, not authentication. Ordinary CLI mutations remain person-only and require a TTY on stdin.
77
+ Ordinary `amem` commands are for the person and refuse agent-session markers, including `AGENTHUB_PEER_ID`, `CLAUDECODE`, and `CODEX_THREAD_ID`. `amem mcp`, `amem hook` and `amem ingest` are intentional session entrypoints. MCP supplies scoped reads and bounded candidate/work-list writes; hooks and ingest write capture only. None writes curated notes directly. `amem --help` and `amem --version` only print text, so they also work in an agent shell; every other command is refused there, with a message that points an agent at `amem mcp` and the person at `amem review`. Environment-marker detection is an honest-session guard, not authentication. Ordinary CLI mutations remain person-only and require a TTY on stdin.
38
78
 
39
- This package does not ship the host's privacy policy. Ordinary person reads show stored data without screening; the vault-candidate listing uses its current policy. Import and promotion retain the built-in secret screen. Commands that cross the privacy boundary require the person's explicit `--policy-free` choice:
79
+ ## Configuration and privacy policy
40
80
 
41
- ```sh
42
- amem promote <id> --policy-free
43
- amem import claude-mem --db <path> --policy-free
44
- amem sync --remote <private-git-url> --policy-free
81
+ amem applies one privacy policy everywhere it reads or writes on its own: capture (hooks and `amem ingest`), the MCP server, the SessionStart brief, `amem review accept`, `promote`, `import`, `sync`, repo promotion and the vault-candidate listing. The policy is the built-in secret and personal-data screen plus what you configure. Ordinary person reads (`list`, `search`, `show`) show stored data without screening.
82
+
83
+ The machine-local file `amem.config.json` in the memory home (see `amem status` for where that is) holds your own policy:
84
+
85
+ ```json
86
+ {
87
+ "denyPaths": ["confidential-vault", "customers/"],
88
+ "piiPatterns": ["codename-zebra", "acme corp"],
89
+ "briefBytes": 900,
90
+ "excerptBytes": 800,
91
+ "capturePaths": 16,
92
+ "capture": { "capMb": 256, "retentionDays": 30 }
93
+ }
45
94
  ```
46
95
 
47
- Use `ahub memory` for the policy-applying path. `--policy-free` does not bypass the secret screen or the sync remote's privacy checks. Forgetting a shared note appends an operation; its previous content remains in the remote's git history.
96
+ - `denyPaths`: substrings of a path that are never captured and that decline any note, candidate or MCP answer naming them. `piiPatterns`: case-insensitive literal substrings (never regular expressions) that decline a value and redact a capture excerpt. Each list holds up to 64 entries of up to 256 bytes.
97
+ - `briefBytes`, `excerptBytes`, `capturePaths` can only lower the built-in ceilings (the 1200-byte SessionStart context, 2000-character capture excerpt, 32 captured paths). `capture.capMb` and `capture.retentionDays` are the lowest layer of the capture size and retention bounds: a shard's own `config.json` and the home's `capture-config.json` still override them.
98
+ - A file with an unknown key, a bad value, a link, or more than 64 KiB is an error that names the file. An unreadable policy is never treated as no policy.
99
+ - `<private>...</private>` spans are dropped before anything else reads the text, at every write boundary (capture, MCP candidates and work lists, notes, goals, promotion and import), so they are never screened, counted or stored. Tags match like HTML (case-insensitive, with attributes or spaces, and `<private/>` opens a span); an unclosed span drops the rest of the text.
100
+
101
+ A repository may add `.amem/config` (same shape, only `denyPaths`, `piiPatterns`, `briefBytes`, `excerptBytes` and `capturePaths`). It can only narrow: its lists are added to yours and its budgets count only when lower. It cannot remove an entry, raise a budget, move the home or change the capture bounds, and any other key is an error (a malformed repository file stops amem in that repository until fixed; it never falls back to no policy). A stricter policy a host passes through the library always wins over both files.
102
+
103
+ `amem sync` reads this machine's own long-term notes through the configured policy before it stages anything. One declined note refuses the whole sync and names its id (never its text); the log is append-only and sync never skips a row. It does not read other machines' segments, which were screened where they were written.
104
+
105
+ `--policy-free` is still accepted by `promote`, `import`, `sync` and `review accept` so existing scripts keep working, and it means nothing more: the configured policy always applies. It does not bypass the secret screen or the sync remote's privacy checks. Forgetting a shared note appends an operation; its previous content remains in the remote's git history.
106
+
107
+ ## Writing notes and reviewing candidates
108
+
109
+ `amem note add [--kind decision|finding|contract|fail] [--tier short|long] [--scope project|shared] [--title T] [text... | -]` writes one curated note as the person (agent-mem#40 F6). The text is the arguments, `-` to read it from the terminal until end of input, or nothing to open `$VISUAL` (an empty one is skipped) or `$EDITOR` on a private temporary file (the command is run without a shell: an existing path with spaces works as is, otherwise quotes and backslashes split it, and Ctrl-C reaches the editor while it runs). `-` reads the text you type on the terminal until end of input: `note add` needs a terminal on stdin, so it cannot read a pipe, and that differs from a literal reading of "from stdin" in the issue. A `fail` note takes its five fields instead of free text: `--approach`, `--symptom`, `--cause`, `--evidence` (repeatable) and `--retry-when`. All five are required, and the command names the ones that are missing; with none given it opens a labelled template in the editor. The configured policy screens the title and text first, and a refusal never echoes them. Like every ordinary command it needs a terminal and refuses an agent session.
110
+
111
+ The SessionStart brief carries one line when candidates wait, for example `2 candidates pending, oldest expires in 27 days` (agent-mem#40 F7). It holds numbers only, is read-only (the hook never marks anything shown), comes out of the brief's own byte budget, and is left out when it does not fit. `amem status` adds a `candidates` object (or `unavailable` when `candidates.db` exists but cannot be read, never a false zero) and a warning when any is within 7 days of expiry. `amem review walk` needs a terminal on both stdin and stdout (it refuses when stdout is redirected to a file or a pipe, because nobody would see what it asks about). It prints each pending candidate, oldest first, and only then asks accept, edit, reject, skip or quit: accept goes through the normal policy, edit opens the text in your editor and writes it as your own note (a candidate proposed as a fail note opens the labelled template), and what it prints has control characters removed. Nothing expires unseen: a candidate counts as shown only when `review walk` has printed it (the plain `review list` prints no content and runs in scripts, and the brief line cannot know you read it, so neither marks anything). A candidate is kept past its 30 days until it has been shown, and then for 7 more days. When 500 unseen candidates wait, a new one is refused with a named error (an MCP tool reports a fixed "the review queue is full, ask the person to run `amem review`" reply) instead of dropping an unseen one.
48
112
 
49
113
  ## Candidate review
50
114
 
@@ -57,11 +121,11 @@ amem review accept <full-candidate-id> --policy-free --json
57
121
  amem review reject <full-candidate-id> --json
58
122
  ```
59
123
 
60
- All review commands remain person-only; acceptance and rejection require a terminal on stdin. Acceptance additionally requires `--policy-free`, acknowledging that the standalone CLI cannot apply a host's privacy policy; use `ahub memory` for the host policy path. The CLI refuses secret-looking candidate input or proposed note content. Listing does not open the ordinary memory index, and acceptance defers unrelated held-note promotions.
124
+ All review commands remain person-only; acceptance and rejection require a terminal on stdin. Acceptance screens the candidate and the proposed note with the configured policy (`--policy-free` is still accepted and ignored). The CLI refuses secret-looking candidate input or proposed note content. Listing does not open the ordinary memory index, and acceptance defers unrelated held-note promotions.
61
125
 
62
126
  Candidates live in `candidates.db` beside the memory logs. This sidecar is machine-local: it is never synced, imported, or read by recall, briefs, ask or graph evidence. Hosts must supply an explicit public privacy verdict before putting candidates or counters; private, PII and unknown verdicts are refused. Library APIs are available, but this phase adds no automatic candidate producer or telemetry consumer. Candidate retention is 500 per project and 30 days; counters retain 10,000 per project and 90 days. Candidate payloads are capped at 16 KiB.
63
127
 
64
- Acceptance creates ordinary v1 memory data. A correction retains its target's scope, project and storage reach, writes the corrected note and applies the existing `supersede` operation; its exact current target revision must match. A local target stays short-term; a synced target stays synced, including a demoted target whose effective tier is short. A fail becomes a long-term project note. The accepted text binds the candidate ID, target revision and target ID; a fail proposal without a target records `none` for both target fields. Both paths require the actual MemoryStore promotion screen to return `promote` at acceptance time; `keep`, `skip` or an absent screen refuses acceptance without falling back to short-term storage. Accepted v1 notes and operations follow the existing storage and sync rules; the candidate/counter sidecar remains local and outside evidence. No new v1 operation is introduced. Decay candidates are listing-only and cannot be accepted; a person may separately choose `ahub memory demote` or `forget`. The sidecar receipt, note append and correction's supersede append are separate durable writes. An interrupted acceptance can leave a note without its supersede; it is held for explicit inspection and never replayed automatically.
128
+ Acceptance creates ordinary v1 memory data. A correction retains its target's scope, project and storage reach, writes the corrected note and applies the existing `supersede` operation; its exact current target revision must match. A local target stays short-term; a synced target stays synced, including a demoted target whose effective tier is short. A fail becomes a long-term project note. The accepted text binds the candidate ID, target revision and target ID; a fail proposal without a target records `none` for both target fields. Both paths require the actual MemoryStore promotion screen to return `promote` at acceptance time; `keep`, `skip` or an absent screen refuses acceptance without falling back to short-term storage. Accepted v1 notes and operations follow the existing storage and sync rules; the candidate/counter sidecar remains local and outside evidence. No new v1 operation is introduced. Decay candidates are listing-only and cannot be accepted; a person may separately choose `amem demote` or `amem forget`. The sidecar receipt, note append and correction's supersede append are separate durable writes. An interrupted acceptance can leave a note without its supersede; it is held for explicit inspection and never replayed automatically.
65
129
 
66
130
  The library entry exports `CandidateStore({ home?, now? })`, `put(input, verdict)`, `recordCounter(key, values, verdict)`, and `accept(id, { project, actor, verdict, store, policy })`. `home` retains the parent-home convention (`<home>/memory`); the default uses `memoryHome()`. The host must authenticate person intent, bind the current project and privacy verdict, and supply a current acceptance policy plus the ordinary store's actual promotion screen. An `actor` string or caller-supplied public verdict alone is not authentication. Counter writes are idempotent snapshots for the same contextual key, not increments: observed `0` stays zero, while `null` means unknown (a later unknown value does not erase an observed value).
67
131
 
@@ -127,22 +191,115 @@ These interactive checks remain unrun until a person checks them in each install
127
191
 
128
192
  ## Data directory
129
193
 
130
- `memoryHome()` selects `AMEM_HOME` first, otherwise `$AGENTHUB_HOME/memory`, otherwise `~/.agenthub/memory`. `AMEM_HOME` names the memory directory itself; `AGENTHUB_HOME` names its parent. Existing agent-hub memory directories need no migration. An explicit `StoreOptions.home` retains the parent-directory convention and appends `/memory`.
194
+ `memoryHome()` selects `AMEM_HOME` first; then the `home` of a pointer file `amem.config.json` in the default home (a file with `home` carries nothing else, and a redirected home's own file cannot redirect again); then `$AGENTHUB_HOME/memory`, otherwise `~/.agenthub/memory`. `amem status` prints the home in use, which of these chose it, and the configuration files in effect. `AMEM_HOME` names the memory directory itself; `AGENTHUB_HOME` names its parent. Existing agent-hub memory directories need no migration. An explicit `StoreOptions.home` retains the parent-directory convention and appends `/memory`.
131
195
 
132
196
  The JSONL segments are authoritative. `index.db` is derived and can be rebuilt. Entries are immutable; changes append operations. The compatibility schema remains `agent-hub.memory/v1` because it participates in entry IDs.
133
197
 
134
198
  ## Capture tier and full-text search
135
199
 
136
- From issue #13 (phase P1), the package carries a second, machine-local tier beside the curated store, born sharded (#15 T1): one capture shard per project key at `projects/<key-hash>/machine/<machine-id>/` under the memory home, with its own locks so two projects never contend. The capture tier records deterministic tool-event rows (project, agent, event, tool, paths, exit status and a bounded, secret-screened excerpt) in the shard's `capture.db`. It is never synced, never read as curated evidence, and never writes a curated log, so its maintenance can never evict notes, goals or work lists. Capture is bounded by a soft size cap and a retention window (defaults 512 MB per shard, 90 days), resolved at every invocation from the library options, then the shard's `config.json`, then the home default `capture-config.json`; eviction removes the oldest capture rows first in bounded batches. The 2026-10-17 owner decision keeps the P1 cap per shard; the machine-wide total is #15 T5. On lock contention a writer appends to an append-only spool and exits fast; the next uncontended writer replays the spool idempotently. The capture path makes no network or model call. The pre-T1 machine-wide `capture/` (an unreleased layout of this branch) is migrated per project on open and removed once empty.
200
+ From issue #13 (phase P1), the package carries a second, machine-local tier beside the curated store, born sharded (#15 T1): one capture shard per project key at `projects/<key-hash>/machine/<machine-id>/` under the memory home, with its own locks so two projects never contend. The capture tier records deterministic tool-event rows (project, agent, event, tool, paths, exit status and a bounded, secret-screened excerpt) in the shard's `capture.db`. It is never synced, never read as curated evidence, and never writes a curated log, so its maintenance can never evict notes, goals or work lists. Capture is bounded by a soft size cap and a retention window (defaults 512 MB per shard, 90 days), resolved at every invocation from the library options, then the shard's `config.json`, then the home default `capture-config.json`; eviction removes the oldest capture rows first in bounded batches. The 2026-10-17 owner decision keeps the P1 cap per shard; the machine-wide total is #15 T5. On lock contention a writer publishes a complete immutable per-writer spool segment and exits fast; the next uncontended writer replays the spool idempotently. The capture path makes no network or model call. The pre-T1 machine-wide `capture/` (an unreleased layout of this branch) is migrated per project on open and removed once empty.
137
201
 
138
202
  Project identity (#15 D2) keeps `memoryProjectKey`: a worktree, a submodule and the main checkout of one repository share its key; two unrelated same-named repositories without a remote get distinct path-hash keys, flagged `local` in the shard's `project.json` and the catalog. A no-remote key hashes the main working root (using the actual top-level for a separate git dir), so keys written from the repository root keep their pre-#15 value. Known limit: a linked worktree of a no-remote separate-git-dir repository gets its own key, because git does not record that repository's main working root (probe: `alpha` and `alpha/sub` share `alpha@<hash>`, `alpha-wt` gets `alpha-wt@<hash>`); the key stays as is for root-key compatibility, and a reviewed alias reconciles its capture. Keys written by older builds from a subdirectory or worktree still used that invocation's cwd hash; a person-reviewed alias maps such a key to its canonical one for capture, work lists and the CLI and MCP project binding. Curated rows written under the old key keep their key in the log (nothing is rewritten), and the CLI and MCP read them under the canonical project: the project filter of notes, goals, candidates, vault candidates and work lists covers the canonical key and every key aliased to it, and presents those rows under the canonical key (#27). The library `MemoryStore` still reads by the raw key unless a host passes `reconcile`, and shared rows stay visible as before. Writes still go to the key's own log and shard, and a settle or forget of an aliased row lands in the shard that holds it. `amem alias add <from-key> <to-key>` appends an op to `aliases.jsonl` (latest wins, never chained, never automatic), `amem alias remove <from-key>` appends a removal (the old key reads under itself again, the history stays), `amem alias list` shows the ops including removals. Both are person-only and need a terminal. An amem or hub older than this change ignores a removal and keeps the old binding until it is upgraded, so upgrade every host that shares the home first. A correction accepted on an aliased row lives under the canonical key, so after a removal the old key shows the superseded note again (see `docs/contracts.md`). `catalog.db` in the memory home (#15 D3's storage part) is derived only from the shards and the alias log and rebuilds to the same rows; the D3 management commands are #15 T5 and not built.
139
203
 
140
204
  `store.search(query, filter)` searches the curated tier and the current project's capture shard with SQLite FTS5 and BM25 plus tier/recency weighting: natural multi-word queries match partial term sets (the substring filter required every word), and a curated row outranks the same text in the capture tier. Prefix terms match inflections and Korean particles in either direction by retaining the original query term and adding a common-particle-stripped alternative. The FTS copy of the curated rows lives in its own derived `search.db` (`index.db`'s table set is a frozen contract and gains nothing); it is written from the same apply path as the index, and a prune, crash or rebuild is detected and resynced, so the incremental FTS copy equals a full resync. Where FTS5 is unavailable the engine falls back to the substring filter and reports `engine: "substring"`; capture recall needs FTS5 and is skipped in the fallback. The library entry exports `CaptureStore`, `captureSearch`, `readCaptureConfig`, `writeCaptureConfig`, `SearchIndex`, `fts5Available`, the shard helpers (`resolveAlias`, `appendAlias`, `listAliases`, `shardKeyHash`), the catalog (`rebuildCatalog`, `readCatalog`, `scanCatalog`) and the capacity advisor (`capacityReport`).
141
205
 
142
- The R7 capacity advisor reports this machine's load and proposes capture bounds: `amem capacity [--json]` shows capture size and growth, disk, memory, the rolling `metrics.jsonl` readings and the proposal with its binding constraint; `amem capacity apply [--cap-mb N] [--retention-days N]` saves a preview, and only a matching later `--confirm` writes the home default config. Applying is person-only (agent markers refused, a terminal required); an agent may read the proposal through the library, never apply it. The 2026-10-17 owner decision keeps P1 shrink-only; growth proposals are a follow-up after the agent-hub release, while explicit person-directed growth remains available through `apply --cap-mb N`. Benchmark samples stay in the measuring script's synthetic home; only real hook and search invocations feed the advisor's rolling metrics.
206
+ `amem eval [--set <file>] [--k 1,5,10] [--json]` measures recall with no model and no network (agent-mem#40 F13): it replays a question set and reports recall@k, hit@k and MRR. It scores the ranked `store.search` (BM25 with tier and recency weights), which is what the MCP `amem_search` tool and hub recall rank with and what #13 AC4 measured. It is not the `amem search` command: that one is a substring filter (every word must appear, newest first), so `amem search lighthouse beacon` can print nothing for a note that `amem eval` finds. Whether `amem search` should use the ranked search is a separate question. Without `--set` it scores the built-in synthetic set (26 invented harbour entries, long and short tier, written a day apart, and 32 questions, shipped in `src/memory/eval-set.json`), which is seeded into a throwaway store, so it never reads or writes your memory home and gives the same numbers on every machine. A set of your own either carries its own `entries` (self-contained, named by `key`) or, without entries, names real entry ids (or a prefix of 8 or more hex characters) and is scored against this project's store; each name is resolved like `amem show` does (a promoted short-term id finds its long-term copy, and a superseded id stands for the entry that replaced it, since search returns only live entries), and a prefix that matches more than one live entry is refused. `mem:<id>` as a brief prints it is accepted. Questions whose expected entry is not in the store score 0 in `all` and are left out of `answerable`, and `answerable` is scored over the entries the store holds, so a ranking problem and a coverage problem stay apart (`n/a` when no question is answerable). Set format: `docs/contracts.md#recall-evaluation-f13`.
207
+
208
+ The R7 capacity advisor reports this machine's load and proposes capture bounds: `amem capacity [--json]` shows capture size and growth, disk, memory, the rolling `metrics.jsonl` readings and the proposal with its binding constraint; `amem capacity apply [--cap-mb N] [--retention-days N]` saves a preview, and only a matching later `--confirm` writes the home default config. Applying is person-only (agent markers refused, a terminal required); an agent may read the proposal through the library, never apply it. Under the 2026-10-17 owner decision (item 2) the proposal is the largest cap and retention that fit, and it may be above the current values: the cap is the disk-share bound itself (default 2% of free space, `--disk-share`), and retention is the share divided by the measured daily growth, up to a 365-day ceiling (a `ponytail:` constant, a person setting is the upgrade path). A hook p95 over 150 ms or hook RSS over 100 MB halves the proposed cap, and search RSS over 200 MB halves the retention. The reason names every binding constraint: `disk-share retention` or `retention ceiling`, `disk-share cap`, `R3 search RSS`, `R2 hook cost`. A proposal is never written by the advisor, and `apply` still takes `--cap-mb` and `--retention-days` to override it. Benchmark samples stay in the measuring script's synthetic home; only real hook and search invocations feed the advisor's rolling metrics.
209
+
210
+ ## Repo scope (#15 T4)
211
+
212
+ Team-shareable decisions, contracts and `fail` lessons can live in the repository itself, in `.amem/log.jsonl`, and travel with git. Nothing enters it by itself.
213
+
214
+ ```sh
215
+ amem promote <id> --to repo --policy-free # preview: the exact line that would be committed, and the repository's visibility
216
+ amem promote <id> --to repo --policy-free --confirm # append it (private repository)
217
+ amem promote <id> --to repo --policy-free --confirm --confirm-public <id prefix> # public or unverified repository: per entry
218
+ amem repo status # the file, its entries, what the screen withheld
219
+ amem repo attributes [--confirm] # add `.amem/log.jsonl merge=union` to .gitattributes, only when you ask
220
+ ```
221
+
222
+ It is person-only and needs a terminal. The entry is screened (secrets, personal data, private paths, the repository's own `local.deny` and `pii.paths` with agent-hub's glob rules), and amem never commits or pushes: you review and commit the file. A repository counts as private only when every remote's effective URL (after `insteadOf`) is a `github.com` repository that `gh` reports private; any other host is unknown and asks for the per-entry confirmation. The file is read as untrusted input: the newest 1 MiB, no control characters, a future date shown as now. Recall, brief and MCP search include the current repository's `.amem/` when that repository is the project, never another project's.
223
+
224
+ ## Managing the project shards (#15 T5)
225
+
226
+ One catalog view across every project shard of the memory home, person-only for anything that changes state (an agent marker is refused, and a mutation needs a terminal on stdin):
227
+
228
+ ```sh
229
+ amem projects [--all] [--json] # key, state, aliases, capture and curated sizes, disk use; forgotten ones only with --all
230
+ amem usage [--limit N] [--all] [--json] # totals and the largest shards
231
+ amem gc [--confirm] [--batches N] # each shard's own retention and cap, in bounded batches; a preview without --confirm
232
+ amem project export <key> --to <file> # one project's capture, notes, goals and work lists as one new 0600 bundle (never overwrites)
233
+ amem project archive|unarchive <key> # take a cold project out of recall, or bring it back (append-only ops)
234
+ amem project forget <key> [--confirm] # append-only record that takes one project out of recall and every listing for good; preview first
235
+ amem search --all-projects <words> # the reply begins "crossed projects" and every hit names its project; --include-archived reads cold ones
236
+ amem vault-candidates [--all-projects | --project <key>]
237
+ amem capacity apply --split [--confirm] # each shard's share of the global capture budget, written to its own config after a preview
238
+ ```
239
+
240
+ - `archive`, `unarchive` and `forget` are append-only ops in `lifecycle.jsonl` of the memory home (local, never synced, like `aliases.jsonl`): nothing rewrites a line and nothing deletes a shard. The latest op wins and `forgotten` is terminal. An alias resolves to its one canonical project, and the reply names it. An archived or forgotten project has no writable capture store, no SessionStart brief and no MCP server; `unarchive` restores an archived one. A forgotten project's bytes stay on disk: `forget` is the person's record, not an erase. An alias shard of an archived or forgotten project counts as that state, `alias add` to or from a forgotten key is refused, and hooks that staged capture for a project before it was archived or forgotten are retired at the next drain (counted as `project_archived` or `project_forgotten`). `openShardStore` refuses such a project at the open, so a library caller such as agent-hub is gated too once it pins this release.
241
+ - `gc` opens only the capture store of each shard and calls its bounded `evict()` (at most four batches of 256 rows and 25 ms per call), so a cap shrink never takes a shard in one pass: the report says `remainingOverCap` and the next run continues. It never opens a curated store, a goal or a work list. An archived shard is still bounded; a forgotten one is skipped and listed as skipped.
242
+ - `export` changes no content under the memory home: it opens the capture databases read-only and reads the curated store, and the only files that may appear or refresh are derived (the curated `index.db` and `search.db`, SQLite sidecars and `catalog.db`), which every reader rebuilds from the logs. A destination inside the home is refused. The bundle is screened like sync and promotion are, so a secret-looking value, a PII pattern or a denied path is withheld and counted in `withheld`.
243
+ - `search --all-projects` runs one ordinary project-scoped search per active project and merges by score. It is a person command: the MCP server stays bound to one project and has no all-projects argument, and the raw memory command (what a host exposes as its own `memory` command) refuses the flag.
244
+ - `capacity` also prints a per-shard split of the global disk-share budget. Each shard's weight halves every 14 days since its last capture, so recently used projects keep more capture and cold ones shrink first; the split never exceeds the budget, gives every shard at least 1 MB, keeps every shard at what it holds whenever the total fits, never proposes a retention shorter than a shard's idle time plus 30 days, and says when the budget forces a shard below what it holds. When the budget holds fewer whole MB than there are shards, no split is proposed and `capacity` says why (`splitRefused`). It proposes capture bounds only, and never evicts curated notes, goals or work lists. `capacity apply --split` previews, then `--confirm` writes exactly the previewed values to each shard's `config.json`, and only if the preview is under an hour old and the set of shards and every shard's config are unchanged since then (a `--disk-share` given only to `--confirm` changes nothing that is written).
245
+ - amem lists vault candidates and never writes the vault.
143
246
 
144
247
  Resource budgets are acceptance criteria, measured by `bun scripts/measure-budget.ts` in CI on Ubuntu and macOS: a hook-like append (including the spool-and-exit path under contention) must stay under 150 ms p95 wall time and 100 MB peak RSS (R2), the search path under 100 MB idle and 200 MB peak RSS (R3), and no amem process may remain after exit (R1). The script prints a machine-readable verdict per budget and its CI log is also the FTS5 availability evidence for Linux (R5). The AC4 retrieval re-check of the frozen P0 query set is in [docs/p1-ac4-recheck.md](https://github.com/STAIxBWLB/agent-mem/blob/main/docs/p1-ac4-recheck.md).
145
248
 
249
+ ## Claude Code hooks and SessionStart context
250
+
251
+ The package includes a local Claude Code plugin: `.claude-plugin/plugin.json` and `hooks/hooks.json`. Install the package as above, then explicitly load its package root in a person's Claude Code session. A checkout can be loaded directly:
252
+
253
+ ```sh
254
+ claude --plugin-dir /absolute/path/to/agent-mem
255
+ ```
256
+
257
+ Use the directory containing `.claude-plugin/`, `hooks/` and `bin/`, not the global executable directory. These are setup instructions, not an installation performed by this repository. Package installation alone does not edit Claude settings, register the plugin or disable any existing memory integration. Check for duplicate capture hooks before opting in; the separate P5 cutover owns changes to existing integrations.
258
+
259
+ The packaged executable uses `#!/usr/bin/env -S bun --smol`, and plugin commands explicitly pass `bun --smol`. An explicit `bun bin/amem.js hook ...` invocation without that flag replaces its bootstrap with `execve` before loading hook stores/policy. That fallback keeps the same PID, stdin, preload arguments and original wall deadline; it does not add a third Bun process. Process helpers load on demand, and machine identity lives in a small shared leaf, avoiding eager curated-store loading in the hook parent/work-list path.
260
+
261
+ The plugin observes seven events: `SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `Stop` and `SessionEnd`. They are deterministic capture/context hooks, never permission hooks: they cannot deny a tool call, replace its arguments or turn a capture failure into a native failure. Input is capped at 64 KiB. Malformed, private, secret-looking, denied-path or unavailable input is skipped, and hook errors exit successfully without a native decision. All seven events may append screened rows to the current project's machine-local capture shard; only SessionStart adds context.
262
+
263
+ The standalone brief prioritizes open work lists, recent curated notes and then recent capture; native SessionStart uses the narrower deadline-safe sources described below. The context starts with an untrusted-data warning and per-section `complete`, `partial` or `unavailable` coverage, followed by JSON records. Capture records carry `evidence: false`. The default context bound is 1,200 UTF-8 bytes, counted conservatively as a token upper bound, including the header. Unicode-safe display limits flatten control/line separators; memory text cannot introduce a new header or instruction frame. Full rows and composed work views pass standalone and any stricter host policy before selection; displayed projections are screened again. Canonical project scope includes reviewed aliases for curated notes and work lists, excludes foreign shared-project rows, and is rechecked with profile/selector bindings after asynchronous policy calls.
264
+
265
+ Before optional work starts, the parent screens the complete hook input and bounded normalized capture fields, then closes and atomically publishes a private immutable record in `<memory-home>/hook-spool/`. It retains normalized capture data and a physical-cwd/scope stamp, never the raw native envelope or native argument vector. Non-SessionStart events use the fixed Bun `--smol` worker in a short-lived owned process group. The worker resolves the current project, rechecks scope, drains by a stable receipt, and acknowledges only after append or capture-spool publication. The parent reaps it, and it has its own deadline. SessionStart computes context in the already-running CLI, avoiding a second Bun boot. Its native metadata commands have owned shell supervisors with the same remaining deadline, an armed self-watchdog and group cleanup even if the CLI dies. A timeout leaves accepted capture pending for later replay; unresolved pending records never enter context.
266
+
267
+ Pending capture has an ordinary soft tier of 256 files/2 MiB and a separate private reserve of 32 files/256 KiB, admitted before optional recovery. Replay is globally oldest-first across both tiers, at most 32 files/256 KiB per drain. A record replays only when its original non-null scope positively matches. Null scope is never adopted later, even with a physical cwd receipt: protecting project privacy takes priority over recovering an unattested event. Alias/profile/shard changes invalidate the derived identity cache and reader binding without changing pending scope. Positive fresh native project identity may still supply known-work context when pending scope is unknown; it does not authorize replay of those records.
268
+
269
+ A valid pending body is eligible for retirement only after at least seven days and current evidence that its scope is unknown/mismatched or its original cwd is positively absent. A transiently removed or unmounted cwd younger than seven days keeps its data. A positively drainable foreign-project record is retained even when older than seven days; another project's admission does not make it undrainable. Retirement removes the body only after persisting a reason/count metric (`captureDrops` and `captureDropReason`, with `cwd_removed`, `undrainable_expired` or `scope_unknown`); failed counter persistence leaves the body pending. No retired body becomes a replay/context source. Malformed publications can be removed through bounded owned-file checks. Explicit current-policy rejection also retains only a numeric private receipt. This bounded loss policy prevents permanently undrainable records from disabling capture, while never rebinding them to a new project.
270
+
271
+ If both replayable tiers fill, or SessionStart's reserve alone fills, the current event can be dropped. The library receipt reports `dropped: true`, `dropReason: "spool_full"`, `dropCount: 1`, and the metric reports `captureDrops: 1` with `captureDropReason: "spool_full"`. Non-SessionStart native stdout remains empty. Native SessionStart emits only `hookSpecificOutput`, placing a bounded notice with reason counts from the last 200 metric samples after its initial untrusted-data header inside `additionalContext`, not in new top-level fields. These are rolling-window counts, not all-time totals. The header stays first even when the brief is otherwise unavailable, and the combined context stays within its byte bound. Finite-capacity and retirement losses are observable rather than reported as captured. Exact-once replay applies to positively bound retained records that can progress; unlimited lossless intake is not promised. Soft-cap overshoot, unpublished `.writing/` leftovers and power-loss durability retain the [contracts ceiling](docs/contracts.md#p3-hook-and-native-intake-boundaries-13).
272
+
273
+ SessionStart prioritizes known open work and defers unsafe curated refresh with a fixed coverage reason. Native capture reads the newest 128 rows through an existing read-only time index and marks excluded older history partial, instead of rejecting a larger store. It never creates schema/FTS or repairs indexes. Missing/locked/incompatible sources are explicitly unavailable. The standalone `sessionStartBrief()` retains all its original sources. Recovery failure cannot discard a computed brief. Work, reader-lease and capture host ports use zero busy-wait and cooperative deadline checks; ordinary library callers retain their original defaults, including 3,000 ms waits. No watcher, resident worker, model/network call, embedding job or vault access is added. Host policy callbacks remain unsupported by `runHook`, while the standalone brief composes them normally.
274
+
275
+ SessionStart admits directly to the reserve with at most 32 metadata entries and performs no active-tier scan, retirement or replay. Non-SessionStart hooks maintain and drain both tiers. This is bounded reserve admission, not O(1) accounting; an atomic slot-reservation protocol is the upgrade. Parent brief storage still contains synchronous filesystem/SQLite work between cooperative deadline checks; asynchronous/deadline-aware storage remains the upgrade, not a larger handler limit. The R2 limits remain 150 ms p95 and 100 MB summed main/helper peak RSS, with a 140 ms handler budget. Functional tests construct a programmatic host-only deadline; the actual executable exposes no environment or CLI deadline extension. Its exec-only boot-clock handoff can only shorten the deadline and is cleared before hook import. Dedicated budget scripts retain the actual CLI, original sample counts, cold attempt 0 and timeout/reaper assertions. The [P3 receipt](docs/p3-hook-budget-2026-10-23.md) identifies prior-head evidence separately from current acceptance; the current rework has no newly claimed both-OS green CI result.
276
+
277
+ A private admission cursor rotates bounded inspection across pending records (at most 32 files/256 KiB per pass), so a healthy first slice cannot indefinitely hide later removed/unknown entries from retirement checks. Git include/conditional identity dependencies are traversed within depth four and 16 files. Pending scope hashes relevant semantic project-identity values rather than unrelated full-config or HEAD churn; derived cache validity may additionally hash raw configuration/HEAD bytes. Unconditional and `gitdir` includes bind semantic identity values. Dynamic `onbranch`/`hasconfig` or unknown conditional trees without identity keys bind nothing; if they contain origin/rewrite/worktree identity keys, scope stays unknown. Native read-only identity may still supply known work, but it never adopts that pending capture; counted `scope_unknown` retirement needs seven days. This conservative ceiling is upgraded by evaluating `onbranch` against HEAD at drain time, not by adding a condition interpreter in this PR.
278
+
279
+ Successful native project identities have a private, bounded derived cache (128 cwd entries, 4 KiB each). A hit must match the current project scope, stricter Git-validity metadata and executable revision; a miss still uses the shared hardened Git resolver. It stores no capture or context, and does not guess an identity when Git fails. The hook uses the selected developer Git directly only when PATH selects Apple's root-owned shim; custom PATH binaries remain selected. Cache ceilings and invalidation are in [the P3 contracts](docs/contracts.md#hook-project-identity-cache).
280
+
281
+ ## Doctor and the sync cadence
282
+
283
+ `amem doctor` prints one read-only report of this project's memory: duplicate and stale notes, pending candidates, capture freshness and hook registration for each CLI (a CLI with no hook is `manual`), the SessionStart brief's token cost, whether the index equals a rebuild of the logs, reader leases and the policy in effect. `amem doctor --json` is a stable machine-readable form (`amem.doctor/v1`). It writes nothing into the memory home (every check runs on a temporary copy of the logs) and creates nothing in a home you never used.
284
+
285
+ Intake runs at the end of each Codex, Kimi and Pi session once `amem setup <cli> --apply` has registered it. Sync is off unless you ask for it: amem registers no sync hook and runs nothing between sessions. To sync on your own schedule, call `amem sync --if-due` from your shell (for example an EXIT trap in `~/.zshrc`; it needs a terminal). It syncs at most hourly, backs off 15 minutes doubling to a day after failures, stops after 30 seconds, and does nothing without a remote set by `amem sync --remote <private git url>`. `--force` skips the schedule and the backoff.
286
+
287
+ ## Native transcript intake
288
+
289
+ `amem ingest` reads existing Codex, Kimi or Pi transcripts and writes screened deterministic capture rows, never curated notes. Run it from the project to bind intake to that working directory; the CLI exposes no project/home override or policy bypass:
290
+
291
+ ```sh
292
+ amem ingest codex --since 2026-10-01T00:00:00Z --json
293
+ amem ingest kimi --json
294
+ amem ingest pi --since 2026-10-01T00:00:00Z --json
295
+ ```
296
+
297
+ Discovery uses `CODEX_HOME` or `~/.codex` (`sessions/` and `archived_sessions/`), `KIMI_CODE_HOME` or `~/.kimi-code` (`sessions/**/wire.jsonl`), and `PI_CODING_AGENT_DIR` or `~/.pi/agent` (`sessions/`). It does not follow directory symlinks. A native record must carry a recognized schema/time and an observed absolute working-directory binding that resolves to this canonical project, including reviewed aliases. Unknown working directories, unsupported Kimi wire versions, missing timestamps and unavailable source roots are reported rather than guessed as an empty successful history. Foreign/private/secret rows are not imported. Full native envelopes and extracted capture inputs both pass current policy.
298
+
299
+ Per-source cursor databases and a fair rotation cursor live in the capture shard's private `ingest/` directory. Filenames and source identities are hashes; cursor contents retain offsets, anchor hashes, scope/protocol/cutoff and ownership metadata, not raw transcript paths or text. Capture receipts make replay idempotent across a crash between append and cursor acknowledgement. Repeating the command resumes its bounded tour instead of repeatedly favoring the first files. Broadening `--since` rechecks earlier history while existing capture receipts prevent duplicates. The JSON receipt reports `files`, `read`, `imported`, `skipped`, `spooled`, `remaining`, `errors` and an optional bounded `reason`; unavailable coverage is not silently represented by zero imported rows.
300
+
301
+ The default invocation reads at most 4 MiB and attempts 32 sources; trusted library options can raise those limits to 16 MiB and 128 sources. It examines at most 4,096 records, bounds a record at 256 KiB, and caps discovery at 4,096 directory entries/depth 8. Re-run a receipt with `remaining: true` after inspecting its reason; ingest creates no polling timer or watcher. Oversized records, including an unbroken record longer than the 4 MiB default window, are counted once and discarded through their next newline using a durable byte offset/skip flag. Later valid records remain reachable without retaining oversized content; an unfinished oversized tail stays pending until its newline arrives. The cursor assumes append-only native files and verifies file identity plus the last 1,024 acknowledged bytes. Historical rewrites outside that anchor are a declared ceiling, not full-file verification. These readers do not establish account-backed native interoperability, P5 cutover or P6 live-week acceptance.
302
+
146
303
  ## Library
147
304
 
148
305
  ```ts
@@ -210,7 +367,7 @@ Real migration remains blocked until the separately reviewed generation-capable
210
367
 
211
368
  ## Curated shard migration (#15 T2)
212
369
 
213
- `amem migrate --shards` moves the curated single home into `projects/<key-hash>/` and `user/` through a resumable generation journal. The original single-home log bytes are never rewritten. A populated selected generation is supported and keeps its generation ID. Each shard keeps private legacy inputs in read-only `legacy/`, public-main inputs in read-only `legacy-main/`, and generation-born rows in its own `log/` and `local/`. It has its own `index.db`, `search.db` and mutation lock; original IDs, hashes, placement and own/local/public provenance are preserved. The source generation bytes stay unchanged. Immutable prune proofs are validated before effects; a multi-shard prune is translated into one deterministic row per shard, with a receipt naming the original row. Existing capture and `work/` directories are not migration inputs and are left untouched. `catalog.db` remains derived and rebuildable, now including curated entry counts and last use. Per-shard sync refs are T3 and are not built.
370
+ `amem migrate --shards` moves the curated single home into `projects/<key-hash>/` and `user/` through a resumable generation journal. The original single-home log bytes are never rewritten. A populated selected generation is supported and keeps its generation ID. Each shard keeps private legacy inputs in read-only `legacy/`, public-main inputs in read-only `legacy-main/`, and generation-born rows in its own `log/` and `local/`. It has its own `index.db`, `search.db` and mutation lock; original IDs, hashes, placement and own/local/public provenance are preserved. The source generation bytes stay unchanged. Immutable prune proofs are validated before effects; a multi-shard prune is translated into one deterministic row per shard, with a receipt naming the original row. Existing capture and `work/` directories are not migration inputs and are left untouched. `catalog.db` remains derived and rebuildable, now including curated entry counts and last use. Split curated stores sync the opened canonical project and user overlay to separate private-remote refs (`amem/p/<key-hash>` and `amem/u`) with `amem sync --policy-free`; unopened projects and machine data are not fetched. Original IDs and attribution survive independent generations. Foreign-generation prune rows refuse the whole sync before publication/import; cross-generation tombstone sync remains the explicit T3 ceiling (see `docs/contracts.md`, "Per-shard private sync").
214
371
 
215
372
  Before splitting a live home, every installed host must open it through `openShardStore({ project })` or the explicit user shard. Plain `new MemoryStore()` refuses any split marker, including an interrupted split, so it cannot silently keep writing the old layout. No host capability registry currently exists; migration preflight output states that limitation. Do not release T2 for agent-hub use until its memory factory is integrated with `openShardStore` and that host version is installed. Existing unannounced older builds remain the documented unchanged-source ceiling.
216
373
 
@@ -254,7 +411,7 @@ The frozen nine-tool contract for dotfiles#255 and maru#458:
254
411
  | `amem_vault_candidates` | Read-only screened durable curated notes |
255
412
  | `amem_capacity` | Numeric machine capacity proposal; no apply capability |
256
413
 
257
- Search/show/timeline label capture as recall with `evidence: false`; capture never enters the curated graph. Policy screens complete rows before selection, limits or graph construction. The standalone secret screen, private flags, PII patterns and deny paths compose with a host's stricter policy through `createMemoryMcp({cwd, base, policy})`. Screening errors fail closed. `base` is the exact memory root; historical library `home` options still name its parent. Scope and profile changes refuse further access instead of silently reopening another store. Graph reads retain the 5,000-row curated ceiling; work-list reads stream the complete ordered history without an operation-count ceiling, applying policy before latest-value reduction. The working state scales with the current result, not historical payloads. These are operational ceilings, not exposed counts of unscreened data. Graphs cover curated-note relationships, not goal/suggestion tables.
414
+ Search/show/timeline label capture as recall with `evidence: false`; capture never enters the curated graph. Policy screens complete rows before selection, limits or graph construction. The standalone secret screen, private flags, PII patterns and deny paths compose with a host's stricter policy through `createMemoryMcp({cwd, base, policy})`. Screening errors fail closed. `base` is the exact memory root; historical library `home` options still name its parent. Scope and profile changes refuse further access instead of silently reopening another store. Graph reads retain the 5,000-row curated ceiling; work-list reads stream the complete ordered history without an operation-count ceiling, applying policy before latest-value reduction. The working state scales with the current result, not historical payloads. The graph ceiling is an operational bound, not an exposed count of unscreened data. Graphs cover curated-note relationships, not goal/suggestion tables.
258
415
 
259
416
  `WorkListStore` writes `projects/<key-hash>/work/` with its own append log, derived index and lock. Scalar fields reduce by latest value, null clears, list status `done` closes, and item status is `todo|doing|done|dropped`. Work lists are outside generation selection and capture eviction. `amem import claude-mem` additionally imports `work_state_entries` only through persisted person-reviewed D2 aliases, preserving source DB bytes and stable replay receipts; JSON keeps observation fields and adds `work` counts/cursor.
260
417
 
@@ -285,3 +442,11 @@ CI runs the typecheck, boundary check and tests on Ubuntu and macOS with Bun 1.4
285
442
  After setup, a `v*` tag triggers both operating-system gates. The tag must equal `v` plus `package.json`'s version. Only after both gates pass does the release job install npm 11.20.0 (trusted publishing needs 11.5.1 or later) and run `npm publish --access public`, authenticated through GitHub OIDC with `id-token: write`; npm adds provenance itself. The workflow uses no `NPM_TOKEN`.
286
443
 
287
444
  License: MIT.
445
+
446
+ ## Intent recall and history
447
+
448
+ Use `amem brief --for "repair the writer" --paths src/writer.ts` for a byte-capped, fail-first brief. Repeat `--paths` or `--symbols` for explicit scope; `--bytes 2048` sets the complete brief-body cap. Agents use the project-bound `amem_brief` MCP tool. Memory is untrusted data, not instructions.
449
+
450
+ Current recall omits superseded and expired entries. `amem show <id> --history` shows retained versions, `amem graph <id> --history` follows retained history, and `amem search writer --as-of 2026-10-20 --history` reads retained state at UTC midnight. Current forget/prune removals still hide the text at every past date. Promotion event time is not serialized, so affected history reports `promotion time unknown: tier before promotion cannot be recovered`. `(stale?)` is only a bounded path-metadata hint. See `docs/contracts.md` for retention and history limits.
451
+
452
+ The library exports `buildIntentBrief`, `planGuardNodes`, `matchingPlanFails` and nullable usage callbacks. Emitted output may record rendered counts; delivered and cited counts need explicit host observations. Missing evidence stays unknown. Agent-hub adoption is a separate follow-up after a published library release.
package/bin/amem.js CHANGED
@@ -1,9 +1,32 @@
1
- #!/usr/bin/env bun
1
+ #!/usr/bin/env -S bun --smol
2
2
  // Plain JavaScript: refuse Node before it can load TypeScript or bun:sqlite.
3
3
  const version = /^(\d+)\.(\d+)\.(\d+)/.exec(process.versions.bun ?? "");
4
4
  if (!version || Number(version[1]) < 1 || (Number(version[1]) === 1 && (Number(version[2]) < 4 || (Number(version[2]) === 4 && Number(version[3]) < 2)))) {
5
5
  console.error("amem: Bun >=1.4.2 is required. Install Bun: https://bun.sh");
6
6
  process.exit(1);
7
7
  }
8
- const { main } = await import("../src/cli.ts");
9
- await main();
8
+ const args = process.argv.slice(2);
9
+ if (args[0] === "hook") {
10
+ // This narrow intake must stay fail-open even if loading the optional hook implementation fails.
11
+ try {
12
+ const absoluteStart = performance.timeOrigin + performance.now() - process.uptime() * 1000;
13
+ // Replace this bootstrap before importing stores/policy. One PID, stdin and process group survive;
14
+ // there is no third resident Bun process and the original invocation still owns the wall budget.
15
+ if (!process.execArgv.includes("--smol")) {
16
+ process.execve(process.execPath, [process.execPath, "--smol", ...process.execArgv, import.meta.path, ...args],
17
+ { ...process.env, AMEM_HOOK_BOOT_AT: String(absoluteStart) });
18
+ }
19
+ // Exec-only clock handoff: untrusted inherited values can only shorten via Math.min below.
20
+ // Clear it before any hook/store code or child launch; it cannot select a longer deadline.
21
+ const inheritedStart = Number(process.env.AMEM_HOOK_BOOT_AT);
22
+ delete process.env.AMEM_HOOK_BOOT_AT;
23
+ const now = performance.timeOrigin + performance.now();
24
+ const started = (Number.isFinite(inheritedStart) && inheritedStart > 0 && inheritedStart <= now ? Math.min(absoluteStart, inheritedStart) : absoluteStart) - performance.timeOrigin;
25
+ const { hookMain } = await import("../src/hooks.ts");
26
+ const output = await hookMain(args.slice(1), started);
27
+ if (output) console.log(output);
28
+ } catch { /* Context-only hooks never deny a native action. */ }
29
+ } else {
30
+ const { main } = await import("../src/cli.ts");
31
+ await main(args);
32
+ }
@@ -0,0 +1,81 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "bun --smol \"${CLAUDE_PLUGIN_ROOT}/bin/amem.js\" hook SessionStart",
9
+ "timeout": 1
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "UserPromptSubmit": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "bun --smol \"${CLAUDE_PLUGIN_ROOT}/bin/amem.js\" hook UserPromptSubmit",
20
+ "timeout": 1
21
+ }
22
+ ]
23
+ }
24
+ ],
25
+ "PreToolUse": [
26
+ {
27
+ "hooks": [
28
+ {
29
+ "type": "command",
30
+ "command": "bun --smol \"${CLAUDE_PLUGIN_ROOT}/bin/amem.js\" hook PreToolUse",
31
+ "timeout": 1
32
+ }
33
+ ]
34
+ }
35
+ ],
36
+ "PostToolUse": [
37
+ {
38
+ "hooks": [
39
+ {
40
+ "type": "command",
41
+ "command": "bun --smol \"${CLAUDE_PLUGIN_ROOT}/bin/amem.js\" hook PostToolUse",
42
+ "timeout": 1
43
+ }
44
+ ]
45
+ }
46
+ ],
47
+ "PostToolUseFailure": [
48
+ {
49
+ "hooks": [
50
+ {
51
+ "type": "command",
52
+ "command": "bun --smol \"${CLAUDE_PLUGIN_ROOT}/bin/amem.js\" hook PostToolUseFailure",
53
+ "timeout": 1
54
+ }
55
+ ]
56
+ }
57
+ ],
58
+ "Stop": [
59
+ {
60
+ "hooks": [
61
+ {
62
+ "type": "command",
63
+ "command": "bun --smol \"${CLAUDE_PLUGIN_ROOT}/bin/amem.js\" hook Stop",
64
+ "timeout": 1
65
+ }
66
+ ]
67
+ }
68
+ ],
69
+ "SessionEnd": [
70
+ {
71
+ "hooks": [
72
+ {
73
+ "type": "command",
74
+ "command": "bun --smol \"${CLAUDE_PLUGIN_ROOT}/bin/amem.js\" hook SessionEnd",
75
+ "timeout": 1
76
+ }
77
+ ]
78
+ }
79
+ ]
80
+ }
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staix/agent-mem",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Curated, shared memory for coding agents: append-only logs, a derived index, tiers and a typed graph.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,6 +28,8 @@
28
28
  "files": [
29
29
  "src/",
30
30
  "bin/",
31
+ ".claude-plugin/",
32
+ "hooks/",
31
33
  "README.md",
32
34
  "LICENSE"
33
35
  ],