mirror-mind 0.35.0 → 0.36.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.
package/AGENTS.md CHANGED
@@ -18,8 +18,8 @@ Applies to all sessions, regardless of project.
18
18
  The mirror operates in four modes, chosen automatically based on context:
19
19
  Mirror, Builder, Explorer, and Soul.
20
20
 
21
- Each skill below is named three ways: `/mm-<skill>` in Pi, `$mm-<skill>` in
22
- Codex, `/mm:<skill>` in Claude Code. A **plugin** install prefixes the plugin's
21
+ Each skill below is named three ways: `/mm-<skill>` in Pi and Grok Build,
22
+ `$mm-<skill>` in Codex, `/mm:<skill>` in Claude Code. A **plugin** install prefixes the plugin's
23
23
  name — `/mirror-mind:mm:<skill>` in Claude Code's menu and in Antigravity,
24
24
  `$mirror-mind:mm:<skill>` in Codex, `mirror-mind:mm-<skill>` as the skill's own
25
25
  name — and the forms below refer to the same skill.
package/README.md CHANGED
@@ -60,8 +60,8 @@ This is not a chatbot with memory. This is a mirror — and yours.
60
60
 
61
61
  ## How It Works
62
62
 
63
- Local-first. SQLite. Four supported runtimes: Pi, Claude Code, Codex,
64
- Antigravity. One TypeScript core, run by Node.js. Jungian architecture: Self, Ego,
63
+ Local-first. SQLite. Five supported runtimes: Pi, Claude Code, Codex,
64
+ Antigravity, Grok Build. One TypeScript core, run by Node.js. Jungian architecture: Self, Ego,
65
65
  Personas, Shadow.
66
66
  Context-activated modes — Mirror, Builder, Explorer, and Soul. The identity lives
67
67
  in your machine, not a server.
@@ -110,7 +110,7 @@ this repository, turning a personal experiment into a reusable framework:
110
110
 
111
111
  - **English** — full migration from Portuguese across code, CLI, schema, identity, and docs
112
112
  - **Framework/user separation** — user identity and memory live privately under `~/.mirror/<user>/`, outside the repository
113
- - **Multi-runtime** — from Claude Code only to Pi, Claude Code, Codex, and Antigravity
113
+ - **Multi-runtime** — from Claude Code only to Pi, Claude Code, Codex, Antigravity, and Grok Build
114
114
  - **Multi-user** — independent mirrors for multiple users on the same machine
115
115
  - **Multi-session** — concurrent sessions tracked safely in a database-backed session registry
116
116
  - **Intelligence depth** — hybrid semantic and lexical search, honest reinforcement, two-pass extraction, memory consolidation, and shadow cultivation
@@ -12,6 +12,7 @@ Future release notes should use the structure defined in [Release Notes](../proc
12
12
 
13
13
  ## Releases
14
14
 
15
+ - [v0.36.0 — Five Runtimes, and a Home's Extensions on Every One](v0.36.0.md) — minor release closing CV21.DS10: Grok Build is the fifth runtime, at its honest L3 through the canonical plugin in two printed steps, measured live on grok 1.0.62. With it, CV21.DS5.US2: a home's installed extensions reach Claude Code, Codex, Antigravity, and Grok Build as one generated plugin per home, chosen by measurement on all four. Three Change Requests found by use: an extension's settings file (`extensions/<id>.env`, merged under the environment for the one spawned process), a quiet exit on a closed pipe, and a `bind` that refuses what it cannot bind. No migration.
15
16
  - [v0.35.0 — Four Runtimes, One Successor](v0.35.0.md) — minor release closing CV21.DS8: the Gemini CLI integration no longer ships and Antigravity stands in its place, the gate met by DS7's live L4 on agy 1.4.2. Four runtimes — Pi, Claude Code, Codex, Antigravity — named everywhere; the cutoff recorded in full, with the four hook entries a Gemini CLI user deletes and v0.34.0 as the last release that carries the integration; a retired-surface guard keeps it removed. No migration.
16
17
  - [v0.34.0 — Codex Through the Canonical Plugin](v0.34.0.md) — minor release closing CV21.DS4: Codex runs Mirror through the canonical plugin, installed in two commands from a marketplace the package ships, measured live at L4 on Codex 0.163.0. The v0.32.1 wrapper wiring no longer ships; `runtime diagnose` and `runtime update` say when Codex's cached plugin copy is behind and name the re-add that brings it level. No migration.
17
18
  - [v0.33.4 — A Name Is a Key](v0.33.4.md) — patch release of three first-run corrections: two from a contributor's PR #43 and CR141, which its review opened. `init --help` created a home named `--help`, `init ../escaped` created one outside `.mirror-minds`, and `init "My User"` put a space into `MIRROR_USER`; a user name is now a kebab-case key like a journey slug or a persona id, refused otherwise with a name that would work and nothing created. `identity edit` on Windows opens Notepad when no editor is named. Existing homes untouched. No migration.
@@ -0,0 +1,222 @@
1
+ ---
2
+ digest: >
3
+ v0.36.0 is a minor release closing CV21.DS10: Grok Build is Mirror Mind's
4
+ fifth runtime, at its honest L3 through the canonical plugin, in two steps
5
+ `mirror init` prints and `runtime diagnose` understands. With it ships
6
+ CV21.DS5.US2: a home's installed extensions reach Claude Code, Codex,
7
+ Antigravity, and Grok Build as one generated plugin per home, the shape
8
+ DS5.TS1 chose by measurement on all four. Three Change Requests ride along,
9
+ each found by using the product: an extension's installation settings get a
10
+ file Mirror reads for them (`extensions/<id>.env`), a closed pipe ends a
11
+ command quietly instead of with a stack trace, and `bind` refuses what it
12
+ cannot bind instead of writing a row that does nothing. No migration.
13
+ ---
14
+
15
+ # v0.36.0 — Five Runtimes, and a Home's Extensions on Every One
16
+
17
+ **Date:** 2026-10-20
18
+
19
+ ## Highlights
20
+
21
+ - **Grok Build is the fifth runtime** (CV21.DS10). Two printed steps — a
22
+ trusted plugin install and a home rule for the Operating Instructions — and
23
+ Grok has Mirror's skills, its hooks, and its MCP server. Measured live on
24
+ grok 1.0.62: the rule reaches the model, `/mm-mirror` answers signed by a
25
+ persona, every turn is logged as `grok_build`, the close fits Grok's 1.5 s
26
+ bound. Honest ceiling, written where a user reads it: Grok adds nothing to
27
+ the model's context per turn, so Mirror Mode lasts as long as the skill's
28
+ own instructions do.
29
+ - **A home's extensions reach every plugin runtime** (CV21.DS5.US2).
30
+ `mirror extensions install` and `uninstall` regenerate one small plugin per
31
+ Mirror home, `runtime/plugin/mirror-ext-<user>/`, and print the one wiring
32
+ step a runtime on the `PATH` still needs. Chosen over two other shapes by a
33
+ measured table on claude 2.1.296, codex 0.163.0, agy 1.4.2, and grok 1.0.62
34
+ ([decision](../project/decisions.md#user-extensions-reach-every-runtime-as-a-per-home-plugin)).
35
+ `runtime diagnose` names a stale copy and the command that brings it level.
36
+ - **An extension's installation settings have a home**
37
+ ([CR145](../project/refinement/rs011-extension-contract-completeness/cr145-an-extension-s-installation-settings-have-nowhere-to-live-but-the-login-shell.md)).
38
+ `~/.mirror-minds/<user>/extensions/<id>.env`, merged *under* the environment
39
+ for the one process Mirror spawns — subcommand or context provider — and
40
+ nothing else. The reason it had to be Mirror: Node reads
41
+ `NODE_EXTRA_CA_CERTS` before any extension code runs, so only the spawner can
42
+ set it, and the alternative was the login shell, where it broke every other
43
+ Postgres client on the machine.
44
+ - **Two front-door manners** ([CR148](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr148-the-front-door-dies-with-a-stack-trace-when-its-stdout-closes-early.md),
45
+ [CR149](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr149-bind-accepts-a-persona-or-journey-that-does-not-exist.md)).
46
+ `mirror … | head -1` exits 0 in silence instead of dying of `EPIPE` with a
47
+ stack trace — on the hook entry too, where exit 1 was the one code it
48
+ promised never to return. And `mirror ext <id> bind` checks the extension,
49
+ the capability, and the persona or journey before writing a row, refusing
50
+ with the fix in the line: `no persona 'engine' (mirror list personas shows
51
+ this home's)`.
52
+
53
+ ## Where We Started
54
+
55
+ v0.35.0 closed CV21's runtime set at four — Pi, Claude Code, Codex,
56
+ Antigravity — with Grok Build and user extensions named as what remained.
57
+ Grok had been reading Mirror's skills by accident since DS12, through its
58
+ Claude compatibility, with no hooks and no logging, and DS12.TS4 had left the
59
+ MCP attachment as a contradiction: the server ready with its seven tools in
60
+ Grok's own view, every session created with `mcp_server_count: 0`. User
61
+ extensions reached Claude Code through `expose-claude` and Pi through its
62
+ catalog, and nothing else; DS12.TS5 had measured that and left the exposure
63
+ as a choice among three shapes, deciding none.
64
+
65
+ Then the product was used. An extension built for a private journey needed a
66
+ TLS mode and a certificate that belonged to the machine, and the only place
67
+ the contract left for them was `~/.zshrc`. A mistyped `tail -` produced a
68
+ Node stack trace from the front door. A mistyped persona was bound without a
69
+ word. Three Change Requests, two days.
70
+
71
+ ## What Changed
72
+
73
+ **Grok Build, in four stories on one day** (2026-10-18, grok 1.0.62).
74
+ CV21.DS10.US1 measured five install shapes and chose two printed steps:
75
+ `grok plugin install "<pkg>/plugins/mirror-mind" --trust`, and the Operating
76
+ Instructions linked as a rule at `~/.grok/rules/mirror-mind.md` — Grok shows a
77
+ session-start hook's context on screen only, so the rule is how the
78
+ instructions reach the model. US1 also found that the root `plugin.json`
79
+ marker DS7 had added was hiding the plugin's version and MCP server from
80
+ Grok; it now carries both, one server on all four plugin runtimes. TS2
81
+ closed DS12's contradiction: the server attaches, its tools sit behind Grok's
82
+ `search_tool` and `use_tool` meta-tools (as `mirror-mind__list_journeys`), and
83
+ the zero count was a snapshot taken while the server was `pending`. US3
84
+ measured L3 with an ablation — remove the rule and the model loses the
85
+ instructions; the hook canaries never reached it — and the deferred close
86
+ tail finished at the next start, DS4.TS2's design on Grok's bound. TS4 wrote
87
+ it: the contract's Grok reference implementation, README, Getting Started,
88
+ REFERENCE, and the Operating Instructions name Grok Build as the fifth
89
+ runtime with its two steps and its ceiling. Paid on the way, CR143: the
90
+ inject hook returns at once on Grok instead of spending 2–3 s per load on a
91
+ context Grok discards.
92
+
93
+ **User extensions, chosen then built** (2026-10-19). CV21.DS5.TS1 measured
94
+ the three shapes DS12.TS5 had left — user-level skill roots, a per-home
95
+ generated plugin, MCP prompts — in scratch homes on all four runtimes, each
96
+ cell read by the runtime's own listing and then by one headless turn asking
97
+ for a canary, with every tool call classified so a find reached by searching
98
+ the disk did not count. Only the per-home plugin qualified on all four: the
99
+ skill roots are three roots that no runtime set reads in common, and MCP
100
+ prompts attached everywhere and reached no model. US2 built it.
101
+ `mirror extensions install`, `uninstall`, and `sync` regenerate
102
+ `runtime/plugin/mirror-ext-<user>/` in the canonical plugin's format,
103
+ version `0.1.<generation>`, carrying each extension's `claude` skill as its
104
+ manifest already declares it — no manifest rewritten. The install prints what
105
+ a runtime on the `PATH` still needs: a link for Claude Code, two commands for
106
+ Codex, one for Antigravity, one with `--trust` for Grok (and the line that
107
+ there is nothing for `--trust` to activate). After a change, the three
108
+ runtimes that copy a plugin hold a stale copy until one command each, which
109
+ the verbs print and `runtime diagnose` raises as five `ext_plugin_*` findings,
110
+ each with its repair. Two homes wire beside each other. `runtime status`
111
+ gained `Extensions plugin: mirror-ext-<user> <version> (<n> skills)`.
112
+
113
+ **The settings file** (CR145). `mirror-cli-v1` handed a subcommand the five
114
+ `MIRROR_*` variables and whatever environment `mirror` had; `mirror-context-v1`
115
+ handed a provider the environment untouched. Now both read
116
+ `extensions/<id>.env` — dotenv, the parser behind Mirror's own `.env` — and
117
+ merge it *under* the process environment: an explicit export still wins, the
118
+ five `MIRROR_*` names can never come from the file, and the result reaches
119
+ that one process. The file sits beside the installed directory, not inside
120
+ it: `install` rewrites the directory, and in a development home the directory
121
+ is a symlink into the extension's own repository. `mirror ext <id>` ends its
122
+ listing with the file's absolute path and one of four states — the variable
123
+ *names* it carries, `no variables`, `not present` with what to do, or
124
+ `unreadable` with the reason — and says when other users can read it. A file
125
+ that exists and cannot be read refuses the subcommand (one line, exit 1,
126
+ nothing spawned) and skips the provider with a `settings_unreadable`
127
+ diagnostic. A full `uninstall` keeps the file and says so. The contract
128
+ reference has the rules in full.
129
+
130
+ **A closed pipe** (CR148). One listener at each entry — `mirror` and
131
+ `mirror-hook` — on `EPIPE`: exit 0, write nothing. `npm`, `cargo`, and
132
+ `kubectl`'s choice over git's 141, because under `set -o pipefail` 141 fails a
133
+ pipeline a person wrote on purpose. Anything that is not `EPIPE` is rethrown.
134
+ The hook leaves one line in `hooks.log` first, `stdout closed early`, because
135
+ its try/catch could never see a stream's `error` event, and a runtime that
136
+ gives up on a hook closes its pipe. Stop means stop: work after the reader
137
+ closes is abandoned, as `SIGPIPE` abandons git's.
138
+
139
+ **`bind` checks what it binds** (CR149). Four checks before the one insert,
140
+ outside in: the id against the manifest's grammar, the extension installed,
141
+ the capability declared (the validator first, then the context reader for the
142
+ list), the persona or journey present in the identity rows a load is later
143
+ matched against. Each refusal is one line on stdout at exit 1 and writes no
144
+ row. `unbind` and `bindings` are untouched: removing a row that points at
145
+ nothing is the repair. A deliberate divergence from the Python oracle; the
146
+ bindings golden kept every byte, with its world given what the checks need.
147
+
148
+ ## Upgrading
149
+
150
+ ```bash
151
+ npm install -g mirror-mind@latest # or: mirror runtime update
152
+ ```
153
+
154
+ No migration. Existing homes, sessions, memories, and bindings are untouched.
155
+
156
+ **Grok Build:** `mirror init` prints the two steps when it finds `grok` on
157
+ your `PATH`; Getting Started's Grok Build section has them with what they
158
+ cost. If you had wired Claude Code, Grok already read the skills from that
159
+ link; after the plugin install each skill appears twice in Grok's menu, and
160
+ the hooks run once.
161
+
162
+ **If you have extensions installed:** the per-home plugin is generated by the
163
+ next `extensions` verb, not by the update. Re-install any one extension —
164
+ `mirror extensions install <id> --extensions-root <root>` from its source, or
165
+ from the home's own `~/.mirror-minds/<user>/extensions` — and the report
166
+ prints the one wiring step each runtime on your `PATH` still needs. Pi needs
167
+ nothing; its catalog path is unchanged.
168
+
169
+ **If an extension of yours reads machine settings from your shell profile:**
170
+ move them to `~/.mirror-minds/<user>/extensions/<id>.env`, `chmod 600` it,
171
+ and `mirror ext <id>` will show the keys it carries. Then delete the exports
172
+ — they reached every other program you run.
173
+
174
+ ## Conscious Exclusions
175
+
176
+ - **DS5 is not closed.** US3 — `review-copy` invoked live on each runtime
177
+ from the shipped package, with version, binary, and date — and TS4 — the
178
+ contract, extension docs, Getting Started, and REFERENCE saying how an
179
+ extension reaches each runtime — remain. Until TS4, the install's printed
180
+ steps and the contract's per-home plugin section are the documentation.
181
+ - **DS11**, the contract and docs refresh that folds all five runtimes into
182
+ one parity matrix, is not this release's.
183
+ - **An extension cannot yet declare the settings it expects** (CR146), so the
184
+ settings file is written from the extension's README, not from a
185
+ checklist Mirror prints. **`install` shows `suggested_personas` the home does
186
+ not have as if it did** (CR150). **Three copies of the extension-id grammar
187
+ remain, two of them disagreeing** (CR147); CR145 and CR149 used the
188
+ manifest's. All three captured, none done.
189
+ - **Still open from earlier:** the plugin smoke's negative probe on a machine
190
+ with a global `mirror-hook` (CR142); the DS7 routes removing agy's credential
191
+ where 1.3.x kept it (CR144).
192
+ - **141** as the exit on a closed pipe, and `EPIPE` handling inside extension
193
+ subcommands: not chosen, recorded in CR148.
194
+
195
+ ## What We Learned
196
+
197
+ Twice in one day a validation route had to be shown failing before it could
198
+ be trusted passing. CR145's first run validated nothing: the `mirror` on the
199
+ Navigator's `PATH` was the published 0.35.0, and the shell wrapped it in the
200
+ very function the CR retires — the workaround, measured where it lived. CR148's
201
+ planned control, `| head -1`, could not fail: a 2 KB listing lands whole in
202
+ the pipe buffer before `head` quits, so there was never an `EPIPE` to handle;
203
+ the reproduction is a reader that closes without reading. A route that names
204
+ the binary by path and a control that has been seen red are now part of what
205
+ a Change Request's validation means.
206
+
207
+ The three CRs came from using the product, not from reading it: a private
208
+ journey's extension, a typo, another typo. The review that preceded each —
209
+ five lenses on CR145, three on CR148, four on CR149 — changed each plan
210
+ before a line was written, and the things it added (a result type instead of
211
+ a throw, a diagnostic kind instead of a generic failure, a check zero on the
212
+ id grammar) are the parts a future reader will be glad of.
213
+
214
+ ## Next Horizon
215
+
216
+ DS5.US3 is the recommended pull: the live proof of `review-copy` on all four
217
+ plugin runtimes from the shipped package, through the steps this release
218
+ prints. TS4 writes what US3 measures. DS11 then folds five runtimes into one
219
+ contract and one matrix, and CV21 closes. RS011 — extension contract
220
+ completeness — holds the gaps the contract has, found one extension at a time;
221
+ CR146, a manifest that declares the settings it expects, is its next natural
222
+ candidate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Mirror Mind: a local-first memory and identity framework for agentic AI runtimes (Pi, Claude Code, Codex, Antigravity)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
5
5
  "author": {
6
6
  "name": "Mirror Mind"
@@ -1,4 +1,14 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
- "description": "Mirror Mind — local-first memory and identity for agentic runtimes."
3
+ "description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
4
+ "version": "0.36.0",
5
+ "mcpServers": {
6
+ "mirror-mind": {
7
+ "command": "/bin/sh",
8
+ "args": [
9
+ "-c",
10
+ "mmbin=\"\"; [ -n \"$MIRROR_BIN\" ] && [ -x \"$MIRROR_BIN/mirror\" ] && mmbin=\"$MIRROR_BIN/mirror\"; [ -n \"$mmbin\" ] || mmbin=\"$(command -v mirror 2>/dev/null)\"; for mmdir in \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmbin\" ] && break; [ -x \"$mmdir/mirror\" ] && mmbin=\"$mmdir/mirror\"; done; [ -n \"$mmbin\" ] || { echo \"mirror-mind: mirror not found on PATH; the MCP server cannot start. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory.\" >&2; exit 1; }; mmnode=\"$MIRROR_NODE\"; [ -n \"$mmnode\" ] && [ -x \"$mmnode\" ] || mmnode=\"$(command -v node 2>/dev/null)\"; for mmdir in \"$(dirname \"$mmbin\")\" \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmnode\" ] && break; [ -x \"$mmdir/node\" ] && mmnode=\"$mmdir/node\"; done; [ -n \"$mmnode\" ] || { echo \"mirror-mind: node not found on PATH; the MCP server cannot start. Set MIRROR_NODE.\" >&2; exit 1; }; exec \"$mmnode\" \"$mmbin\" mcp"
11
+ ]
12
+ }
13
+ }
4
14
  }
@@ -18,8 +18,8 @@ Applies to all sessions, regardless of project.
18
18
  The mirror operates in four modes, chosen automatically based on context:
19
19
  Mirror, Builder, Explorer, and Soul.
20
20
 
21
- Each skill below is named three ways: `/mm-<skill>` in Pi, `$mm-<skill>` in
22
- Codex, `/mm:<skill>` in Claude Code. A **plugin** install prefixes the plugin's
21
+ Each skill below is named three ways: `/mm-<skill>` in Pi and Grok Build,
22
+ `$mm-<skill>` in Codex, `/mm:<skill>` in Claude Code. A **plugin** install prefixes the plugin's
23
23
  name — `/mirror-mind:mm:<skill>` in Claude Code's menu and in Antigravity,
24
24
  `$mirror-mind:mm:<skill>` in Codex, `mirror-mind:mm-<skill>` as the skill's own
25
25
  name — and the forms below refer to the same skill.
@@ -1,6 +1,7 @@
1
1
  // What a runtime's session lifecycle lets Mirror do at its edges, by the label
2
2
  // US8 stamps on the session row (`claude_code`, `codex`, `grok_build`). One home
3
- // for the two policies the close path reads (CV21.DS4.TS2, US4).
3
+ // for the policies the hooks read: the two the close path reads (CV21.DS4.TS2,
4
+ // US4) and the one the inject hook reads (CV21.DS10.TS4, CR143).
4
5
 
5
6
  /**
6
7
  * Whether a runtime's SessionEnd hook has the time to run the LLM tail at exit
@@ -11,8 +12,10 @@
11
12
  * codex synchronous ("running async SessionEnd hook synchronously",
12
13
  * DS12.TS2 L2), 1 s for a handler with no `timeout` (DS2's live
13
14
  * `/hooks` table, 2026-10-13, Codex 0.162.1).
14
- * grok_build 1.5 s for a handler with no `timeout` (DS12.TS4, Grok 1.0.52).
15
- * DS10 re-measures it and may flip this entry.
15
+ * grok_build 1.5 s for a handler with no `timeout` (DS12.TS4, Grok 1.0.52);
16
+ * re-measured on 1.0.62 (CV21.DS10.US3): the guide says a
17
+ * plugin's SessionEnd hook gets at most that default whatever
18
+ * its own `timeout`, and the close landed 213–259 ms after it.
16
19
  *
17
20
  * Any other label, and no label, keeps today's behavior: the tail runs.
18
21
  */
@@ -57,3 +60,23 @@ export function sessionEndRunsLlmTail(interfaceName: string | null | undefined):
57
60
  export function sessionOutlivesTui(interfaceName: string | null | undefined): boolean {
58
61
  return interfaceName === "codex" || interfaceName === "antigravity";
59
62
  }
63
+
64
+ /**
65
+ * Whether a runtime gives the model what a `UserPromptSubmit` hook returns as
66
+ * `additionalContext` — the channel the inject hook uses (CV21.DS10.TS4, CR143).
67
+ *
68
+ * grok_build no. Grok shows a SessionStart hook's `systemMessage` on
69
+ * screen and discards both events' `additionalContext`
70
+ * (DS12.TS4 L2/L3 on 1.0.52; CV21.DS10.US3 on 1.0.62, with a
71
+ * random canary the probe hook emitted on every turn and the
72
+ * model never quoted). The loads the hook made there held a
73
+ * turn 2.3–2.6 s for a context nobody read; the hook returns
74
+ * before any of them when this is false.
75
+ *
76
+ * Any other label, and no label, reads it: an unlabelled payload is a Claude
77
+ * Code payload by detection's own rule (`detectHookRuntime`), and a predicate
78
+ * that failed closed would silence injection on the runtime that needs it.
79
+ */
80
+ export function runtimeReadsPromptContext(interfaceName: string | null | undefined): boolean {
81
+ return interfaceName !== "grok_build";
82
+ }
@@ -22,8 +22,10 @@ import { join } from "node:path";
22
22
  import type { Database, WritableDatabase } from "#db/database.ts";
23
23
  import { PROGRAM } from "#util/program.ts";
24
24
  import type { RenderedCommand } from "./catalog.ts";
25
+ import { readDeclaredCapabilityIds } from "./contextRuntime.ts";
25
26
  import { extensionNotInstalled, installedExtensionDir } from "./dispatch.ts";
26
- import { ExtensionError } from "./errors.ts";
27
+ import { ExtensionError, ExtensionValidationError } from "./errors.ts";
28
+ import { loadExtensionManifest, SKILL_ID_RE } from "./manifest.ts";
27
29
  import { runMigrations } from "./migrations.ts";
28
30
 
29
31
  /** Python `datetime.now(timezone.utc).isoformat()`: `+00:00`, microseconds. */
@@ -49,14 +51,50 @@ function label(target: BindingTarget): string {
49
51
  return target.id ? `${target.kind}/${target.id}` : target.kind;
50
52
  }
51
53
 
52
- /** Port of `_cmd_bind`. */
54
+ /**
55
+ * `_cmd_bind` checked nothing it bound: a missing extension, an undeclared
56
+ * capability, and a persona that does not exist all answered `bound`, and the
57
+ * person learned of the typo when the context they expected never appeared.
58
+ * CR149 diverges on purpose: four checks, outside in, before the one insert,
59
+ * each refusing with one line on stdout at exit 1 -- the shape of the
60
+ * family's other refusals -- and writing no row.
61
+ *
62
+ * The id check comes first and is not cosmetic. `installedExtensionDir` does
63
+ * not normalize (Python's bytes), so `../x` would pass the existence check
64
+ * when `<home>/x/skill.yaml` exists, and the context runtime later builds a
65
+ * path from whatever this row holds.
66
+ */
53
67
  export function runBind(
54
68
  db: WritableDatabase,
69
+ mirrorHome: string,
55
70
  extensionId: string,
56
71
  capabilityId: string,
57
72
  target: BindingTarget,
58
73
  deps: BindingDeps,
59
74
  ): RenderedCommand {
75
+ if (!SKILL_ID_RE.test(extensionId)) {
76
+ return out(`'${extensionId}' is not a valid extension id\n`, 1);
77
+ }
78
+ const extensionDir = installedExtensionDir(mirrorHome, extensionId);
79
+ if (!existsSync(extensionDir)) return extensionNotInstalled(mirrorHome, extensionId);
80
+ // The validator first, as `install` runs it: its message is already the
81
+ // user-facing one. Then the context runtime's reader for the ids.
82
+ try {
83
+ loadExtensionManifest(extensionDir);
84
+ } catch (error) {
85
+ if (!(error instanceof ExtensionValidationError)) throw error;
86
+ return out(`${error.message}\n`, 1);
87
+ }
88
+ const declared = readDeclaredCapabilityIds(extensionDir);
89
+ if (!declared.includes(capabilityId)) {
90
+ const list = declared.length === 0 ? "declares none" : `declared: ${declared.join(", ")}`;
91
+ return out(`extension/${extensionId} declares no capability '${capabilityId}' (${list})\n`, 1);
92
+ }
93
+ if (target.kind !== "global" && !targetExists(db, target.kind, target.id ?? "")) {
94
+ // The fix in the line, as `missing target: pass --persona <id>, ...` does.
95
+ const shows = target.kind === "persona" ? `${PROGRAM} list personas` : `${PROGRAM} journeys`;
96
+ return out(`no ${target.kind} '${target.id ?? ""}' (${shows} shows this home's)\n`, 1);
97
+ }
60
98
  db.prepare(
61
99
  "INSERT OR IGNORE INTO _ext_bindings " +
62
100
  "(extension_id, capability_id, target_kind, target_id, created_at) " +
@@ -65,6 +103,19 @@ export function runBind(
65
103
  return out(`bound ${extensionId}/${capabilityId} -> ${label(target)}\n`);
66
104
  }
67
105
 
106
+ /**
107
+ * The rows a load is later matched against (`selectExtensionBindings`), and
108
+ * nothing else: `layer = 'journey'`, not `journey_path`; exact key, so
109
+ * `Engineer` is not `engineer`. What `bind` accepts is what a load can find.
110
+ */
111
+ function targetExists(db: Database, layer: "persona" | "journey", key: string): boolean {
112
+ return (
113
+ db
114
+ .prepare("SELECT 1 AS found FROM identity WHERE layer = ? AND key = ? LIMIT 1")
115
+ .get(layer, key) !== undefined
116
+ );
117
+ }
118
+
68
119
  /** Port of `_cmd_unbind`. `target_id IS ?` is what makes a global unbind work. */
69
120
  export function runUnbind(
70
121
  db: WritableDatabase,
@@ -17,8 +17,12 @@
17
17
 
18
18
  import { existsSync } from "node:fs";
19
19
  import { homedir } from "node:os";
20
- import { join } from "node:path";
20
+ import { basename, join } from "node:path";
21
21
  import type { WritableDatabase } from "#db/database.ts";
22
+ import {
23
+ extensionPluginStates,
24
+ renderExtensionPluginBlock,
25
+ } from "#runtime/extensionPluginWiring.ts";
22
26
  import { PROGRAM } from "#util/program.ts";
23
27
  import {
24
28
  type BindingDeps,
@@ -59,6 +63,8 @@ import {
59
63
  renderSubcommandListing,
60
64
  } from "./dispatch.ts";
61
65
  import { ExtensionValidationError } from "./errors.ts";
66
+ import { regenerateExtensionPlugin } from "./extensionPlugin.ts";
67
+ import { readExtensionSettings } from "./settings.ts";
62
68
 
63
69
  export class UnsupportedCatalogCommandError extends Error {}
64
70
 
@@ -98,6 +104,15 @@ const EXTENSIONS_VERBS = new Set([
98
104
  export interface CatalogContext {
99
105
  /** The resolved Mirror home; `--mirror-home` has already been applied. */
100
106
  readonly mirrorHome: string;
107
+ /**
108
+ * The environment the extensions-plugin readers take their homes and PATH
109
+ * from (CV21.DS5.US2): HOME, CODEX_HOME, GROK_HOME, PATH. A test passes a
110
+ * stand-in; the route passes the process's. Absent, the readers see no
111
+ * runtime and the plugin block is its first line alone.
112
+ */
113
+ readonly env?: NodeJS.ProcessEnv;
114
+ /** Whether a PATH candidate is executable; absent, nothing is. */
115
+ readonly isExecutable?: (path: string) => boolean;
101
116
  }
102
117
 
103
118
  /** The writable half, for the four `ext` verbs that touch the database. */
@@ -201,6 +216,30 @@ function writeContext(context: CatalogContext, command: string): ExtWriteContext
201
216
  * the recorded divergence shape -- the same exit code, one line on stderr --
202
217
  * and, like Python, writes nothing to stdout.
203
218
  */
219
+ /**
220
+ * CV21.DS5.US2: after a write that changed the home's `claude` catalog, the
221
+ * home's extensions plugin is regenerated and the lines a person needs are
222
+ * appended to the report — the runtimes on the PATH that still need a step, or
223
+ * one line saying the plugin is wired everywhere. New lines at the end, never
224
+ * changed ones, so a caller parsing the report sees what it saw before.
225
+ */
226
+ function withExtensionPluginBlock(
227
+ context: CatalogContext,
228
+ rendered: RenderedCommand,
229
+ removed: readonly string[] = [],
230
+ ): RenderedCommand {
231
+ if (rendered.exitCode !== 0) return rendered;
232
+ const plugin = regenerateExtensionPlugin(context.mirrorHome);
233
+ if (plugin === null) return rendered;
234
+ const states = extensionPluginStates(
235
+ plugin,
236
+ context.env ?? {},
237
+ context.isExecutable ?? (() => false),
238
+ );
239
+ const block = renderExtensionPluginBlock(plugin, states, removed);
240
+ return { ...rendered, stdout: `${rendered.stdout}${block.join("\n")}\n` };
241
+ }
242
+
204
243
  function failLoudly(run: () => RenderedCommand): RenderedCommand {
205
244
  try {
206
245
  return run();
@@ -229,15 +268,18 @@ export function runExtensionsCommand(
229
268
  if (options.extensionsRoot === null) return usage("install requires --extensions-root PATH");
230
269
  const write = writeContext(context, "install");
231
270
  return failLoudly(() =>
232
- renderInstallReport(
233
- installExtension({
234
- extensionId: options.positional[1] as string,
235
- sourceRoot: options.extensionsRoot as string,
236
- mirrorHome: write.mirrorHome,
237
- runtime: options.runtime,
238
- db: write.db,
239
- deps: write.deps,
240
- }),
271
+ withExtensionPluginBlock(
272
+ context,
273
+ renderInstallReport(
274
+ installExtension({
275
+ extensionId: options.positional[1] as string,
276
+ sourceRoot: options.extensionsRoot as string,
277
+ mirrorHome: write.mirrorHome,
278
+ runtime: options.runtime,
279
+ db: write.db,
280
+ deps: write.deps,
281
+ }),
282
+ ),
241
283
  ),
242
284
  );
243
285
  }
@@ -252,15 +294,19 @@ export function runExtensionsCommand(
252
294
  // `install` lets the same class escape as a traceback. Same command, two
253
295
  // refusal shapes, measured rather than unified.
254
296
  try {
255
- return renderUninstallReport(
256
- uninstallExtension({
257
- extensionId: options.positional[1] as string,
258
- mirrorHome: write.mirrorHome,
259
- runtime: options.runtime,
260
- db: write.db,
261
- deps: write.deps,
262
- }),
263
- );
297
+ const report = uninstallExtension({
298
+ extensionId: options.positional[1] as string,
299
+ mirrorHome: write.mirrorHome,
300
+ runtime: options.runtime,
301
+ db: write.db,
302
+ deps: write.deps,
303
+ });
304
+ // What the copying runtimes "still have" after the removal: the claude
305
+ // command names this uninstall took out of the catalog.
306
+ const removed = report.removed
307
+ .filter(([runtimeName]) => runtimeName === "claude")
308
+ .flatMap(([, paths]) => paths.map((path) => basename(path)));
309
+ return withExtensionPluginBlock(context, renderUninstallReport(report), removed);
264
310
  } catch (error) {
265
311
  if (!(error instanceof ExtensionValidationError)) throw error;
266
312
  return out(`${error.message}\n`, 1);
@@ -337,7 +383,9 @@ export function runExtensionsCommand(
337
383
  `${stdout}Synced ${synced.length} extension(s) to ${options.targetRoot}`,
338
384
  ...synced.map((item) => ` ${item.command_name} -> ${item.installed_skill_path}`),
339
385
  ];
340
- return out(`${lines.join("\n")}\n`);
386
+ // A verb that regenerates the plugin without installing (plan amendment 6):
387
+ // from the home's own claude catalog, when the home has one.
388
+ return withExtensionPluginBlock(context, out(`${lines.join("\n")}\n`));
341
389
  }
342
390
 
343
391
  /**
@@ -404,7 +452,7 @@ export function runExtCommand(
404
452
  if ("stdout" in parsed) return parsed;
405
453
  const target = { kind: parsed.kind, id: parsed.id };
406
454
  return verb === "bind"
407
- ? runBind(context.db, extensionId, parsed.capabilityId, target, context.deps)
455
+ ? runBind(context.db, home, extensionId, parsed.capabilityId, target, context.deps)
408
456
  : runUnbind(context.db, extensionId, parsed.capabilityId, target);
409
457
  }
410
458
 
@@ -444,13 +492,24 @@ function dispatchOrNotInstalled(
444
492
  if (!existsSync(extensionRoot)) return extensionNotInstalled(mirrorHome, extensionId);
445
493
 
446
494
  const entries = readSubcommandListing(extensionRoot);
447
- if (HELP_FLAGS.has(subcommand)) return out(renderSubcommandListing(extensionId, entries));
495
+ // The listing says what the settings file holds (CR145) on both paths that
496
+ // render it -- a help spelling and an unknown subcommand -- by decision, not
497
+ // by accident: it is the same listing wherever it appears.
498
+ if (HELP_FLAGS.has(subcommand)) {
499
+ return out(
500
+ renderSubcommandListing(extensionId, entries, readExtensionSettings(mirrorHome, extensionId)),
501
+ );
502
+ }
448
503
 
449
504
  const documented = entries.find((entry) => entry.name === subcommand);
450
505
  if (documented === undefined) {
451
506
  return out(
452
507
  `unknown subcommand '${subcommand}' for extension/${extensionId}\n` +
453
- renderSubcommandListing(extensionId, entries),
508
+ renderSubcommandListing(
509
+ extensionId,
510
+ entries,
511
+ readExtensionSettings(mirrorHome, extensionId),
512
+ ),
454
513
  1,
455
514
  );
456
515
  }