mirror-mind 0.33.3 → 0.34.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 (35) hide show
  1. package/.agents/plugins/marketplace.json +16 -0
  2. package/AGENTS.md +4 -4
  3. package/docs/releases/index.md +2 -0
  4. package/docs/releases/v0.33.4.md +104 -0
  5. package/docs/releases/v0.34.0.md +142 -0
  6. package/package.json +2 -3
  7. package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
  8. package/ts/src/conversation/backfill.ts +13 -129
  9. package/ts/src/conversation/commandTurn.ts +42 -0
  10. package/ts/src/conversation/deferredCloseTail.ts +154 -0
  11. package/ts/src/conversation/logger.ts +104 -20
  12. package/ts/src/conversation/loggerCli.ts +6 -18
  13. package/ts/src/conversation/loggerRuntime.ts +6 -1
  14. package/ts/src/conversation/runtimePolicy.ts +50 -0
  15. package/ts/src/conversation/sessionComposites.ts +54 -8
  16. package/ts/src/frontDoor/cli.ts +12 -2
  17. package/ts/src/frontDoor/routing.ts +3 -2
  18. package/ts/src/frontDoor/runtimeRoute.ts +13 -1
  19. package/ts/src/guards/claudePlugin.ts +39 -2
  20. package/ts/src/guards/packContents.ts +4 -4
  21. package/ts/src/guards/retiredSurfaces.ts +36 -0
  22. package/ts/src/hooks/claude.ts +16 -4
  23. package/ts/src/hooks/gemini.ts +3 -1
  24. package/ts/src/hooks/main.ts +6 -16
  25. package/ts/src/identity/identityEdit.ts +11 -2
  26. package/ts/src/identity/identityKey.ts +18 -2
  27. package/ts/src/providers/config.ts +11 -0
  28. package/ts/src/runtime/diagnose.ts +115 -1
  29. package/ts/src/runtime/pluginWiring.ts +135 -0
  30. package/ts/src/runtime/update.ts +39 -6
  31. package/ts/src/runtime/wiring.ts +24 -10
  32. package/scripts/codex-hooks/session-end.sh +0 -85
  33. package/scripts/codex-hooks/session-start.sh +0 -85
  34. package/scripts/codex-mirror.sh +0 -48
  35. package/ts/src/hooks/codex.ts +0 -45
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "mirror-mind",
3
+ "owner": {
4
+ "name": "Mirror Mind"
5
+ },
6
+ "plugins": [
7
+ {
8
+ "name": "mirror-mind",
9
+ "source": {
10
+ "source": "local",
11
+ "path": "./plugins/mirror-mind"
12
+ },
13
+ "description": "Mirror Mind — local-first memory and identity for agentic runtimes."
14
+ }
15
+ ]
16
+ }
package/AGENTS.md CHANGED
@@ -18,10 +18,10 @@ The mirror operates in four modes, chosen automatically based on context:
18
18
  Mirror, Builder, Explorer, and Soul.
19
19
 
20
20
  Each skill below is named three ways: `/mm-<skill>` in Pi and Gemini CLI,
21
- `$mm-<skill>` in Codex, `/mm:<skill>` in Claude Code. A Claude Code **plugin**
22
- install prefixes the plugin's name — `/mirror-mind:mm:<skill>` in the menu,
23
- `mirror-mind:mm-<skill>` as the skill's own name — and the forms below refer to
24
- the same skill.
21
+ `$mm-<skill>` in Codex, `/mm:<skill>` in Claude Code. A **plugin** install
22
+ prefixes the plugin's name — `/mirror-mind:mm:<skill>` in Claude Code's menu,
23
+ `$mirror-mind:mm:<skill>` in Codex, `mirror-mind:mm-<skill>` as the skill's own
24
+ name — and the forms below refer to the same skill.
25
25
 
26
26
  **Mirror Mode** — activate for: life decisions, feelings, business strategy,
27
27
  writing, mentoring, health, existential questions, sensemaking, psychological
@@ -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.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.
16
+ - [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.
15
17
  - [v0.33.3 — Mirror Finds Itself Under Any Path](v0.33.3.md) — patch release of one Change Request, CR140, carried by a contributor's PR #42: Mirror read its own install path from `URL.pathname`, so a checkout or package under a path with a space or a non-ASCII character read as `Install: unknown`, the welcome card showed no update, and `runtime update` refused. The path is now a filesystem path, a guard keeps the idiom out, and an `unknown` install names the path it looked up from. On Windows, npm's global layout is recognized and npm runs with no shell, verified on Windows in CI for the two files they touch; there is still no Windows product. Affected installs update once by hand. No migration.
16
18
  - [v0.33.2 — Git Runs Where Mirror Lives](v0.33.2.md) — patch release of one Change Request, CR138: on a clone install, `runtime status`, `version`, and `diagnose` describe the install from any directory, and `diagnose` grades Mirror's own tree. Git now runs only in the install's own checkout, so a checkout someone else made can no longer run a `core.fsmonitor` command through these commands; `version --start PATH` is the one explicit exception. Package installs were never exposed. No migration.
17
19
  - [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.
@@ -0,0 +1,104 @@
1
+ ---
2
+ digest: >
3
+ v0.33.4 is a maintenance release of three first-run corrections, two from a
4
+ contributor's PR #43 and one Change Request, CR141, that its review opened.
5
+ `mirror init --help` created a home named `--help`; `init ../escaped` created
6
+ one outside `.mirror-minds`; `init "My User"` put a space into `MIRROR_USER`
7
+ for every wiring command. A user name is now a kebab-case key, like a journey
8
+ slug or a persona id, refused otherwise with a name that would work and
9
+ nothing created. And `identity edit` on Windows opens Notepad when no editor
10
+ is named, instead of failing to start nano. Existing homes are untouched; no
11
+ migration.
12
+ ---
13
+
14
+ # v0.33.4 — A Name Is a Key
15
+
16
+ **Date:** 2026-10-17
17
+
18
+ ## Highlights
19
+
20
+ - **`init` takes a kebab-case key for a user name, and nothing else** (CR141, from the
21
+ review of PR #43). `init ../escaped` created `~/escaped`, outside the directory the
22
+ product owns; `init "My User"` put a space into `MIRROR_USER`, which every wiring
23
+ command `init` prints then carried; `init a/b` nested. Each is refused before anything
24
+ is created, with the sentence `identity set` already uses: the consequence, the rule,
25
+ and a key that would work.
26
+ - **`init --help` no longer creates a home named `--help`** (PR #43 by
27
+ @rodrigoimmaginario). The command has no flags; a word that starts with `-` gets the
28
+ usage line and exit 2.
29
+ - **`identity edit` opens Notepad on Windows when no editor is named** (PR #43). The
30
+ order is unchanged, `EDITOR`, then `VISUAL`, then the platform's default: `notepad` on
31
+ Windows, where nano exists only inside Git Bash, `nano` elsewhere.
32
+ - **Existing homes are untouched.** `init` only creates; a home with another kind of name
33
+ keeps it.
34
+
35
+ ## Where We Started
36
+
37
+ A contributor's first run on Windows met two small things and sent a PR for each:
38
+ `init --help` created a home, and `identity edit` with no `EDITOR` set failed to start
39
+ nano. The first was not a Windows bug at all; it reproduced on macOS in a second.
40
+
41
+ Its review asked what else `init` accepted. The answer was anything. The name becomes
42
+ `~/.mirror-minds/<user>` and the value of `MIRROR_USER`, and the repository had already
43
+ decided, in CR104, that a key which goes into paths and shell commands must be one plain
44
+ word a shell cannot split, substitute, or read as an option. Journey slugs and persona
45
+ ids had met that rule since `v0.32.0`. The user name was the last key of its kind that
46
+ did not.
47
+
48
+ ## What Changed
49
+
50
+ **One sentence for every key.** `newUserNameProblem` joins `newIdentityKeyProblem` in
51
+ `ts/src/identity/identityKey.ts`, both over one builder, so `init`, `identity set`,
52
+ `identity edit`, and `seed` refuse a non-key the same way. The key is printed escaped: a
53
+ name with a newline or a `;` in it cannot forge output or a command. `runInit` asks before
54
+ `initUserHome`, so a refusal leaves `$HOME` as it was: no home, no identity directory, no
55
+ config file.
56
+
57
+ **The test is the PR's, widened.** `../escaped`, `My User`, `a/b`, and an accented name go
58
+ through the real front door; each asserts the exit, the sentence with its suggestion, and
59
+ an empty home afterwards. PR #43's own `init --help` test stands beside it.
60
+
61
+ **Notepad, with its evidence still owed.** The platform is a dependency of
62
+ `resolveEditor`, so both defaults are tested on every OS. The PR verified the Windows
63
+ round trip on an unchanged buffer, which prints `No changes detected.` whether Notepad
64
+ blocked or returned at once; early Windows 11 Notepad did return at once. A round trip
65
+ that changes the buffer is asked for on the PR, and the default ships as the better of
66
+ the two available answers: nano could not start at all.
67
+
68
+ ### What you will notice
69
+
70
+ - A first `mirror init` with a name that is not a plain lowercase word is refused with
71
+ a name that would work. `mirror init your-name`, as Getting Started has always said,
72
+ is unchanged.
73
+ - On Windows, `mirror identity edit …` opens Notepad instead of failing.
74
+ - Nothing else.
75
+
76
+ ## Upgrading
77
+
78
+ ```bash
79
+ mirror runtime update # or: npm install -g mirror-mind
80
+ ```
81
+
82
+ There is no database migration and no runtime wiring change. If `v0.33.3`'s note applied
83
+ to you, a Mirror under a path with a space or an accent, and you have not yet taken its
84
+ one manual update, take it first; from `v0.33.3` on, `runtime update` works.
85
+
86
+ ## Conscious Exclusions
87
+
88
+ - **Selecting an existing home by name.** `MIRROR_USER` pointing at a home created
89
+ before this rule, under whatever name, still selects it. Only creation is constrained.
90
+ - **`init`'s wiring lines on Windows.** They print `ln -s`, which Windows has no answer
91
+ to; that waits for the runtimes to be verified there, as the contributor's PR says.
92
+ - **Counting verdicts as front-door errors** (CR139), unchanged since `v0.33.2`.
93
+
94
+ ## What We Learned
95
+
96
+ A fix for an instance invites the question "what else?", and the cheapest moment to ask
97
+ it is the review. The `-` check was right; the class was one line away, and the rule
98
+ already existed with its sentence, its escaping, and its tests. When a repository has
99
+ decided something once, the second instance should cost a reference, not a decision.
100
+
101
+ ## Next Horizon
102
+
103
+ CR139, the verdict loop, is the visible next step in RS010. The contributor's Windows
104
+ series, if it comes, begins with the suite on `windows-latest`.
@@ -0,0 +1,142 @@
1
+ ---
2
+ digest: >
3
+ v0.34.0 is a minor release closing CV21.DS4: Codex runs Mirror through the
4
+ canonical plugin, the same one Claude Code loads. The package ships a Codex
5
+ marketplace; two commands install the plugin, Codex asks once to trust its
6
+ hooks, and from then on every session carries the skills, the Operating
7
+ Instructions, per-turn Mirror Mode injection, the MCP server, and logging as
8
+ `codex` with every reply beside its prompt — measured live at L4 on Codex
9
+ 0.163.0. The v0.32.1 wrapper wiring is removed: `scripts/codex-mirror.sh`,
10
+ its hooks, and the JSONL backfill no longer ship. Because Codex copies the
11
+ plugin into a versioned cache, `runtime diagnose` and `runtime update` now
12
+ say when that copy is behind and name the one command that brings it level.
13
+ No migration.
14
+ ---
15
+
16
+ # v0.34.0 — Codex Through the Canonical Plugin
17
+
18
+ **Date:** 2026-10-17
19
+
20
+ ## Highlights
21
+
22
+ - **Codex installs Mirror as a plugin, from a marketplace the package ships**
23
+ (CV21.DS4.US1). Two commands — `codex plugin marketplace add "<pkg>"`, then
24
+ `codex plugin add mirror-mind@mirror-mind` — and Codex asks once, at the
25
+ next session, to trust the plugin's hooks. `mirror init` prints exactly
26
+ these steps when it finds `codex` on the PATH.
27
+ - **Codex is at L4, measured live** (CV21.DS4.US4, on Codex 0.163.0): all 25
28
+ skills, the Operating Instructions as a developer message, Mirror Mode on
29
+ an ordinary turn and on `$mirror-mind:mm:mirror`, the 7 MCP tools behind
30
+ Codex's `tool_search`, and every prompt and reply logged as `codex`. Codex
31
+ detaches the TUI on `/quit` and fires SessionEnd only when its daemon reaps
32
+ the idle thread, so Mirror gained a safety net for sessions that outlive
33
+ their TUI: session maintenance closes them as orphans
34
+ (`MIRROR_STALE_ORPHAN_MINUTES`, default 30).
35
+ - **A bounded SessionEnd no longer cuts extraction** (CV21.DS4.TS2). Codex
36
+ bounds the hook at 1 s; the close now runs no LLM work, and the next
37
+ session start finishes the deferred tail. Grok's 1.5 s bound rides the same
38
+ change.
39
+ - **`$`-skill turns are command turns** (CV21.DS4.TS3), as `/`-commands are
40
+ on every other runtime, with Mirror's explicit form the one exception.
41
+ - **The v0.32.1 Codex wiring is removed** (CV21.DS4.TS5). What no longer
42
+ exists: `scripts/codex-mirror.sh`, `scripts/codex-hooks/`, the
43
+ `codex:session-*` hook entries, and
44
+ `conversation-logger backfill-codex-session` — the plugin logs Codex turns
45
+ as they happen, so nothing re-imports the transcript at exit. A
46
+ retired-surfaces guard keeps the wiring from drifting back.
47
+ - **An updated Mirror now reaches Codex's plugin cache — or says so**
48
+ (CV21.DS4.TS6). Codex copies the plugin into its cache at a version, so an
49
+ npm update changes the install but not the copy Codex runs. `runtime
50
+ diagnose` reports `codex_plugin_cache_behind` naming both versions and the
51
+ measured command — `codex plugin add mirror-mind@mirror-mind`; re-adding
52
+ refreshes the cache in place, no remove needed — and a successful `runtime
53
+ update` prints the same line. A dangling Claude Code skills link is
54
+ reported too (`claude_plugin_link_dangling`). Absence is not drift: an
55
+ unwired machine stays green.
56
+
57
+ ## Where We Started
58
+
59
+ v0.33.0 proved one plugin works as it is on Claude Code, Codex, and Grok.
60
+ But a Codex user still wired Mirror by hand: a skills link, an `AGENTS.md`
61
+ link, and a wrapper script around every session that imported the transcript
62
+ from JSONL at exit — v0.32.1's wiring, built when Codex had no hook system.
63
+ Codex 0.157 added plugins; DS4 asked whether the canonical plugin could be
64
+ the whole answer, and set a gate: the old wiring retires only once parity is
65
+ shown live.
66
+
67
+ ## What Changed
68
+
69
+ **The install is two commands.** The npm package root carries
70
+ `.agents/plugins/marketplace.json`, one local plugin — the same
71
+ `plugins/mirror-mind` Claude Code loads. Codex copies it into its cache at
72
+ the plugin's version and runs its Claude-family hooks as they are; runtime
73
+ detection reads Codex's `turn_id` and labels the session `codex`.
74
+
75
+ **The gate was met, so the old path is gone.** US4 measured L4 twice, live,
76
+ through the shipped plugin alone. TS5 then removed the wrapper, its generated
77
+ hooks, the `codex:*` hook family, the JSONL backfill (its only caller was the
78
+ wrapper's exit hook; the plugin's SessionEnd closes without reading it),
79
+ their pack-contents guards, and their CI step. The docs name the marketplace
80
+ install and nothing else.
81
+
82
+ **The one stale copy is watched.** Claude Code reads the plugin through a
83
+ link, in place; Codex runs a versioned copy. TS6 taught `runtime diagnose`
84
+ and `runtime update`'s post-update hints to compare that copy against the
85
+ installed version — the landed version, read from the just-updated tree —
86
+ and to print the one command that works on Codex 0.163.0, which has no
87
+ plugin-update verb: the re-add.
88
+
89
+ ## Upgrading
90
+
91
+ ```bash
92
+ npm install -g mirror-mind@latest # or: mirror runtime update
93
+ ```
94
+
95
+ **If Codex runs the plugin already:** after this update (and any future one),
96
+ bring the cache level once:
97
+
98
+ ```bash
99
+ codex plugin add mirror-mind@mirror-mind
100
+ ```
101
+
102
+ `mirror runtime diagnose` will tell you whenever this is needed.
103
+
104
+ **If you wired Codex before the plugin** (a skills link, an `AGENTS.md` link,
105
+ the wrapper script): install the plugin with the two commands above, then
106
+ undo the old wiring — the exact steps are in Getting Started's Codex section.
107
+ If you do nothing, everything still works, at the cost of doubled skills and
108
+ double logging where the old wrapper still runs; an older installed package
109
+ keeps its own wrapper copy, but this version ships none.
110
+
111
+ No migration. Existing homes, sessions, and memories are untouched.
112
+
113
+ ## Conscious Exclusions
114
+
115
+ - **Codex logging is no longer end-to-end tested**, by decision: the retired
116
+ wrapper smoke was the only artifact-level Codex check. Confidence now rests
117
+ on the shared Claude-family hook path (smoked), runtime-detection units,
118
+ and US4's live measurement.
119
+ - **Mirror never runs `codex` itself.** The cache readers are read-only
120
+ filesystem checks; the person runs the printed command.
121
+ - **Two named silences in the wiring checks:** a cache *ahead* of the install
122
+ (a deliberate downgrade gets no advice from the older tree), and a Claude
123
+ link that resolves to another install tree (healthy means the target
124
+ reads). Both are recorded as decisions with tests.
125
+ - **CR142 is captured, not fixed:** the Claude plugin smoke's negative probe
126
+ fails on a machine with a globally installed `mirror-hook` — a smoke
127
+ isolation bug found during TS5's validation, pre-existing, filed under
128
+ RS010.
129
+
130
+ ## What We Learned
131
+
132
+ The gate ordering did the work: retiring the old wiring only after L4 was
133
+ measured live meant the removal was a deletion, not a bet. And the one real
134
+ surprise of the arc — Codex sessions outlive their TUI — was caught by the
135
+ live measurement, not by any test, which is why the measurement is part of
136
+ the method.
137
+
138
+ ## Next Horizon
139
+
140
+ DS4 closes with this release. CV21's remaining runtime stories — Antigravity
141
+ through a native layer (DS7), Grok (DS10) — are pulled deliberately from the
142
+ CV21 index.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
- "version": "0.33.3",
3
+ "version": "0.34.0",
4
4
  "description": "Mirror Mind: a local-first memory and identity framework for agentic AI runtimes (Pi, Claude Code, Gemini CLI, Codex)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,8 +38,7 @@
38
38
  ".claude/hooks",
39
39
  ".gemini/hooks",
40
40
  "plugins/mirror-mind",
41
- "scripts/codex-hooks",
42
- "scripts/codex-mirror.sh",
41
+ ".agents/plugins",
43
42
  "AGENTS.md",
44
43
  "README.md",
45
44
  "LICENSE"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
- "version": "0.33.3",
3
+ "version": "0.34.0",
4
4
  "description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
5
5
  "author": {
6
6
  "name": "Mirror Mind"
@@ -1,20 +1,17 @@
1
1
  // Session backfills from runtime-written JSONL (CV22.DS7.US10 slice E).
2
2
  //
3
- // Ports `backfill_pi_sessions` and `backfill_codex_session` from
4
- // `src/memory/cli/conversation_logger.py`. Both import a closed session the
5
- // live hooks never saw, through the atomic seam in `sessionImport.ts`, and
6
- // both are graded against `ts/test/goldens/backfill.golden.json`.
3
+ // Ports `backfill_pi_sessions` from `src/memory/cli/conversation_logger.py`:
4
+ // it imports closed sessions the live hooks never saw, through the atomic
5
+ // seam in `sessionImport.ts`, and is graded against
6
+ // `ts/test/goldens/backfill.golden.json`. (The Codex importer,
7
+ // `backfill_codex_session`, was ported beside it and retired with the
8
+ // v0.32.1 Codex wrapper by CV21.DS4.TS5: its only caller was the wrapper's
9
+ // session-end hook, and the plugin logs Codex turns live.)
7
10
  //
8
- // The two are deliberately NOT unified beyond that seam, because Python's
9
- // asymmetries are the contract:
10
- // * Pi titles through `set_provisional_title` (whitespace collapse plus
11
- // `title_source`/`title_status` provenance in metadata); Codex titles
12
- // through `_generate_title` and a bare column write with no provenance.
13
- // * Pi tolerates a corrupt or structurally invalid file by skipping THAT
14
- // file and continuing the walk; Codex gives up on its single file.
15
- // * Pi parses every timestamp form (millisecond epoch, string, absent);
16
- // Codex stores the raw `timestamp` value and lets the model reject a
17
- // missing one.
11
+ // Pi titles through `set_provisional_title` (whitespace collapse plus
12
+ // `title_source`/`title_status` provenance in metadata), tolerates a corrupt
13
+ // or structurally invalid file by skipping THAT file and continuing the
14
+ // walk, and parses every timestamp form (millisecond epoch, string, absent).
18
15
  //
19
16
  // Nothing here touches a provider or the clock directly: `now` is injected
20
17
  // through `LoggerDeps`, which is what makes "no timestamp -> now" pinnable.
@@ -24,12 +21,11 @@
24
21
  // home meant the person's whole Pi history, copied and spent on before the
25
22
  // first prompt. The floor is explicit (`notBefore`), never defaulted, and
26
23
  // the rule fails closed: there is no value that means "everything" except
27
- // the epoch, which says so. The Codex importer is untouched: it imports the
28
- // one session the wrapper names, not a history.
24
+ // the epoch, which says so.
29
25
 
30
26
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
31
27
  import { join, relative, sep } from "node:path";
32
- import { generateTitle, type LoggerDeps, setProvisionalTitle } from "#conversation/logger.ts";
28
+ import { type LoggerDeps, setProvisionalTitle } from "#conversation/logger.ts";
33
29
  import { type ImportedMessage, importClosedConversation } from "#conversation/sessionImport.ts";
34
30
  import type { WritableDatabase } from "#db/database.ts";
35
31
  import { expandHome } from "#util/paths.ts";
@@ -76,13 +72,6 @@ function getFrom(container: unknown, key: string): unknown {
76
72
  return getOr(container, key, undefined);
77
73
  }
78
74
 
79
- /** Python truthiness for JSON values: empty containers are falsy too. */
80
- function pythonTruthy(value: unknown): boolean {
81
- if (Array.isArray(value)) return value.length > 0;
82
- if (isRecord(value)) return Object.keys(value).length > 0;
83
- return Boolean(value);
84
- }
85
-
86
75
  /** Python `_parse_pi_content`: a string as-is, text blocks joined by `\n`, else `""`. */
87
76
  export function parsePiContent(content: unknown): string {
88
77
  if (typeof content === "string") return content;
@@ -280,108 +269,3 @@ export function backfillPiSessions(
280
269
  }
281
270
  return count;
282
271
  }
283
-
284
- interface CodexParse {
285
- sessionId: string | null;
286
- messages: { role: string; content: unknown; createdAt: unknown }[];
287
- }
288
-
289
- /**
290
- * Parse a Codex session file. Throws on the first line that is not valid
291
- * JSON or not the expected shape; Python's single `try` turns that into
292
- * "return 0". Values are collected raw: Python validates them only when the
293
- * import constructs its models.
294
- */
295
- function parseCodexSessionFile(path: string): CodexParse {
296
- const parsed: CodexParse = { sessionId: null, messages: [] };
297
- for (const line of readFileSync(path, "utf8").split(/\r?\n|\r/)) {
298
- if (!line.trim()) continue;
299
- const entry: unknown = JSON.parse(line);
300
- const entryType = getFrom(entry, "type");
301
- const payload = getOr(entry, "payload", {});
302
- if (entryType === "session_meta") {
303
- // Codex writes a UUID string; a non-string id is treated as absent
304
- // (the same simplification the hook payloads make).
305
- const id = getFrom(payload, "id");
306
- parsed.sessionId = typeof id === "string" ? id : null;
307
- } else if (entryType === "event_msg") {
308
- const payloadType = getFrom(payload, "type");
309
- if (payloadType === "user_message" || payloadType === "agent_message") {
310
- const content = getOr(payload, "message", "");
311
- // Python: `if content:` -- any truthy value is kept and validated later.
312
- if (pythonTruthy(content)) {
313
- parsed.messages.push({
314
- role: payloadType === "user_message" ? "user" : "assistant",
315
- content,
316
- createdAt: getFrom(entry, "timestamp"),
317
- });
318
- }
319
- }
320
- }
321
- }
322
- return parsed;
323
- }
324
-
325
- export interface CodexBackfillOptions {
326
- jsonlPath: string;
327
- interface?: string;
328
- }
329
-
330
- /**
331
- * Python `backfill_codex_session`: import one Codex session file as a closed
332
- * conversation titled through `_generate_title`. Returns 1 on import, else 0.
333
- *
334
- * Throws when a kept event has a non-string `message` or `timestamp`: Python's
335
- * `Message(...)` model rejects those inside the import transaction, so the
336
- * database is left untouched and the caller sees the failure.
337
- */
338
- export function backfillCodexSession(
339
- db: WritableDatabase,
340
- options: CodexBackfillOptions,
341
- deps: LoggerDeps,
342
- ): number {
343
- const jsonlPath = expandHome(options.jsonlPath);
344
- if (!existsSync(jsonlPath) || !statSync(jsonlPath).isFile()) return 0;
345
-
346
- let parsed: CodexParse;
347
- try {
348
- parsed = parseCodexSessionFile(jsonlPath);
349
- } catch {
350
- return 0;
351
- }
352
- if (!parsed.sessionId || parsed.messages.length === 0) return 0;
353
- if (db.prepare("SELECT 1 FROM runtime_sessions WHERE session_id = ?").get(parsed.sessionId)) {
354
- return 0;
355
- }
356
-
357
- const messages: ImportedMessage[] = parsed.messages.map((message) => {
358
- if (typeof message.content !== "string") {
359
- throw new TypeError("Codex event message must be a string");
360
- }
361
- if (typeof message.createdAt !== "string") {
362
- throw new TypeError("Codex event timestamp must be a string");
363
- }
364
- return { role: message.role, content: message.content, createdAt: message.createdAt };
365
- });
366
-
367
- const conversationId = importClosedConversation(
368
- db,
369
- parsed.sessionId,
370
- {
371
- interface: options.interface ?? "codex",
372
- messages,
373
- endedAt: (messages.at(-1) as ImportedMessage).createdAt,
374
- },
375
- deps,
376
- );
377
- if (conversationId === null) return 0;
378
-
379
- const firstUser = messages.find((message) => message.role === "user");
380
- if (firstUser) {
381
- db.prepare("UPDATE conversations SET title = ? WHERE id = ?").run(
382
- generateTitle(firstUser.content),
383
- conversationId,
384
- );
385
- }
386
- return 1;
387
- }
@@ -0,0 +1,42 @@
1
+ // A command turn: a prompt that opens with a skill invocation, in any runtime's
2
+ // sigil (CV21.DS4.TS3).
3
+ //
4
+ // The prompt hook skips a command turn and records it, so the Stop hook skips
5
+ // its reply too (`command_turn`, CV21.DS2.US8): a skill's rendering -- a
6
+ // journey table, help text -- is not conversation, and extraction reading it
7
+ // as if the person said it is noise. What you say to the mirror is remembered;
8
+ // a skill you invoke is not. The one exception is the Mirror skill's explicit
9
+ // form, whose question IS the conversation turn (`mirrorForm.ts`); the prompt
10
+ // hook checks that form first, and a test holds every Mirror form to be a
11
+ // command turn, so the exception always sits inside this rule.
12
+ //
13
+ // The two sides are deliberately asymmetric:
14
+ //
15
+ // `/` skips ANYTHING. Every runtime uses `/` for its own built-ins as well as
16
+ // for skills (`/hooks`, `/mcp`, `/compact`), so a prompt opening with it
17
+ // is a command whatever follows. This is the rule US8 inherited.
18
+ // `$` skips only a SKILL NAME. `$` is Codex's skill sigil and nothing else
19
+ // (`$mirror-mind:mm:journeys` through the plugin, DS12.TS2 L10;
20
+ // `$mm-journeys` through a skills link), and `$5 a month` is English.
21
+ // The name is lowercase segments -- letters, digits, hyphens, and
22
+ // underscores after the first letter, since Codex discovers
23
+ // `~/.codex/skills/<dir>` by directory name -- with an optional
24
+ // plugin-name prefix joined by `:`, ending at whitespace or the end of
25
+ // the prompt, the boundary `mirrorForm` draws. A comma is not a boundary
26
+ // and a leading space is not a prompt start, as ` /help` is not a
27
+ // command today.
28
+ //
29
+ // One rule for every runtime: a Claude Code user who types `$mm-journeys`,
30
+ // which Claude Code does not understand, is still invoking a skill, and that
31
+ // turn is not conversation either. The rule has one home for every hook that
32
+ // runs in our process (the Claude-family logger, the Gemini hook); the Pi
33
+ // extension, a separate process in Pi's file, keeps its own `/` check.
34
+
35
+ const SKILL_SEGMENT = "[a-z][a-z0-9_-]*";
36
+ const DOLLAR_SKILL_FORM = new RegExp(`^\\$${SKILL_SEGMENT}(?::${SKILL_SEGMENT})*(?=\\s|$)`);
37
+
38
+ /** Whether the prompt opens with a skill invocation in any runtime's sigil. */
39
+ export function isCommandTurn(prompt: string): boolean {
40
+ if (prompt.startsWith("/")) return true;
41
+ return DOLLAR_SKILL_FORM.test(prompt);
42
+ }