mirror-mind 0.32.1 → 0.33.1
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/.claude/hooks/log-assistant-reply.sh +85 -0
- package/.pi/extensions/mirror-logger.ts +107 -22
- package/docs/releases/index.md +2 -0
- package/docs/releases/v0.33.0.md +164 -0
- package/docs/releases/v0.33.1.md +174 -0
- package/package.json +1 -1
- package/plugins/mirror-mind/.claude-plugin/plugin.json +6 -2
- package/plugins/mirror-mind/hooks/hooks.json +18 -6
- package/plugins/mirror-mind/hooks/log-assistant-reply.sh +98 -0
- package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +4 -2
- package/ts/src/conversation/hookPayload.ts +51 -0
- package/ts/src/conversation/logger.ts +300 -25
- package/ts/src/conversation/loggerCli.ts +41 -2
- package/ts/src/frontDoor/cli.ts +8 -1
- package/ts/src/frontDoor/conversationLoggerRoute.ts +20 -10
- package/ts/src/frontDoor/routing.ts +2 -0
- package/ts/src/frontDoor/runtimeRoute.ts +142 -24
- package/ts/src/frontDoor/sessionStartWelcomeRoute.ts +166 -0
- package/ts/src/frontDoor/stdinSource.ts +37 -0
- package/ts/src/guards/claudePlugin.ts +56 -10
- package/ts/src/guards/packContents.ts +1 -1
- package/ts/src/hooks/claude.ts +52 -14
- package/ts/src/hooks/payload.ts +18 -13
- package/ts/src/hooks/runtime.ts +24 -4
- package/ts/src/hooks/runtimeDetection.ts +34 -0
- package/ts/src/mirror/mirrorForm.ts +43 -0
- package/ts/src/runtime/git.ts +8 -2
- package/ts/src/runtime/releaseNotes.ts +230 -52
- package/ts/src/runtime/strategies/clone.ts +10 -3
- package/ts/src/runtime/strategies/package.ts +112 -23
- package/ts/src/util/tarEntries.ts +128 -0
- package/ts/src/welcome/card.ts +182 -60
- package/ts/src/welcome/statusLine.ts +12 -2
- package/ts/src/welcome/updateCache.ts +45 -27
- package/plugins/mirror-mind/mcp/launch.sh +0 -52
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Mirror Mind hook wrapper — GENERATED by scripts/generate_hook_wrappers.sh.
|
|
3
|
+
# Edit the generator, never this file: every wrapper shares these bytes and
|
|
4
|
+
# differs only in the hook name and in how it finds the Mirror entry
|
|
5
|
+
# (CV22.DS10.TS5 decision D4; CV22.DS10.US3 decision D5).
|
|
6
|
+
set -uo pipefail
|
|
7
|
+
HOOK="claude:stop"
|
|
8
|
+
|
|
9
|
+
# Resolve the tree from THIS FILE, never from the cwd: a runtime spawns hooks
|
|
10
|
+
# from whatever directory the session is in. The tree is a checkout or the
|
|
11
|
+
# installed package; the wrapper's depth to its root is the same in both.
|
|
12
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
13
|
+
REPO_ROOT="$(cd "$HERE/../.." && pwd -P)"
|
|
14
|
+
|
|
15
|
+
# A hook never fails the user's turn, and never fails in silence: when it
|
|
16
|
+
# cannot do its job it appends ONE line to <mirror home>/hooks.log and exits 0.
|
|
17
|
+
# The home is found the way the core finds it -- MIRROR_HOME, else
|
|
18
|
+
# MIRROR_USER -- from the environment when set there, else from the tree's
|
|
19
|
+
# .env when the wrapper is in a tree, else from the user's config file
|
|
20
|
+
# (~/.config/mirror/env), in the core's order (US3, D3). A GUI launch carries
|
|
21
|
+
# neither variable, and the first wrappers then wrote to a home nobody reads.
|
|
22
|
+
env_file_value() {
|
|
23
|
+
[ -f "$2" ] || return 0
|
|
24
|
+
sed -n -E "s/^[[:space:]]*(export[[:space:]]+)?$1[[:space:]]*=[[:space:]]*//p" "$2" \
|
|
25
|
+
| tail -n 1 | tr -d "\"'\r"
|
|
26
|
+
}
|
|
27
|
+
env_value() {
|
|
28
|
+
local value=""
|
|
29
|
+
if [ -n "${!1+set}" ]; then printf '%s' "${!1}"; return; fi
|
|
30
|
+
[ -z "$REPO_ROOT" ] || value="$(env_file_value "$1" "$REPO_ROOT/.env")"
|
|
31
|
+
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
32
|
+
printf '%s' "$value"
|
|
33
|
+
}
|
|
34
|
+
note() {
|
|
35
|
+
local home user
|
|
36
|
+
home="$(env_value MIRROR_HOME)"
|
|
37
|
+
user="$(env_value MIRROR_USER)"
|
|
38
|
+
if [ -n "$home" ]; then
|
|
39
|
+
home="${home/#\~/$HOME}"
|
|
40
|
+
elif [ -n "$user" ]; then
|
|
41
|
+
home="$HOME/.mirror-minds/$user"
|
|
42
|
+
[ -d "$home" ] || [ ! -d "$HOME/.mirror/$user" ] || home="$HOME/.mirror/$user"
|
|
43
|
+
else
|
|
44
|
+
return 0
|
|
45
|
+
fi
|
|
46
|
+
mkdir -p "$home" 2>/dev/null \
|
|
47
|
+
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
ENTRY="$REPO_ROOT/bin/mirror-hook.js"
|
|
51
|
+
|
|
52
|
+
# Find Node. /usr/bin/python3 exists on every macOS; node usually lives under
|
|
53
|
+
# nvm or Homebrew and is NOT on the PATH of a GUI-launched runtime. The Python
|
|
54
|
+
# hooks could end in `|| true` because they always found their interpreter;
|
|
55
|
+
# the same silence here would hide a new failure — no logging, no inject, no
|
|
56
|
+
# close tail — that a user would meet weeks later as "Mirror stopped
|
|
57
|
+
# remembering". So an unresolvable Node is RECORDED, never swallowed. The
|
|
58
|
+
# directory the entry was found in is searched first: an npm global bin holds
|
|
59
|
+
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
60
|
+
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
61
|
+
NODE="${MIRROR_NODE:-}"
|
|
62
|
+
if [ -z "$NODE" ] || [ ! -x "$NODE" ]; then
|
|
63
|
+
NODE="$(command -v node 2>/dev/null || true)"
|
|
64
|
+
fi
|
|
65
|
+
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
66
|
+
[ -n "$NODE" ] && break
|
|
67
|
+
[ -x "$candidate" ] && NODE="$candidate"
|
|
68
|
+
done
|
|
69
|
+
|
|
70
|
+
if [ -z "$NODE" ]; then
|
|
71
|
+
note "node not found on PATH; hook skipped. Set MIRROR_NODE."
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# Not `exec`: the entry exits 0 whatever happens inside it, so a non-zero exit
|
|
76
|
+
# means Node itself could not run the hook -- too old for the loader shim, or
|
|
77
|
+
# it crashed. That used to vanish into a stderr no runtime shows (TS5 handoff
|
|
78
|
+
# review, finding N1). No flags: the entry reads configuration and silences
|
|
79
|
+
# its own warnings (US3, D3).
|
|
80
|
+
"$NODE" "$ENTRY" "$HOOK" "$@"
|
|
81
|
+
status=$?
|
|
82
|
+
if [ "$status" -ne 0 ]; then
|
|
83
|
+
note "node exited $status before the hook finished ($NODE, $("$NODE" --version 2>/dev/null || echo 'version unknown')); Mirror needs Node 24 or later. Set MIRROR_NODE."
|
|
84
|
+
fi
|
|
85
|
+
exit 0
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* Integrates Pi with the Mirror Mind memory system.
|
|
5
5
|
*
|
|
6
6
|
* Events handled:
|
|
7
|
-
* - session_start → unmute +
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
7
|
+
* - session_start → unmute + welcome card + status line, in ONE front-door
|
|
8
|
+
* process (CV9.E2.S32); maintenance detached
|
|
9
|
+
* - before_agent_start → log user prompt, NOT awaited: queued on a per-session
|
|
10
|
+
* chain so the turn starts at once (CV9.E2.S32)
|
|
11
|
+
* - agent_end → log assistant response, after the chain settles
|
|
12
|
+
* - session_shutdown → close conversation (after the chain settles) + backup
|
|
11
13
|
*
|
|
12
14
|
* All heavy logic lives in the TypeScript core, reached through the `mirror`
|
|
13
15
|
* bin that ships beside this file (`bin/mirror.js`). This extension is a thin
|
|
@@ -274,8 +276,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
274
276
|
}
|
|
275
277
|
}
|
|
276
278
|
|
|
279
|
+
/**
|
|
280
|
+
* CV9.E2.S32 test knob: a delay before each awaited front-door exec, so the
|
|
281
|
+
* quit-mid-write and ordering cases can be driven deterministically.
|
|
282
|
+
* Honored ONLY under `MEMORY_ENV=test`; a production session never sleeps.
|
|
283
|
+
*/
|
|
284
|
+
function testFrontDoorDelayMs(): number {
|
|
285
|
+
if (process.env.MEMORY_ENV !== "test") return 0;
|
|
286
|
+
const raw = Number.parseInt(process.env.MIRROR_TEST_FRONT_DOOR_DELAY_MS ?? "", 10);
|
|
287
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
277
290
|
async function runMirror(args: string[]): Promise<string> {
|
|
278
291
|
try {
|
|
292
|
+
const delay = testFrontDoorDelayMs();
|
|
293
|
+
if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
279
294
|
const result = await pi.exec("node", [...FRONT_DOOR_ARGV, ...args], {
|
|
280
295
|
timeout: 30_000,
|
|
281
296
|
});
|
|
@@ -291,6 +306,60 @@ export default function (pi: ExtensionAPI) {
|
|
|
291
306
|
}
|
|
292
307
|
}
|
|
293
308
|
|
|
309
|
+
/**
|
|
310
|
+
* The `session-start --fast --welcome --format json` envelope (CV9.E2.S32).
|
|
311
|
+
* One process answers what used to take three: the unmute, the welcome
|
|
312
|
+
* card, and the status line. A parse failure is a WARN and the same empty
|
|
313
|
+
* result the three calls used to yield when one of them failed.
|
|
314
|
+
*/
|
|
315
|
+
interface SessionOpenEnvelope {
|
|
316
|
+
sessionStart: string;
|
|
317
|
+
welcome: string;
|
|
318
|
+
statusLine: string;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function parseSessionOpenEnvelope(stdout: string): SessionOpenEnvelope | null {
|
|
322
|
+
if (!stdout.trim()) return null;
|
|
323
|
+
try {
|
|
324
|
+
const parsed: unknown = JSON.parse(stdout);
|
|
325
|
+
if (!parsed || typeof parsed !== "object") throw new Error("not an object");
|
|
326
|
+
const record = parsed as Record<string, unknown>;
|
|
327
|
+
const field = (name: string): string => (typeof record[name] === "string" ? (record[name] as string) : "");
|
|
328
|
+
return { sessionStart: field("sessionStart"), welcome: field("welcome"), statusLine: field("statusLine") };
|
|
329
|
+
} catch (err: unknown) {
|
|
330
|
+
log("WARN", `session-open envelope unreadable: ${err instanceof Error ? err.message : String(err)}`);
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* CV9.E2.S32: the per-session chain of pending `log-user` writes.
|
|
337
|
+
*
|
|
338
|
+
* The invariant protected: the `log-user` process has EXITED before the
|
|
339
|
+
* `log-assistant` process for the same turn is SPAWNED, and before
|
|
340
|
+
* `session-end-pi` runs. Message reads are `ORDER BY created_at, id` and
|
|
341
|
+
* `created_at` is stamped by the writing process at insert time, so order
|
|
342
|
+
* in the database is wall-clock order of the inserts; the chain is what
|
|
343
|
+
* makes that order the prompt order. Consecutive prompts serialize on the
|
|
344
|
+
* same chain. A link that fails (or times out at `pi.exec`'s 30 s) settles
|
|
345
|
+
* the chain all the same: `runMirror` swallows, and the `.then(\u2026, \u2026)` is
|
|
346
|
+
* belt and braces so a rejection can never wedge it.
|
|
347
|
+
*/
|
|
348
|
+
const pendingUserLog = new Map<string, Promise<void>>();
|
|
349
|
+
|
|
350
|
+
function enqueueUserLog(sessionId: string, args: string[]): void {
|
|
351
|
+
const prior = pendingUserLog.get(sessionId) ?? Promise.resolve();
|
|
352
|
+
const next = prior.then(() => runMirror(args)).then(
|
|
353
|
+
() => undefined,
|
|
354
|
+
() => undefined,
|
|
355
|
+
);
|
|
356
|
+
pendingUserLog.set(sessionId, next);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function settleUserLog(sessionId: string): Promise<void> {
|
|
360
|
+
await (pendingUserLog.get(sessionId) ?? Promise.resolve());
|
|
361
|
+
}
|
|
362
|
+
|
|
294
363
|
/** Extract readable text from a content blocks array or plain string. */
|
|
295
364
|
function extractText(content: unknown): string {
|
|
296
365
|
if (typeof content === "string") return content;
|
|
@@ -307,14 +376,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
307
376
|
return text.slice(0, MAX_CONTENT_SIZE) + "\n[… truncated]";
|
|
308
377
|
}
|
|
309
378
|
|
|
310
|
-
|
|
379
|
+
/** The status bar line: the front door's compact status, or `?`, plus the external-skill count. */
|
|
380
|
+
function setMirrorStatus(ctx: MirrorStatusContext, compactStatus: string): void {
|
|
311
381
|
if (!ctx.hasUI) return;
|
|
312
|
-
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
313
|
-
const statusArgs = ["welcome", "--status-line"];
|
|
314
|
-
if (sessionId) {
|
|
315
|
-
statusArgs.push("--session-id", sessionId);
|
|
316
|
-
}
|
|
317
|
-
const compactStatus = (await runMirror(statusArgs)).trim();
|
|
318
382
|
const externalCatalog = loadInstalledPiExternalSkills();
|
|
319
383
|
const externalSkills = externalCatalog?.extensions ?? [];
|
|
320
384
|
const status = compactStatus || "◇ Mirror · ?";
|
|
@@ -324,6 +388,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
324
388
|
);
|
|
325
389
|
}
|
|
326
390
|
|
|
391
|
+
async function refreshMirrorStatus(ctx: MirrorStatusContext): Promise<void> {
|
|
392
|
+
if (!ctx.hasUI) return;
|
|
393
|
+
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
394
|
+
const statusArgs = ["welcome", "--status-line"];
|
|
395
|
+
if (sessionId) {
|
|
396
|
+
statusArgs.push("--session-id", sessionId);
|
|
397
|
+
}
|
|
398
|
+
setMirrorStatus(ctx, (await runMirror(statusArgs)).trim());
|
|
399
|
+
}
|
|
400
|
+
|
|
327
401
|
// Resolve the active Mirror home the same way the Python core does
|
|
328
402
|
// (src/memory/config.py resolve_mirror_home), including the per-workspace
|
|
329
403
|
// .env that Node would otherwise never see.
|
|
@@ -436,7 +510,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
436
510
|
log("INFO", `operating instructions appended from ${AGENTS_FILE}`);
|
|
437
511
|
});
|
|
438
512
|
|
|
439
|
-
// --- 1. session_start → unmute +
|
|
513
|
+
// --- 1. session_start → unmute + welcome + status line in one process ---
|
|
514
|
+
//
|
|
515
|
+
// CV9.E2.S32. `session-start --fast` WRITES the orientation that `welcome`
|
|
516
|
+
// and `welcome --status-line` READ, so the three cannot run in parallel;
|
|
517
|
+
// the composite runs them in one process, write first. Maintenance stays
|
|
518
|
+
// detached. Fires on startup, reload, new, resume, and fork alike.
|
|
440
519
|
|
|
441
520
|
pi.on("session_start", async (_event, ctx) => {
|
|
442
521
|
log("INFO", "session_start fired");
|
|
@@ -452,10 +531,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
452
531
|
log("WARN", line);
|
|
453
532
|
if (ctx.hasUI) ctx.ui.notify(line, "warning");
|
|
454
533
|
}
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
534
|
+
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
535
|
+
const openArgs = ["conversation-logger", "session-start", "--fast", "--welcome", "--format", "json"];
|
|
536
|
+
if (sessionId) openArgs.push("--session-id", sessionId);
|
|
537
|
+
const envelope = parseSessionOpenEnvelope(await runMirror(openArgs));
|
|
538
|
+
const summary = envelope?.sessionStart ?? "";
|
|
459
539
|
const externalCatalog = loadInstalledPiExternalSkills();
|
|
460
540
|
const externalSkills = externalCatalog?.extensions ?? [];
|
|
461
541
|
const externalSkillSummary = externalSkills.length
|
|
@@ -464,7 +544,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
464
544
|
log("INFO", `session-start result: ${summary || "(empty)"}`);
|
|
465
545
|
log("INFO", externalSkillSummary);
|
|
466
546
|
|
|
467
|
-
const welcome = (
|
|
547
|
+
const welcome = (envelope?.welcome ?? "").trim();
|
|
468
548
|
if (welcome) {
|
|
469
549
|
log("INFO", `welcome: ${welcome.split("\n")[0]}`);
|
|
470
550
|
}
|
|
@@ -473,11 +553,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
473
553
|
if (welcome) {
|
|
474
554
|
ctx.ui.notify(welcome, "info");
|
|
475
555
|
}
|
|
476
|
-
|
|
477
|
-
runMirrorBackground(["conversation-logger", "session-maintenance"], "session-maintenance");
|
|
478
|
-
} else {
|
|
479
|
-
runMirrorBackground(["conversation-logger", "session-maintenance"], "session-maintenance");
|
|
556
|
+
setMirrorStatus(ctx, (envelope?.statusLine ?? "").trim());
|
|
480
557
|
}
|
|
558
|
+
runMirrorBackground(["conversation-logger", "session-maintenance"], "session-maintenance");
|
|
481
559
|
});
|
|
482
560
|
|
|
483
561
|
// --- 2. before_agent_start → log user prompt with explicit session id ---
|
|
@@ -490,7 +568,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
490
568
|
if (!prompt || prompt.startsWith("/")) return;
|
|
491
569
|
|
|
492
570
|
log("INFO", `log-user: ${prompt.slice(0, 80)}...`);
|
|
493
|
-
|
|
571
|
+
// Not awaited (CV9.E2.S32): the turn starts now; `agent_end` and
|
|
572
|
+
// `session_shutdown` settle the chain before anything that must follow.
|
|
573
|
+
enqueueUserLog(sessionId, [
|
|
494
574
|
"conversation-logger",
|
|
495
575
|
"log-user",
|
|
496
576
|
sessionId,
|
|
@@ -535,6 +615,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
535
615
|
const combined = assistantTexts.join("\n\n---\n\n");
|
|
536
616
|
const content = truncate(combined);
|
|
537
617
|
|
|
618
|
+
// The user message's process has exited before this one is spawned.
|
|
619
|
+
await settleUserLog(sessionId);
|
|
538
620
|
runMirrorBackground(
|
|
539
621
|
["conversation-logger", "log-assistant", sessionId, content, "--interface", "pi"],
|
|
540
622
|
"log-assistant",
|
|
@@ -551,6 +633,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
551
633
|
const sessionId = ctx.sessionManager.getSessionFile() ?? null;
|
|
552
634
|
|
|
553
635
|
if (sessionId) {
|
|
636
|
+
// A prompt still in flight must land before the conversation closes.
|
|
637
|
+
await settleUserLog(sessionId);
|
|
638
|
+
pendingUserLog.delete(sessionId);
|
|
554
639
|
await runMirror(["conversation-logger", "session-end-pi", sessionId]);
|
|
555
640
|
log("INFO", `session closed: ${sessionId}`);
|
|
556
641
|
}
|
package/docs/releases/index.md
CHANGED
|
@@ -12,6 +12,8 @@ Future release notes should use the structure defined in [Release Notes](../proc
|
|
|
12
12
|
|
|
13
13
|
## Releases
|
|
14
14
|
|
|
15
|
+
- [v0.33.1 — The Install Answers For Itself](v0.33.1.md) — patch release of six RS010 Change Requests: the welcome card, the status line, and both `release-notes` readers answer for the install that is running, never for the cwd or the repository around the install. A package asks its own tree and the npm registry, a clone its own checkout and remote; `pending` says `unknown` when it cannot tell, and a `--ref` value can no longer run a command through git. No migration.
|
|
16
|
+
- [v0.33.0 — One Plugin, Three Runtimes](v0.33.0.md) — minor release closing CV21.DS2: the one plugin works as it is on Claude Code, Codex, and Grok. Its MCP server starts on all three, the explicit Mirror form is recognized in every runtime's spelling, and each session is logged under its runtime with every reply beside its prompt. Also: a faster Pi session start, and notice that Gemini CLI is retired in favor of Antigravity.
|
|
15
17
|
- [v0.32.1 — The Release That Shipped](v0.32.1.md) — patch release that is the published CV22 release: `v0.32.0` was tagged and its publication refused by the retired-surface tripwire over one sentence in Getting Started, now rephrased. No code, behavior, or schema differs from `v0.32.0`; read that note for the release itself.
|
|
16
18
|
- [v0.32.0 — One Language, One Install](v0.32.0.md) — minor release that closes CV22, the TypeScript Core Port. Mirror Mind is one TypeScript package run by Node.js 24, installed with `npm install -g mirror-mind` and wired to Pi, Claude Code, Gemini CLI, or Codex by one documented step each; no Python remains in the repository or the shipped artifact; every command answers as it did. Eleven surfaces are retired with cutoffs, recorded in [Pending Cutoffs](pending-cutoffs.md), which this note carries. A new home imports only the Pi sessions newer than itself.
|
|
17
19
|
- [v0.31.14 — Change Request Resume](v0.31.14.md) — patch release that adds the public Ariad Workbench `change-request resume` verb so a non-terminal advanced Change Request that lost the active pointer can continue without direct SQLite edits or artificial status regression. Resume preserves the CR record, restores only the Refinement cursor, allows `implemented` to proceed to `validate` and `validated` to proceed to `done`, and keeps terminal CRs closed.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.33.0 closes CV21.DS2, the Mirror Plugin & MCP Foundation: the one plugin
|
|
4
|
+
Mirror ships now works as it is on Claude Code, Codex, and Grok. Its MCP
|
|
5
|
+
server starts on all three, from a GUI launch too, because its manifest no
|
|
6
|
+
longer names a path a runtime must expand; the explicit Mirror form is
|
|
7
|
+
recognized in every runtime's spelling; and each session is logged under
|
|
8
|
+
its own runtime, with the Mirror question and every reply logged beside
|
|
9
|
+
its prompt. It was validated in isolated live sessions on Claude Code
|
|
10
|
+
2.1.296, Codex 0.162.1, and Grok 1.0.52. The release also carries a
|
|
11
|
+
faster Pi session start (CV9.E2.S32). Package and Claude Code users update
|
|
12
|
+
as usual; a plugin installed into Codex or Grok by copy is re-installed to
|
|
13
|
+
receive the new entry.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# v0.33.0 — One Plugin, Three Runtimes
|
|
17
|
+
|
|
18
|
+
**Date:** 2026-10-13
|
|
19
|
+
|
|
20
|
+
## Highlights
|
|
21
|
+
|
|
22
|
+
- **The MCP server starts on every runtime that takes the plugin.** The manifest's entry
|
|
23
|
+
used to name `${CLAUDE_PLUGIN_ROOT}/mcp/launch.sh`, which Codex never expanded, so the
|
|
24
|
+
server never started there. It is now a small locator run by `/bin/sh` that finds the
|
|
25
|
+
installed `mirror` and `node` itself (`MIRROR_BIN`, the `PATH`, the global bin
|
|
26
|
+
directories), so it also starts when a runtime is launched from the GUI without your
|
|
27
|
+
shell's `PATH`.
|
|
28
|
+
- **Every reply is logged, beside its prompt, under the runtime it came from.** The shared
|
|
29
|
+
hooks detect Claude Code, Codex, or Grok, label the session `claude_code`, `codex`, or
|
|
30
|
+
`grok_build`, and log the turn's final answer from the runtime's Stop event, paired with
|
|
31
|
+
its prompt by the runtime's own turn id.
|
|
32
|
+
- **A Mirror turn is a conversation turn.** `/mirror-mind:mm-mirror …`, `$mirror-mind:mm:mirror …`,
|
|
33
|
+
`/mm-mirror …`, and the other spellings are recognized by the inject hook, and the
|
|
34
|
+
question is logged without the form, so memory extraction sees the question with its
|
|
35
|
+
answer.
|
|
36
|
+
- **The plugin validates cleanly and its hooks cannot hang a turn.** Hook commands quote
|
|
37
|
+
the plugin root, so `claude plugin validate` passes with no warnings, and hook timeouts
|
|
38
|
+
are the seconds the runtimes read them as. The values shipped before meant hours.
|
|
39
|
+
- **Codex's plugin tools, and Grok's, are not yet in the model's hands.** Codex starts the
|
|
40
|
+
server and keeps a plugin's MCP tools behind its own `tool_search`; Grok starts it and
|
|
41
|
+
lists its seven tools, but does not attach it to a session. The second is the next Grok
|
|
42
|
+
story's to investigate.
|
|
43
|
+
|
|
44
|
+
## Where We Started
|
|
45
|
+
|
|
46
|
+
CV21 converges Mirror's runtimes on one artifact: a Claude-format plugin carrying the
|
|
47
|
+
skills, the lifecycle hooks, and the Mirror MCP server, taken by each runtime by install
|
|
48
|
+
rather than wired by hand. Its re-baseline, earlier the same day, measured what the
|
|
49
|
+
installed runtimes do with the plugin `v0.32.1` ships. Three of four take it as it is,
|
|
50
|
+
and four things in it were wrong:
|
|
51
|
+
- Claude Code's validator warned on every hook command;
|
|
52
|
+
- the MCP entry did not start on Codex;
|
|
53
|
+
- the inject hook missed the Mirror skill on every plugin install, because each runtime
|
|
54
|
+
spells it differently;
|
|
55
|
+
- the shared hooks logged Codex and Grok sessions as Claude Code's, without their replies.
|
|
56
|
+
|
|
57
|
+
## What Changed
|
|
58
|
+
|
|
59
|
+
The four fixes were planned as one Delivery Story, after a review through six lenses
|
|
60
|
+
(engineering, operations, quality, security, data, and model behavior).
|
|
61
|
+
|
|
62
|
+
**The MCP entry was chosen by measurement.** Two placeholder-free entries were run on all
|
|
63
|
+
three runtimes before either was written: the inline locator, and the bare `mirror mcp`.
|
|
64
|
+
Both start from a terminal. Only the locator starts from a GUI-launch `PATH`, as written
|
|
65
|
+
and inside Claude Code, where `mirror mcp` failed with "executable not found". The
|
|
66
|
+
measurement also showed Grok substituting set environment variables inside MCP arguments,
|
|
67
|
+
so the locator's own variables carry names nobody exports. `mcp/launch.sh` is retired.
|
|
68
|
+
|
|
69
|
+
**The reply rule changed before a line was written.** The plan first paired replies with
|
|
70
|
+
logged prompts and kept skipping slash commands. The review showed that would drop the
|
|
71
|
+
Mirror answers on Claude Code, which until now reached the database only through the
|
|
72
|
+
transcript import at session end. So the Mirror question is logged too, and the reply
|
|
73
|
+
beside it. Replies to other slash commands, such as `/mm-journeys`, are not recorded:
|
|
74
|
+
they are commands, not conversation.
|
|
75
|
+
|
|
76
|
+
**Sessions know their runtime.** Grok's runner sets its own variables on every hook and
|
|
77
|
+
names events in camelCase, and Codex's payload carries a `turn_id`; Claude Code is what
|
|
78
|
+
remains. The label is stamped on every prompt, so a conversation the Mirror skill opens
|
|
79
|
+
inherits it.
|
|
80
|
+
|
|
81
|
+
**A defect of the release's own was paid before it shipped.** A skipped reply was written
|
|
82
|
+
to `hooks.log`, which `runtime diagnose` reads as failures, so every slash command would
|
|
83
|
+
have raised a warning. Only a reply genuinely lost, its prompt never having reached
|
|
84
|
+
Mirror, is written there now.
|
|
85
|
+
|
|
86
|
+
### What you will notice
|
|
87
|
+
|
|
88
|
+
- **On Claude Code**, replies are logged as each turn ends rather than imported when the
|
|
89
|
+
session closes, and an explicit Mirror turn's question is logged with its answer. The
|
|
90
|
+
`mm-mirror` skill no longer records a summary by hand where the hooks run.
|
|
91
|
+
- **In `runtime diagnose`**, a hook warning now means a real failure.
|
|
92
|
+
- **For a plugin installed by copy** (Codex from a marketplace, Grok with `grok plugin
|
|
93
|
+
install`), nothing changes until you re-install: the copy keeps its own manifest and
|
|
94
|
+
launcher, and keeps working as before.
|
|
95
|
+
|
|
96
|
+
## Upgrading
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
mirror runtime update # or: npm install -g mirror-mind
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
There is no database migration in this release.
|
|
103
|
+
|
|
104
|
+
- **Claude Code**, wired the documented way (`~/.claude/skills/mirror-mind` linked to the
|
|
105
|
+
package's plugin), reads the plugin in place: new sessions get the new hooks and MCP
|
|
106
|
+
entry with nothing to do.
|
|
107
|
+
- **Codex and Grok, if you installed the plugin into them**, re-install it to receive
|
|
108
|
+
the new MCP entry and the Stop hook. Codex copies a plugin into its cache at a version;
|
|
109
|
+
Grok copies it on install. Codex's documented wiring is still `v0.32.1`'s two steps and
|
|
110
|
+
wrapper, which this release does not change.
|
|
111
|
+
- **Pi** gets the faster session start described below, with nothing to do.
|
|
112
|
+
- **Gemini CLI** is unaffected by this release; see the notice below.
|
|
113
|
+
|
|
114
|
+
## Also In This Release
|
|
115
|
+
|
|
116
|
+
- **Pi opens a session in one front-door call** (CV9.E2.S32). The orientation write and
|
|
117
|
+
the two renders that read it, the welcome card and the status line, run in one process
|
|
118
|
+
instead of three. Startup to the status line went from 846 ms to 470 ms, the median of
|
|
119
|
+
five starts in the development home. The prompt log no longer holds the turn: it is
|
|
120
|
+
queued, and settled before the reply is logged.
|
|
121
|
+
- **Notice: Gemini CLI is retired as a Mirror runtime, and Antigravity is its
|
|
122
|
+
successor** ([decision](../project/decisions.md#gemini-cli-is-retired-and-antigravity-is-its-successor)).
|
|
123
|
+
Google deprecated the Gemini Code Assist backend for individuals. The integration still
|
|
124
|
+
ships in this release and still works. CV21.DS8 removes it behind a documented cutoff,
|
|
125
|
+
only once Antigravity gives a Gemini CLI user what they have now.
|
|
126
|
+
|
|
127
|
+
## Conscious Exclusions
|
|
128
|
+
|
|
129
|
+
- **Codex through the plugin, as the documented route**, and its marketplace: CV21.DS4.
|
|
130
|
+
- **Grok's attachment of the MCP server to a session**, and Grok's honest L3 (it offers no
|
|
131
|
+
per-turn injection): CV21.DS10.
|
|
132
|
+
- **The SessionEnd time bound.** Codex gives a SessionEnd hook with no timeout 1 second,
|
|
133
|
+
Grok 1.5 seconds. With a provider key the close tail's extraction may not finish in
|
|
134
|
+
that time; the conversation is closed first and extracted at the next session start, so
|
|
135
|
+
nothing is lost. Left to CV21.DS4 and DS10.
|
|
136
|
+
- **A status line in the plugin.** A plugin cannot carry one (CV21.DS12); CV21.DS11 will
|
|
137
|
+
document it as a user step.
|
|
138
|
+
- **Three small findings, captured** as CR129 (`mirror mcp` with no home prints a stack
|
|
139
|
+
trace), CR130 (double-encoded dashes in the configuration reference), and CR131
|
|
140
|
+
(`runtime diagnose` does not run the MCP locator).
|
|
141
|
+
|
|
142
|
+
## What We Learned
|
|
143
|
+
|
|
144
|
+
The instrument needs proving before the thing it measures. The live validation found four
|
|
145
|
+
defects in its own route before it found anything about the plugin:
|
|
146
|
+
- the real config file reaching a scratch session;
|
|
147
|
+
- Claude Code remembering an earlier MCP failure for fifteen minutes;
|
|
148
|
+
- macOS's bash 3.2 refusing a construct;
|
|
149
|
+
- a static step that could not run twice.
|
|
150
|
+
|
|
151
|
+
Writing the proof of the last fix found a fifth: Codex leaves an app-server daemon running
|
|
152
|
+
from its home after a session, and it had outlived the previous story's cleanup too.
|
|
153
|
+
|
|
154
|
+
A runtime's own view is often the better witness. Codex's log showed the server's
|
|
155
|
+
handshake, and its `/mcp` view showed the seven tools, where the model could not name
|
|
156
|
+
them. Grok's MCP view showed the server ready while its session log said none was
|
|
157
|
+
attached. That contradiction is now the next Grok story's starting point.
|
|
158
|
+
|
|
159
|
+
## Next Horizon
|
|
160
|
+
|
|
161
|
+
CV21 continues with Codex through the canonical plugin (DS4), then Antigravity through
|
|
162
|
+
its skills and a native layer (DS7) and the Gemini CLI sunset behind it (DS8), Grok
|
|
163
|
+
through the plugin (DS10), user extensions on every runtime (DS5), and the contract and
|
|
164
|
+
docs refresh (DS11).
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.33.1 is a maintenance release: six Change Requests from RS010, closed
|
|
4
|
+
since v0.33.0, and no story or epic. Its subject is one defect in four
|
|
5
|
+
surfaces. The welcome card, the status line, and both release-notes readers
|
|
6
|
+
answered for the directory the person was in, or for the git repository
|
|
7
|
+
around the install, instead of for the install that was running. Each now
|
|
8
|
+
asks the install itself: a package its own tree and the npm registry, a
|
|
9
|
+
clone its own checkout and remote. `release-notes pending` says `unknown`
|
|
10
|
+
when it cannot tell, and a `--ref` value can no longer run a command through
|
|
11
|
+
git. Upgrade with `mirror runtime update`; there is no migration.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# v0.33.1 — The Install Answers For Itself
|
|
15
|
+
|
|
16
|
+
**Date:** 2026-10-15
|
|
17
|
+
|
|
18
|
+
## Highlights
|
|
19
|
+
|
|
20
|
+
- **The welcome card and the status line announce the update the running install
|
|
21
|
+
has** (CR135). A package asks the registry for its channel's version, a clone its own
|
|
22
|
+
checkout and remote. Before, both asked the git repository of whatever directory the
|
|
23
|
+
session started in: another project's tag could be announced, a package was never told
|
|
24
|
+
of a release, and the status line kept a stale `⬆` after the update.
|
|
25
|
+
- **npm runs in the install's own directory**, for the card and for `runtime update`, so
|
|
26
|
+
a project's `.npmrc` can no longer choose the registry the card reads and the update
|
|
27
|
+
installs from.
|
|
28
|
+
- **`release-notes latest` and `vX.Y.Z` read the notes the package ships** (CR133). On a
|
|
29
|
+
Homebrew install on Apple Silicon, where `/opt/homebrew` is itself a git repository,
|
|
30
|
+
they answered `Release notes: not found`.
|
|
31
|
+
- **`release-notes pending` answers for the install, never the cwd** (CR134). A clone
|
|
32
|
+
reads its own channel's ref in its own checkout. A package asks the registry which
|
|
33
|
+
version its channel holds and reads that version's notes from its tarball. When
|
|
34
|
+
nothing can answer it says `Pending releases: unknown` with a reason and exits 1, so
|
|
35
|
+
`none` always means a source answered.
|
|
36
|
+
- **`--ref` can no longer run a command.** A value like `--upload-pack=<command>`
|
|
37
|
+
reached `git fetch` as an option, and git ran it. Refs beginning with `-` are refused,
|
|
38
|
+
and every git call takes `--end-of-options` before its operands.
|
|
39
|
+
- **A clone's update fetch has git's network budget again** (CR132): 120 seconds, not
|
|
40
|
+
the 2 seconds a local git call gets.
|
|
41
|
+
|
|
42
|
+
## Where We Started
|
|
43
|
+
|
|
44
|
+
`v0.33.0` was installed onto a Homebrew package on 2026-10-14, and three surfaces
|
|
45
|
+
answered wrongly the same day. `mirror runtime release-notes v0.33.0` said `not found`, with
|
|
46
|
+
the note sitting in the package. `release-notes pending` said `none` from `/tmp` and,
|
|
47
|
+
from inside an unrelated project, listed that project's releases after fetching from its
|
|
48
|
+
remote. The welcome card asked the same wrong repository whether Mirror had an update.
|
|
49
|
+
|
|
50
|
+
The three had one root. The readers were ported faithfully from a Python core that ran
|
|
51
|
+
from a checkout, where the cwd, git's top level, and the install were usually one
|
|
52
|
+
directory. Under npm distribution they are three: a skill runs in the person's project,
|
|
53
|
+
and a global prefix can sit inside someone else's repository. CV22 had already moved the
|
|
54
|
+
version and the welcome card's release note onto the running tree, and handed `latest`
|
|
55
|
+
that tree. But the reader still climbed out of it to git's top level, and `pending` and
|
|
56
|
+
the update check still began at the cwd.
|
|
57
|
+
|
|
58
|
+
The promotion of `v0.33.0` itself had stumbled on the same day. Its push timed out at two
|
|
59
|
+
seconds and was finished by hand (CR132), and npm's `latest` had needed a move nobody's
|
|
60
|
+
route printed (CR125).
|
|
61
|
+
|
|
62
|
+
## What Changed
|
|
63
|
+
|
|
64
|
+
**Each install asks about itself.** The card, the status line, and `pending` detect the
|
|
65
|
+
install from where the front door lives, as `runtime channel` does, with nothing spawned
|
|
66
|
+
and no cwd involved. A package reads its channel's dist-tag through the same call `runtime
|
|
67
|
+
update --check` makes. A clone runs its check, its tag lookup, and its "commits behind"
|
|
68
|
+
count in its own checkout. The channel shown is the install's. On `stable` the card checks
|
|
69
|
+
at every render; on `main` it never checks and shows the cached verdict.
|
|
70
|
+
|
|
71
|
+
**npm's directory is a trust decision.** A four-lens review of CR135's plan found that npm
|
|
72
|
+
reads the `.npmrc` of the directory it runs in. A cloned project that ships
|
|
73
|
+
`registry=` could therefore choose what the card announced and what `runtime update`
|
|
74
|
+
installed. Every npm call the product makes now runs in the install's own root, whose
|
|
75
|
+
project config is the package's own.
|
|
76
|
+
|
|
77
|
+
**One bound for the session start.** The card's remote reads share one 5-second deadline,
|
|
78
|
+
and the registry read does not retry, so an unreachable registry costs at most that. A
|
|
79
|
+
cached update the install already has is shown by neither surface, and a successful
|
|
80
|
+
`runtime update` records `up_to_date`.
|
|
81
|
+
|
|
82
|
+
**Release notes come from the install.** `latest` and `vX.Y.Z` read `docs/releases` under
|
|
83
|
+
the running tree and ask git nothing. `pending` reads, for a clone, `origin/<channel>`
|
|
84
|
+
(or `--ref`) in the clone's own repository, after fetching it there. For a package, it
|
|
85
|
+
resolves the channel's dist-tag to a version, as `runtime update` does when it plans.
|
|
86
|
+
When that version is newer, `npm pack --ignore-scripts` fetches that exact tarball into a
|
|
87
|
+
private directory, and its notes are read in memory and the directory removed. When it is
|
|
88
|
+
not newer, nothing is downloaded.
|
|
89
|
+
|
|
90
|
+
Three choices shaped the package path:
|
|
91
|
+
|
|
92
|
+
- the registry tarball, not the GitHub branch: the branch is a second authority that can
|
|
93
|
+
disagree with the dist-tag, as a half-promoted release does;
|
|
94
|
+
- not version numbers alone, which would not answer "what's new in this update?";
|
|
95
|
+
- a reader of its own over `node:zlib` rather than a spawned `tar`, which would take
|
|
96
|
+
archive-chosen names as arguments. It stops at 64 MB inflated. It refuses an archive
|
|
97
|
+
that ends before its end-of-archive block, so a cut tarball cannot pass for a whole one
|
|
98
|
+
missing its newest notes.
|
|
99
|
+
|
|
100
|
+
**`unknown` is said.** A failed fetch, a ref or dist-tag that does not resolve, an
|
|
101
|
+
unreachable registry, or an install that cannot be identified answers `Pending releases:
|
|
102
|
+
unknown` with a `Reason:` line and exit 1. `--no-fetch` reads a clone's ref as last
|
|
103
|
+
fetched; a package keeps no copy of its channel, so it cannot answer offline.
|
|
104
|
+
|
|
105
|
+
**The ref is an operand.** `--ref` was split at its first slash into a remote and a
|
|
106
|
+
branch for `git fetch`, so `--upload-pack=touch X/y`, or `origin/--upload-pack=touch X`,
|
|
107
|
+
reached git as an option, and git ran the command. A test claimed this could not happen.
|
|
108
|
+
It passed only because its marker path was absolute, which the split cuts at the leading
|
|
109
|
+
`/`. The exposure was local: whoever can pass `--ref` can run commands anyway. Refs
|
|
110
|
+
beginning with `-` are now refused, and `fetch`, `ls-tree`, and `show` take
|
|
111
|
+
`--end-of-options`, which covers the half after the slash.
|
|
112
|
+
|
|
113
|
+
### What you will notice
|
|
114
|
+
|
|
115
|
+
- On a package, `release-notes pending` shows `Source: npm mirror-mind@stable (<version>)`.
|
|
116
|
+
On a clone it shows `Source: origin/<channel>`, so a clone on `main` now reads
|
|
117
|
+
`origin/main`, where it used to read `origin/stable`.
|
|
118
|
+
- A package on `main` answers `unknown`: npm has no `main` dist-tag, as `runtime update
|
|
119
|
+
--check` already says.
|
|
120
|
+
- A `⬆` that the status line kept after an update is no longer shown once the install
|
|
121
|
+
has that version.
|
|
122
|
+
|
|
123
|
+
## Upgrading
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
mirror runtime update # or: npm install -g mirror-mind
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
There is no database migration, and no runtime wiring changes. The update itself is run
|
|
130
|
+
by the version you have now, so this release's changes to it apply from the next update
|
|
131
|
+
on: npm running in the install's root, and recording `up_to_date` when it succeeds. A
|
|
132
|
+
Homebrew package on Apple Silicon reads its release notes again from the first command
|
|
133
|
+
after the update.
|
|
134
|
+
|
|
135
|
+
## Also In This Release
|
|
136
|
+
|
|
137
|
+
- **For maintainers.** `release:promote` pushes with the network budget, and a failed push
|
|
138
|
+
shows git's message with a recovery that says what is where (CR132). A `stable` release
|
|
139
|
+
now ends, in both places the route is printed, with `npm dist-tag add
|
|
140
|
+
mirror-mind@<version> latest`. The publish workflow stages under `stable` or `next` and
|
|
141
|
+
nothing else (CR125).
|
|
142
|
+
- **For contributors.** The updater smoke gives the same result on any machine, whatever
|
|
143
|
+
the shell exports (CR137). The npm package smoke installs into a prefix that is a git
|
|
144
|
+
repository, the Homebrew shape. Its `npm` is a stub after the install, so it never
|
|
145
|
+
reaches the live registry.
|
|
146
|
+
|
|
147
|
+
## Conscious Exclusions
|
|
148
|
+
|
|
149
|
+
- **`runtime status` and `runtime version` on a clone**, which still read the clone's
|
|
150
|
+
repository and channel from the cwd, so a healthy clone reads `attention needed` outside
|
|
151
|
+
its checkout. It is the same family, captured as CR138.
|
|
152
|
+
- **The `release` environment's ref check.** It accepts a dispatch from any ref, so a
|
|
153
|
+
branch's own copy of the publish workflow could stage (CR136).
|
|
154
|
+
- **A `--channel` option on `pending`**, which `update --check` and `version` have.
|
|
155
|
+
- **The `mm-release-notes` skill's wording**, which still says "the current stable
|
|
156
|
+
release", true on the default channel.
|
|
157
|
+
|
|
158
|
+
## What We Learned
|
|
159
|
+
|
|
160
|
+
A faithful port inherits its oracle's idea of where it runs. Each of these readers matched
|
|
161
|
+
the Python core's goldens, and each was wrong once the program stopped living in the
|
|
162
|
+
directory it was asked about. The tests agreed with the oracle for the same reason: they
|
|
163
|
+
ran from a clone's root, where the cwd, git, and the install coincide. The new tests for
|
|
164
|
+
the card and for `pending` run every case from two places, outside any repository and
|
|
165
|
+
inside an unrelated one, and the answer must not move.
|
|
166
|
+
|
|
167
|
+
A validation route must say which install it tests. On a machine with both a package and
|
|
168
|
+
a linked checkout, a bare `mirror` runs whichever comes first on the PATH, and both
|
|
169
|
+
report the same version.
|
|
170
|
+
|
|
171
|
+
## Next Horizon
|
|
172
|
+
|
|
173
|
+
CR138, the same defect in `runtime status` and `runtime version`, is the visible next
|
|
174
|
+
step. CR136 would harden the publish route. RS010's other hygiene requests follow.
|
package/package.json
CHANGED