mirror-mind 0.39.1 → 0.39.3

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.
@@ -59,9 +59,15 @@ Update identity content directly in the database.
59
59
 
60
60
  ```bash
61
61
  mirror identity set <layer> <key> --content "..." [--mirror-home PATH]
62
+ mirror identity set <layer> <key> --file <path> [--mirror-home PATH]
62
63
  ```
63
64
 
64
- If `--content` is omitted, content is read from stdin.
65
+ If neither `--content` nor `--file` is given, content is read from stdin.
66
+
67
+ For anything longer than a line, write the text to a file and pass `--file`:
68
+ Mirror reads it as bytes, so the shell never splits, substitutes, or re-encodes
69
+ it. On Windows PowerShell 5.1 it is the only safe route: the pipe is ASCII by
70
+ default and turns every accented letter into `?`. The pipe stays for one-liners.
65
71
 
66
72
  A new `journey` or `persona` key must be kebab-case: lowercase letters, digits,
67
73
  and single hyphens, up to 80 characters, such as `product-launch`. Agents put
@@ -70,7 +76,8 @@ one that would work. A key created before this rule keeps working.
70
76
 
71
77
  **Examples:**
72
78
  - `mirror identity set ego behavior --content "Be direct."`
73
- - `cat new-soul.md | mirror identity set self soul`
79
+ - `mirror identity set self soul --file new-soul.md`
80
+ - `echo "Be direct." | mirror identity set ego behavior` (a one-liner; not on PowerShell 5.1)
74
81
 
75
82
  ---
76
83
 
@@ -16,7 +16,9 @@ When receiving `/mm-journey update <slug> <content>`: answered by the TS core (C
16
16
 
17
17
  ```bash
18
18
  mirror journey update <slug> "<content>"
19
- # or pipe via stdin:
19
+ # longer than a line: write it to a file, read as bytes, untouched by the shell
20
+ mirror journey update <slug> --file <path>
21
+ # a one-liner through stdin (not on Windows PowerShell 5.1, whose pipe is ASCII):
20
22
  echo "<content>" | mirror journey update <slug> -
21
23
  ```
22
24
 
package/README.md CHANGED
@@ -76,6 +76,9 @@ in your machine, not a server.
76
76
  - [Node.js 24+](https://nodejs.org/) — runs Mirror Mind's core
77
77
  (`node --version` must be ≥ v24; POSIX/macOS/Linux supported). Nothing
78
78
  else to install: there is no Python and no build step
79
+ - On Windows: [Mirror Mind for Windows](https://github.com/mirror-mind-ai/mirror-installer),
80
+ a separate per-user installer that brings Node, Git Bash, Pi and Mirror in
81
+ one pass. It lives in its own repository, outside this package and its CI
79
82
  - [OpenRouter](https://openrouter.ai) account with at least $5 in credits —
80
83
  embeddings, memory extraction, and multi-LLM
81
84
  - An AI runtime subscription:
@@ -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.39.3 — Windows Gets Its Hooks](v0.39.3.md) — patch release of PR #53, PR #50, and PR #54 (@rodrigoimmaginario): the plugin's hook wrappers and MCP locator run the package's own bin when npm's global entry is a shell shim, so Claude Code hooks and the MCP server work on a Windows package install; `runtime diagnose` no longer counts its own verdict as a front-door error (CR139); the docs point a Windows reader to the installer in its own repository. No migration.
16
+ - [v0.39.2 — Input, Read the Platform's Way](v0.39.2.md) — patch release of PR #49 and PR #48 (@rodrigoimmaginario), CR194, and CR193: `runtime diagnose` stops warning every Windows install about Node; a PowerShell byte-order mark is not stored; stdin is decoded by its mark, UTF-8 or UTF-16, and `identity set` and `journey update` gain `--file`, the route around PowerShell 5.1's ASCII pipe; `package-lock.json` names the package at its version, and the release doctor checks it. No migration.
15
17
  - [v0.39.1 — Pi Finds Its Node, or Says So](v0.39.1.md) — patch release of PR #47 (@rodrigoimmaginario) and CR191: a Pi opened where `node` is not on the PATH no longer exits; the Pi extension resolves Node where the hook wrappers look, so a global install needs nothing configured; when no Node resolves, one warning names the fix and `runtime diagnose` reports the `hooks.log` line. No migration.
16
18
  - [v0.39.0 — The Core as a Library](v0.39.0.md) — minor release of CV24's first eight Delivery Stories: `mirror-mind/desktop`, a library entry through which a host such as Mirror Desktop 0.3 embeds this core; calls for a home's migration state, Conversations, sidebar reads, home creation, migration on request, Mirror's context, learning from open Conversations, and Journey administration, each with an explicit path and no environment. The new `exports` field closes every other deep import. No migration.
17
19
  - [v0.38.1 — The Refusal Names the Way Out](v0.38.1.md) — patch release of three Change Requests, CR185, CR186, and CR187: a package with no home configured names the homes it finds and the `mirror init <name>` that selects one, and `init` adopts a home that already exists; a missing or refused OpenRouter key is one line that names the file, before anything is touched; a `stable` clone ahead only by `main`'s commits is told that `stable` is behind it, not to push or reset. No migration.
@@ -0,0 +1,113 @@
1
+ ---
2
+ digest: >
3
+ v0.39.2 is a patch release about input read the platform's way, most of it on Windows.
4
+ `runtime diagnose` stops warning every Windows install that Node is unresolvable, and a
5
+ PowerShell byte-order mark is no longer stored as identity content (PR #49, Rodrigo
6
+ Immaginario). From that review, CR194: text on stdin is decoded by its mark, UTF-8 or
7
+ UTF-16, and `identity set` and `journey update` gain `--file`, the route for any text
8
+ longer than a line and the only safe one on Windows PowerShell 5.1, whose pipe is ASCII.
9
+ `package-lock.json` names the package at its version after nine releases that left it
10
+ behind (PR #48), and the release doctor now checks it (CR193). No migration, and no model
11
+ or prompt change.
12
+ ---
13
+
14
+ # v0.39.2 — Input, Read the Platform's Way
15
+
16
+ **Date:** 2026-11-03
17
+
18
+ ## Highlights
19
+
20
+ - **`runtime diagnose` no longer warns every Windows install that Node is unresolvable**
21
+ ([PR #49](https://github.com/mirror-mind-ai/mirror/pull/49), by @rodrigoimmaginario).
22
+ It split the `PATH` on `:` and looked for `node`; on Windows that is `;` and `node.exe`,
23
+ so nothing ever matched. It now looks the platform's way, and the test proves the Windows
24
+ branch on every host.
25
+ - **A PowerShell byte-order mark is not content** (PR #49). `identity set` and
26
+ `journey update -` drop the one at the head of stdin instead of storing it as the first
27
+ character of an entry.
28
+ - **Text on stdin is decoded by its mark, and `--file` reads a file as bytes**
29
+ ([CR194](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr194-powershell-stdin-reaches-identity-set-as-utf-16-or-ascii.md)).
30
+ UTF-16, as Windows PowerShell 5.1's `>` writes it, is read as UTF-16. The pipe's ASCII
31
+ default, which turns every accented letter into `?`, cannot be detected, so
32
+ `identity set <layer> <key> --file <path>` and `journey update <slug> --file <path>` read
33
+ the file themselves: the route for anything longer than a line on every platform, and
34
+ the only safe one on PowerShell 5.1. The troubleshooting page names the three PowerShell
35
+ behaviors and the way around each.
36
+ - **`package-lock.json` names `mirror-mind` at the package's version**
37
+ ([PR #48](https://github.com/mirror-mind-ai/mirror/pull/48), by @rodrigoimmaginario),
38
+ with a test that keeps it so. Its root had said `mirror-core 0.0.0` through nine releases,
39
+ and the first `npm install` on any clone left the tree dirty.
40
+ - **The release doctor fails on a lock left behind**
41
+ ([CR193](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr193-the-release-doctor-does-not-check-the-lockfile.md)),
42
+ before anything is tagged, naming the command that levels it. This is the first release it
43
+ checked.
44
+ - No migration, and no model or prompt change.
45
+
46
+ ## Where We Started
47
+
48
+ Rodrigo Immaginario's PRs #48 and #49 had waited since 2026-10-19 and 2026-10-21. One
49
+ described a lockfile that nine releases in a row had left behind, each rebase of the PR
50
+ proving it again, the last of them v0.39.1 the afternoon before it merged. The other
51
+ described two inputs misread on Windows: a `PATH` split as if it were POSIX, so `runtime
52
+ diagnose` prescribed `MIRROR_NODE` to every healthy Windows install, and a byte-order mark
53
+ stored as the first character of an identity entry, repaired by hand on 2026-10-17.
54
+
55
+ Both were merged as written, each with one maintainer commit: the lock brought to the
56
+ current version so the PR's own test was green at merge, and `hookNodeFindings` given the
57
+ platform as a parameter so the branch the PR exists for is proved on macOS and Ubuntu,
58
+ where CI runs the suite, and not only on a Windows machine.
59
+
60
+ The review of PR #49 found the two hazards the BOM fix sat beside. In Windows PowerShell
61
+ 5.1, `>` writes UTF-16LE, and the pipe to a native command goes through `$OutputEncoding`,
62
+ whose default is ASCII: Portuguese text arrives with every accent as `?`, and Mirror stored
63
+ what arrived. The second is not detectable — a `?` is a character — which is what shaped
64
+ CR194.
65
+
66
+ ## What Changed
67
+
68
+ **One decoder for human text** (`ts/src/frontDoor/textInput.ts`). A UTF-8 or UTF-16 mark
69
+ at the head of the bytes chooses the decoding and is dropped; nothing else about the text
70
+ is touched. UTF-16 without a mark is not guessed at. `identity set` and `journey update -`
71
+ read stdin through it.
72
+
73
+ **`--file`, the route around the pipe.** Mirror reads the file as bytes, so the shell never
74
+ splits, substitutes, or re-encodes the text. `--file` beside `--content`, or beside `-`, is
75
+ refused in one line; a path that cannot be read is refused naming the path and nothing from
76
+ the error; nothing is written in either case. The skills present `--file` for anything
77
+ longer than a line, and keep the pipe for one-liners.
78
+
79
+ **`runtime diagnose` on Windows** looks for `node.exe` on a `;`-separated `PATH`, and only
80
+ `node.exe`: on Windows the executable check is an existence check, and an extensionless
81
+ `node` is nothing the system would run.
82
+
83
+ **The lockfile.** Regenerated once with `npm install --package-lock-only --ignore-scripts`;
84
+ `packageLock.test.ts` fails CI when its root and `package.json` disagree; the release doctor
85
+ makes the same four comparisons before the tag exists, and the versioning page names the
86
+ lock among what a release commit changes.
87
+
88
+ Validated on 2026-11-03 against a scratch copy of the maintainer's database: UTF-8, UTF-8
89
+ with a mark, and UTF-16LE as PowerShell 5.1 writes it, through `--file` and through stdin;
90
+ the refusals writing nothing; the one-liner pipe unchanged.
91
+
92
+ ## Conscious Exclusions
93
+
94
+ - Windows PowerShell 5.1 itself was not run: there is no Windows machine on the maintainer's
95
+ side. The decoder's cases are pinned byte for byte; a measurement on the OS is asked of
96
+ the reporter.
97
+ - Detecting text the ASCII pipe damaged: not possible without guessing.
98
+ - The hook wrappers and the MCP locator still accept a relative `MIRROR_NODE`
99
+ ([CR192](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr192-the-hook-wrappers-and-the-mcp-locator-accept-a-relative-mirror-node.md),
100
+ captured).
101
+ - The JSON stdin readers (`conversations append`, the hooks' payloads) are unchanged: their
102
+ input comes from runtimes, which write UTF-8 without a mark.
103
+
104
+ ## Upgrading
105
+
106
+ ```bash
107
+ mirror runtime update
108
+ ```
109
+
110
+ ## Next Horizon
111
+
112
+ Rodrigo Immaginario's PRs #50, #53, and #54, all Windows and packaging; CR190 and CR192 in
113
+ RS010's backlog. CV24 continues on its branch.
@@ -0,0 +1,94 @@
1
+ ---
2
+ digest: >
3
+ v0.39.3 is a patch release of three pull requests by Rodrigo Immaginario. On a Windows
4
+ package install, npm's global `mirror` and `mirror-hook` are shell shims, and the plugin's
5
+ hook wrappers and MCP locator handed them to Node: no Claude Code conversation was ever
6
+ logged there, and the MCP server died on arrival. They now run the package's own bin
7
+ (PR #53). `runtime diagnose` no longer counts its own "attention needed" exit as a
8
+ front-door error, so the finding stops feeding itself (PR #50, CR139). The docs point a
9
+ Windows reader to the installer that now lives in its own repository (PR #54). No
10
+ migration, and no model or prompt change.
11
+ ---
12
+
13
+ # v0.39.3 — Windows Gets Its Hooks
14
+
15
+ **Date:** 2026-11-03
16
+
17
+ ## Highlights
18
+
19
+ - **Claude Code hooks and the MCP server work on a Windows package install**
20
+ ([PR #53](https://github.com/mirror-mind-ai/mirror/pull/53), by @rodrigoimmaginario). On
21
+ Windows npm writes no symlink: its global `mirror-hook` and `mirror` are POSIX shell
22
+ shims with `node_modules/` beside them. Handed to Node, the shim ran as JavaScript:
23
+ every hook ended `node exited 1` in `hooks.log`, and the MCP server died with a
24
+ `SyntaxError` from the shim's second line. The wrappers and the locator now run the bin
25
+ the shim wraps, one fixed path away, when it is there. A POSIX prefix is unchanged.
26
+ - **`runtime diagnose` no longer counts its own verdict as a front-door error**
27
+ ([PR #50](https://github.com/mirror-mind-ai/mirror/pull/50), by @rodrigoimmaginario;
28
+ [CR139](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr139-a-verdict-that-exits-1-is-logged-as-a-front-door-error.md)).
29
+ Its exit 1 means "attention needed", a contract with scripts, and the front door logged
30
+ it as `ERROR`, which `front_door_errors` then counted: one more error per run, a finding
31
+ that could never clear. The exit is now logged at `INFO` as a verdict; a real failure of
32
+ the same command stays `ERROR`.
33
+ - **A Windows reader is pointed to the installer**
34
+ ([PR #54](https://github.com/mirror-mind-ai/mirror/pull/54), by @rodrigoimmaginario):
35
+ [mirror-mind-ai/mirror-installer](https://github.com/mirror-mind-ai/mirror-installer), a
36
+ per-user installer in its own repository, from README, Getting Started, and REFERENCE's
37
+ platform envelope. The envelope itself is unchanged: this repository ships no Windows
38
+ product, and POSIX is what is supported.
39
+ - No migration, and no model or prompt change.
40
+
41
+ ## Where We Started
42
+
43
+ v0.39.2 shipped the morning's Windows fixes: `runtime diagnose` reading the PATH the
44
+ platform's way, and PowerShell's marks and encodings on stdin. Three of Rodrigo's PRs
45
+ remained, and the largest of them was the one that mattered most: on a Windows package
46
+ install, Mirror's Claude Code integration had never worked, and nothing said so but
47
+ `hooks.log`.
48
+
49
+ The diagnose loop in PR #50 had been in the backlog three times over, CR127, CR139, and
50
+ CR176, the last two captured without finding the first. The PR implements CR139's first
51
+ option as written, arrived at independently. CR139 is done with Rodrigo as its Driver;
52
+ CR127 and CR176 are closed as its duplicates.
53
+
54
+ ## What Changed
55
+
56
+ **The shim rule, in the two generators.** `scripts/generate_hook_wrappers.sh` and
57
+ `MCP_LOCATOR` in `ts/src/guards/claudePlugin.ts` each gain one rule: when
58
+ `<dir of the entry>/node_modules/mirror-mind/bin/<entry>.js` exists, that is the entry.
59
+ The twelve wrappers and manifests are regenerated from it, and regenerating on the merge
60
+ leaves the tree unchanged. A hook test models the Windows prefix on POSIX with a fake
61
+ Node that records the entry it was handed; a maintainer commit added the locator's
62
+ run-as-written case, which fails on the previous locator.
63
+
64
+ **The verdict, beside the command.** `isRuntimeVerdict` in `runtimeRoute.ts` says which
65
+ exit is an answer; the dispatcher asks it. A maintainer commit moved the rule there from
66
+ the dispatcher, and made the end-to-end test's finding sure on purpose (`loose_permissions`
67
+ by `chmod`), since v0.39.1's Node search no longer guarantees the one the test had relied
68
+ on.
69
+
70
+ ## Conscious Exclusions
71
+
72
+ - Windows is not a supported platform of this repository: CI runs the suite on macOS and
73
+ Ubuntu, and the Windows job runs two files. The installer repository pins the core it
74
+ tests against. Whether that changes is a later decision.
75
+ - The front-door log's `route=usage` lines still count as errors (CR139's second option,
76
+ not taken). On the maintainer's home every such line comes from the test suite:
77
+ [CR195](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr195-the-test-suite-writes-four-lines-into-the-real-home-s-front-door-log.md),
78
+ captured.
79
+ - One macOS CI run on PR #54 failed in the updater smoke's package lane with a bare exit
80
+ 1 and passed on re-run. Noted; not yet understood.
81
+
82
+ ## Upgrading
83
+
84
+ ```bash
85
+ mirror runtime update
86
+ ```
87
+
88
+ On Windows, the installer at mirror-mind-ai/mirror-installer pins the core version it
89
+ ships; this release is the one its Claude Code hooks need.
90
+
91
+ ## Next Horizon
92
+
93
+ Alan Nascimento's PR #51 (the stop hook losing a reply) and Moacir Moda's PR #27; CR190,
94
+ CR192, and CR195 in RS010's backlog. CV24 continues on its branch.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
- "version": "0.39.1",
3
+ "version": "0.39.3",
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.39.1",
3
+ "version": "0.39.3",
4
4
  "description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
5
5
  "author": {
6
6
  "name": "Mirror Mind"
@@ -10,7 +10,7 @@
10
10
  "command": "/bin/sh",
11
11
  "args": [
12
12
  "-c",
13
- "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"
13
+ "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; }; mmjs=\"$(dirname \"$mmbin\")/node_modules/mirror-mind/bin/mirror.js\"; [ -f \"$mmjs\" ] && mmbin=\"$mmjs\"; 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"
14
14
  ]
15
15
  }
16
16
  }
@@ -61,6 +61,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
61
61
  [ -n "$ENTRY" ] && break
62
62
  [ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
63
63
  done
64
+ # On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
65
+ # a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
66
+ # shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
67
+ # package's own bin, so that is the entry when it is there. A POSIX prefix
68
+ # keeps node_modules under lib/ and its bin is already the .js: no change.
69
+ shim_entry=""
70
+ [ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
71
+ [ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
64
72
 
65
73
  if [ -z "$ENTRY" ]; then
66
74
  note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
@@ -61,6 +61,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
61
61
  [ -n "$ENTRY" ] && break
62
62
  [ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
63
63
  done
64
+ # On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
65
+ # a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
66
+ # shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
67
+ # package's own bin, so that is the entry when it is there. A POSIX prefix
68
+ # keeps node_modules under lib/ and its bin is already the .js: no change.
69
+ shim_entry=""
70
+ [ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
71
+ [ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
64
72
 
65
73
  if [ -z "$ENTRY" ]; then
66
74
  note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
@@ -61,6 +61,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
61
61
  [ -n "$ENTRY" ] && break
62
62
  [ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
63
63
  done
64
+ # On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
65
+ # a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
66
+ # shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
67
+ # package's own bin, so that is the entry when it is there. A POSIX prefix
68
+ # keeps node_modules under lib/ and its bin is already the .js: no change.
69
+ shim_entry=""
70
+ [ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
71
+ [ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
64
72
 
65
73
  if [ -z "$ENTRY" ]; then
66
74
  note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
@@ -56,6 +56,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
56
56
  [ -n "$ENTRY" ] && break
57
57
  [ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
58
58
  done
59
+ # On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
60
+ # a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
61
+ # shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
62
+ # package's own bin, so that is the entry when it is there. A POSIX prefix
63
+ # keeps node_modules under lib/ and its bin is already the .js: no change.
64
+ shim_entry=""
65
+ [ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
66
+ [ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
59
67
 
60
68
  if [ -z "$ENTRY" ]; then
61
69
  note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
@@ -56,6 +56,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
56
56
  [ -n "$ENTRY" ] && break
57
57
  [ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
58
58
  done
59
+ # On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
60
+ # a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
61
+ # shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
62
+ # package's own bin, so that is the entry when it is there. A POSIX prefix
63
+ # keeps node_modules under lib/ and its bin is already the .js: no change.
64
+ shim_entry=""
65
+ [ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
66
+ [ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
59
67
 
60
68
  if [ -z "$ENTRY" ]; then
61
69
  note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
@@ -56,6 +56,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
56
56
  [ -n "$ENTRY" ] && break
57
57
  [ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
58
58
  done
59
+ # On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
60
+ # a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
61
+ # shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
62
+ # package's own bin, so that is the entry when it is there. A POSIX prefix
63
+ # keeps node_modules under lib/ and its bin is already the .js: no change.
64
+ shim_entry=""
65
+ [ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
66
+ [ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
59
67
 
60
68
  if [ -z "$ENTRY" ]; then
61
69
  note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
@@ -56,6 +56,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
56
56
  [ -n "$ENTRY" ] && break
57
57
  [ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
58
58
  done
59
+ # On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
60
+ # a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
61
+ # shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
62
+ # package's own bin, so that is the entry when it is there. A POSIX prefix
63
+ # keeps node_modules under lib/ and its bin is already the .js: no change.
64
+ shim_entry=""
65
+ [ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
66
+ [ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
59
67
 
60
68
  if [ -z "$ENTRY" ]; then
61
69
  note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
@@ -56,6 +56,14 @@ for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
56
56
  [ -n "$ENTRY" ] && break
57
57
  [ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
58
58
  done
59
+ # On Windows npm writes no symlink: the `mirror-hook` in its global prefix is
60
+ # a POSIX shell shim, and `node_modules/` sits beside it. Node cannot run the
61
+ # shim ("node exited 1" in hooks.log on every hook); the entry it wraps is the
62
+ # package's own bin, so that is the entry when it is there. A POSIX prefix
63
+ # keeps node_modules under lib/ and its bin is already the .js: no change.
64
+ shim_entry=""
65
+ [ -n "$ENTRY" ] && shim_entry="$(dirname "$ENTRY")/node_modules/mirror-mind/bin/mirror-hook.js"
66
+ [ -n "$shim_entry" ] && [ -f "$shim_entry" ] && ENTRY="$shim_entry"
59
67
 
60
68
  if [ -z "$ENTRY" ]; then
61
69
  note "mirror-hook not found on PATH; hook skipped. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory."
@@ -4,7 +4,7 @@
4
4
  "command": "/bin/sh",
5
5
  "args": [
6
6
  "-c",
7
- "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"
7
+ "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; }; mmjs=\"$(dirname \"$mmbin\")/node_modules/mirror-mind/bin/mirror.js\"; [ -f \"$mmjs\" ] && mmbin=\"$mmjs\"; 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"
8
8
  ]
9
9
  }
10
10
  }
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
3
  "description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
4
- "version": "0.39.1",
4
+ "version": "0.39.3",
5
5
  "mcpServers": {
6
6
  "mirror-mind": {
7
7
  "command": "/bin/sh",
8
8
  "args": [
9
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"
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; }; mmjs=\"$(dirname \"$mmbin\")/node_modules/mirror-mind/bin/mirror.js\"; [ -f \"$mmjs\" ] && mmbin=\"$mmjs\"; 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
11
  ]
12
12
  }
13
13
  }
@@ -59,9 +59,15 @@ Update identity content directly in the database.
59
59
 
60
60
  ```bash
61
61
  mirror identity set <layer> <key> --content "..." [--mirror-home PATH]
62
+ mirror identity set <layer> <key> --file <path> [--mirror-home PATH]
62
63
  ```
63
64
 
64
- If `--content` is omitted, content is read from stdin.
65
+ If neither `--content` nor `--file` is given, content is read from stdin.
66
+
67
+ For anything longer than a line, write the text to a file and pass `--file`:
68
+ Mirror reads it as bytes, so the shell never splits, substitutes, or re-encodes
69
+ it. On Windows PowerShell 5.1 it is the only safe route: the pipe is ASCII by
70
+ default and turns every accented letter into `?`. The pipe stays for one-liners.
65
71
 
66
72
  A new `journey` or `persona` key must be kebab-case: lowercase letters, digits,
67
73
  and single hyphens, up to 80 characters, such as `product-launch`. Agents put
@@ -70,7 +76,8 @@ one that would work. A key created before this rule keeps working.
70
76
 
71
77
  **Examples:**
72
78
  - `mirror identity set ego behavior --content "Be direct."`
73
- - `cat new-soul.md | mirror identity set self soul`
79
+ - `mirror identity set self soul --file new-soul.md`
80
+ - `echo "Be direct." | mirror identity set ego behavior` (a one-liner; not on PowerShell 5.1)
74
81
 
75
82
  ---
76
83
 
@@ -30,7 +30,15 @@ If the journey path appears outdated relative to recent conversations and memori
30
30
  mirror journey update JOURNEY "UPDATED_CONTENT"
31
31
  ```
32
32
 
33
- For long content, use stdin:
33
+ For anything longer than a line, write it to a file and pass `--file`: Mirror
34
+ reads it as bytes, untouched by the shell (on Windows PowerShell 5.1 the pipe
35
+ is ASCII by default, and this is the only safe route):
36
+
37
+ ```bash
38
+ mirror journey update JOURNEY --file PATH
39
+ ```
40
+
41
+ A one-liner can go through stdin:
34
42
 
35
43
  ```bash
36
44
  echo "CONTENT" | mirror journey update JOURNEY -
@@ -80,6 +80,7 @@ import { configFilePath, loadConfiguration, silenceExperimentalWarnings } from "
80
80
  import { runSeed } from "#seed/seed.ts";
81
81
  import { getTasksForWeek, listTasks } from "#tasks/taskStore.ts";
82
82
  import { computeWeekRange } from "#tasks/weekView.ts";
83
+ import { expandHome } from "#util/paths.ts";
83
84
  import { PROGRAM } from "#util/program.ts";
84
85
  import { newId, nowIso } from "#util/pyGenerators.ts";
85
86
  import { hasOption, optionValue, stripOptionWithValue } from "./args.ts";
@@ -169,7 +170,7 @@ import {
169
170
  } from "./render/tasksImportSync.ts";
170
171
  import { renderWeekView } from "./render/week.ts";
171
172
  import { retiredRefusal, routeMemoryCommand } from "./routing.ts";
172
- import { runRuntimeReadRoute, runWelcomeRoute } from "./runtimeRoute.ts";
173
+ import { isRuntimeVerdict, runRuntimeReadRoute, runWelcomeRoute } from "./runtimeRoute.ts";
173
174
  import { runMemorySearchRoute } from "./searchRoute.ts";
174
175
  import { resolveSeedPaths } from "./seedPaths.ts";
175
176
  import { isSessionStartWelcome, runSessionStartWelcomeRoute } from "./sessionStartWelcomeRoute.ts";
@@ -187,6 +188,7 @@ import {
187
188
  applyTasksStatusChange,
188
189
  type TaskStatusTarget,
189
190
  } from "./tasksWriteRoute.ts";
191
+ import { readStdinText, readTextFile } from "./textInput.ts";
190
192
  import { answerUsage } from "./usage.ts";
191
193
 
192
194
  /**
@@ -522,12 +524,19 @@ function runShadowRead(db: Database, args: readonly string[]): number {
522
524
  return 0;
523
525
  }
524
526
 
527
+ /** Stdin as human text, decoded by its mark (PR #49, CR194: `textInput.ts`). */
525
528
  function readStdinContent(): string {
526
- try {
527
- return readFileSync(0, "utf8");
528
- } catch {
529
- return "";
530
- }
529
+ return readStdinText();
530
+ }
531
+
532
+ /**
533
+ * CR194: `--file <path>`, the text read by Mirror as bytes, so the shell's pipe
534
+ * never touches it (PowerShell 5.1's pipe is ASCII by default, and its `>` is
535
+ * UTF-16LE). `~` expands as it does for `--mirror-home`; a relative path is
536
+ * the cwd's. The refusal names the path and nothing from the error.
537
+ */
538
+ function readFileContent(path: string): { text: string } | { error: string } {
539
+ return readTextFile(expandHome(path));
531
540
  }
532
541
 
533
542
  /** `conversations append` (CV22.DS7.US10 slice B′). */
@@ -840,7 +849,10 @@ function refusedNewIdentityKey(args: readonly string[], layer: string, key: stri
840
849
  function runIdentityWrite(argv: readonly string[]): number {
841
850
  const args = argv.slice(2);
842
851
  const positionals = stripOptionWithValue(
843
- stripOptionWithValue(stripOptionWithValue(args, "--content"), "--db-path"),
852
+ stripOptionWithValue(
853
+ stripOptionWithValue(stripOptionWithValue(args, "--content"), "--file"),
854
+ "--db-path",
855
+ ),
844
856
  "--mirror-home",
845
857
  );
846
858
  const layer = positionals[0];
@@ -854,7 +866,23 @@ function runIdentityWrite(argv: readonly string[]): number {
854
866
  console.error(`Error: ${refused}`);
855
867
  return 1;
856
868
  }
857
- const content = optionValue(args, "--content") ?? readStdinContent();
869
+ const inlineContent = optionValue(args, "--content");
870
+ const filePath = optionValue(args, "--file");
871
+ if (inlineContent !== null && filePath !== null) {
872
+ console.error("Error: --file and --content name two sources for one content; pass one.");
873
+ return 1;
874
+ }
875
+ let content: string;
876
+ if (filePath !== null) {
877
+ const read = readFileContent(filePath);
878
+ if ("error" in read) {
879
+ console.error(`Error: ${read.error}`);
880
+ return 1;
881
+ }
882
+ content = read.text;
883
+ } else {
884
+ content = inlineContent ?? readStdinContent();
885
+ }
858
886
  if (!content.trim()) {
859
887
  console.error("Error: content is empty.");
860
888
  return 1;
@@ -1007,22 +1035,36 @@ function runJourneyWrite(argv: readonly string[]): number {
1007
1035
  */
1008
1036
  const JOURNEY_UPDATE_USAGE =
1009
1037
  `Usage: ${PROGRAM} journey update <slug> <content>\n` +
1010
- " Pass '-' as <content> to read it from stdin.";
1038
+ " Pass '-' as <content> to read it from stdin, or --file <path> to read a file.";
1011
1039
  const FLAG_SHAPED_CONTENT = /^--?[A-Za-z]/;
1012
1040
 
1013
1041
  function runJourneyUpdateWrite(argv: readonly string[]): number {
1014
1042
  const args = argv.slice(2);
1015
1043
  const positionals = stripOptionWithValue(
1016
- stripOptionWithValue(args, "--db-path"),
1044
+ stripOptionWithValue(stripOptionWithValue(args, "--file"), "--db-path"),
1017
1045
  "--mirror-home",
1018
1046
  );
1019
1047
  const slug = positionals[0];
1020
1048
  let content = positionals[1];
1021
- if (!slug || content === undefined) {
1049
+ const filePath = optionValue(args, "--file");
1050
+ if (!slug || (content === undefined && filePath === null)) {
1022
1051
  console.error(JOURNEY_UPDATE_USAGE);
1023
1052
  return 1;
1024
1053
  }
1025
- if (content === "-") {
1054
+ if (filePath !== null) {
1055
+ if (content !== undefined) {
1056
+ console.error(
1057
+ `Error: --file and ${content === "-" ? "'-'" : "<content>"} name two sources for one content; pass one.`,
1058
+ );
1059
+ return 1;
1060
+ }
1061
+ const read = readFileContent(filePath);
1062
+ if ("error" in read) {
1063
+ console.error(`Error: ${read.error}`);
1064
+ return 1;
1065
+ }
1066
+ content = read.text;
1067
+ } else if (content === "-") {
1026
1068
  content = readStdinContent();
1027
1069
  } else if (FLAG_SHAPED_CONTENT.test(content)) {
1028
1070
  console.error(
@@ -1669,6 +1711,8 @@ interface DispatchOutcome {
1669
1711
  exitCode: number;
1670
1712
  /** Content-free route metadata for the durable front-door log. */
1671
1713
  detail?: string;
1714
+ /** The non-zero exit is the command's answer, not a failure (`frontDoorLog.ts`). */
1715
+ verdict?: boolean;
1672
1716
  }
1673
1717
 
1674
1718
  async function dispatch(argv: readonly string[]): Promise<DispatchOutcome> {
@@ -1777,7 +1821,13 @@ async function dispatchTs(argv: readonly string[]): Promise<number | DispatchOut
1777
1821
  // database, when at all, through a read-only handle -- no bootstrap, no
1778
1822
  // migrate-on-open, and for `welcome --status-line` no subprocess whatsoever.
1779
1823
  if (argv[0] === "welcome") return runWelcomeRoute(argv);
1780
- if (argv[0] === "runtime") return runRuntimeReadRoute(argv);
1824
+ if (argv[0] === "runtime") {
1825
+ const exitCode = await runRuntimeReadRoute(argv);
1826
+ // A verdict, not a failure: `isRuntimeVerdict` says which (CR139).
1827
+ return isRuntimeVerdict(argv, exitCode)
1828
+ ? { exitCode, detail: "verdict=attention", verdict: true }
1829
+ : exitCode;
1830
+ }
1781
1831
  if (argv[0] === "mcp") return runMcpServer();
1782
1832
  if (isMemorySearch(argv)) return runMemorySearch(argv);
1783
1833
  if (isConsult(argv)) {
@@ -1853,6 +1903,7 @@ export async function main(rawArgv = process.argv.slice(2)): Promise<number> {
1853
1903
  route: decision.engine,
1854
1904
  exitCode: outcome.exitCode,
1855
1905
  ...(outcome.detail ? { detail: outcome.detail } : {}),
1906
+ ...(outcome.verdict ? { verdict: true } : {}),
1856
1907
  });
1857
1908
  return outcome.exitCode;
1858
1909
  } catch (error) {
@@ -29,6 +29,13 @@ export interface FrontDoorLogEntry {
29
29
  exitCode: number;
30
30
  /** A short, content-free note or error category. Never argument values. */
31
31
  detail?: string;
32
+ /**
33
+ * The non-zero exit is the command's answer, not a failure: `runtime
34
+ * diagnose` exits 1 to say "attention needed". Logged at INFO, so the line
35
+ * does not count as a front-door error -- which `front_door_errors` reads,
36
+ * and which each diagnose with findings then fed, one line per run.
37
+ */
38
+ verdict?: boolean;
32
39
  }
33
40
 
34
41
  /** The log path for a resolved database path (sibling of the DB). */
@@ -40,7 +47,7 @@ export function frontDoorLogPath(dbPath: string): string {
40
47
  export function logFrontDoor(logPath: string | null, entry: FrontDoorLogEntry): void {
41
48
  if (!logPath) return;
42
49
  try {
43
- const level = entry.exitCode === 0 ? "INFO" : "ERROR";
50
+ const level = entry.exitCode === 0 || entry.verdict ? "INFO" : "ERROR";
44
51
  const detail = entry.detail ? entry.detail.replace(/[\r\n\t]+/g, " ") : "";
45
52
  const line = [
46
53
  new Date().toISOString(),
@@ -254,6 +254,17 @@ function logWelcomeCheck(home: string, env: NodeJS.ProcessEnv, check: RemoteChec
254
254
  }
255
255
 
256
256
  /** Port of the read half of `cmd_runtime`. */
257
+ /**
258
+ * `runtime diagnose` exits 1 to say "attention needed": its contract with
259
+ * scripts, not a failure. The front door logs that exit as a verdict (PR #50,
260
+ * CR139), at INFO, so `front_door_errors` does not count the diagnose that
261
+ * reported it -- every diagnose with findings used to feed the finding, one
262
+ * line per run. The rule lives here, beside the command whose contract it is.
263
+ */
264
+ export function isRuntimeVerdict(argv: readonly string[], exitCode: number): boolean {
265
+ return argv[0] === "runtime" && argv[1] === "diagnose" && exitCode === 1;
266
+ }
267
+
257
268
  export async function runRuntimeReadRoute(
258
269
  argv: readonly string[],
259
270
  io: InstallRouteIo = {},
@@ -0,0 +1,55 @@
1
+ // Human text that reaches the front door as bytes: stdin behind `identity set`
2
+ // and `journey update -`, and the file behind `--file` (CR194).
3
+ //
4
+ // The bytes carry no encoding but what their first bytes say. PowerShell 5.1
5
+ // writes a UTF-8 mark at the head of what it pipes (PR #49) and UTF-16LE from
6
+ // `>`; an editor writes UTF-8 with or without a mark. The mark is an encoding's
7
+ // signature, not content: it chooses the decoding and is dropped. UTF-16
8
+ // without a mark is not detectable and is not attempted: a guess about bytes
9
+ // would be wrong about some file, silently.
10
+ //
11
+ // What the ASCII pipe does to a text (`$OutputEncoding` in PowerShell 5.1
12
+ // turns every accented letter into `?`) is not detectable either: a `?` is a
13
+ // character. `--file` is the route that never meets the pipe.
14
+
15
+ import { readFileSync } from "node:fs";
16
+
17
+ /** The text the bytes carry, by their mark; the mark itself is never returned. */
18
+ export function decodeTextInput(bytes: Buffer): string {
19
+ if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
20
+ return bytes.subarray(2).toString("utf16le");
21
+ }
22
+ if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
23
+ const payload = Buffer.from(bytes.subarray(2));
24
+ // An odd trailing byte is not a code unit; `swap16` refuses it.
25
+ return (payload.length % 2 === 0 ? payload : payload.subarray(0, payload.length - 1))
26
+ .swap16()
27
+ .toString("utf16le");
28
+ }
29
+ if (bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
30
+ return bytes.subarray(3).toString("utf8");
31
+ }
32
+ return bytes.toString("utf8");
33
+ }
34
+
35
+ /** Stdin to its end, decoded; "" when there is none. */
36
+ export function readStdinText(): string {
37
+ try {
38
+ return decodeTextInput(readFileSync(0));
39
+ } catch {
40
+ return "";
41
+ }
42
+ }
43
+
44
+ /**
45
+ * The file behind `--file`, decoded; or the one-line refusal. The line names
46
+ * the path and nothing from the error: a directory, a missing file, and a
47
+ * permission refused all read the same to the person, and `errno` is noise.
48
+ */
49
+ export function readTextFile(path: string): { text: string } | { error: string } {
50
+ try {
51
+ return { text: decodeTextInput(readFileSync(path)) };
52
+ } catch {
53
+ return { error: `cannot read ${path}` };
54
+ }
55
+ }
@@ -111,6 +111,11 @@ export const MCP_LOCATOR = [
111
111
  '[ -n "$mmbin" ] || mmbin="$(command -v mirror 2>/dev/null)"',
112
112
  `for mmdir in ${GLOBAL_BIN_DIRS.join(" ")}; do [ -n "$mmbin" ] && break; [ -x "$mmdir/mirror" ] && mmbin="$mmdir/mirror"; done`,
113
113
  '[ -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; }',
114
+ // On Windows npm's global `mirror` is a POSIX shell shim with node_modules/
115
+ // beside it, and Node cannot run the shim; the package's own bin is the
116
+ // entry when it is there (a POSIX prefix keeps node_modules under lib/).
117
+ 'mmjs="$(dirname "$mmbin")/node_modules/mirror-mind/bin/mirror.js"',
118
+ '[ -f "$mmjs" ] && mmbin="$mmjs"',
114
119
  'mmnode="$MIRROR_NODE"',
115
120
  '[ -n "$mmnode" ] && [ -x "$mmnode" ] || mmnode="$(command -v node 2>/dev/null)"',
116
121
  `for mmdir in "$(dirname "$mmbin")" ${GLOBAL_BIN_DIRS.join(" ")}; do [ -n "$mmnode" ] && break; [ -x "$mmdir/node" ] && mmnode="$mmdir/node"; done`,
@@ -25,7 +25,7 @@
25
25
  // inconclusive without a key; the live fetch belongs to DS8.
26
26
 
27
27
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
28
- import { basename, dirname, join } from "node:path";
28
+ import { basename, dirname, join, posix, win32 } from "node:path";
29
29
  import { openDatabaseReadOnly } from "#db/database.ts";
30
30
  import {
31
31
  type ExtensionPluginReport,
@@ -913,16 +913,24 @@ export function hookFailureFindings(
913
913
  export function hookNodeFindings(
914
914
  env: NodeJS.ProcessEnv,
915
915
  isExecutable: (path: string) => boolean,
916
+ platform: NodeJS.Platform = process.platform,
916
917
  ): DriftFinding[] {
917
918
  const explicit = env.MIRROR_NODE ?? "";
918
919
  if (explicit && isExecutable(explicit)) return [];
919
920
  const candidates = HOOK_NODE_CANDIDATES.map((candidate) =>
920
921
  candidate.replace("$HOME", env.HOME ?? ""),
921
922
  );
923
+ // The platform's PATH separator and its executable name: `;` and `node.exe`
924
+ // on Windows, where splitting on `:` cut `C:\Program Files\nodejs` into `C`
925
+ // and a path and the finding fired on every install that had Node (PR #49).
926
+ // `platform` is a parameter so the test runs both branches on every host:
927
+ // CI's Windows job runs two files and this one is not among them.
928
+ const paths = platform === "win32" ? win32 : posix;
929
+ const name = platform === "win32" ? "node.exe" : "node";
922
930
  const onPath = (env.PATH ?? "")
923
- .split(":")
931
+ .split(paths.delimiter)
924
932
  .filter(Boolean)
925
- .some((entry) => isExecutable(`${entry}/node`));
933
+ .some((entry) => isExecutable(paths.join(entry, name)));
926
934
  if (onPath || candidates.some((candidate) => isExecutable(candidate))) return [];
927
935
  return [
928
936
  {