mirror-mind 0.39.3 → 0.39.5

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.
@@ -31,8 +31,13 @@ env_value() {
31
31
  [ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
32
32
  printf '%s' "$value"
33
33
  }
34
+ # `note <line> daily` writes at most once a day (UTC): for a setting to fix
35
+ # rather than a hook that failed, so the line stays inside diagnose's seven-day
36
+ # window while the setting stays wrong, without one per hook per turn (CR192).
37
+ # The log is read into a variable, not piped to `grep -q`: under pipefail, a
38
+ # reader that stops early can fail the pipeline and the line be written twice.
34
39
  note() {
35
- local home user
40
+ local home user seen today
36
41
  home="$(env_value MIRROR_HOME)"
37
42
  user="$(env_value MIRROR_USER)"
38
43
  if [ -n "$home" ]; then
@@ -43,6 +48,11 @@ note() {
43
48
  else
44
49
  return 0
45
50
  fi
51
+ if [ "${2:-}" = daily ]; then
52
+ today="$(date -u +%Y-%m-%d)"
53
+ seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
54
+ case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
55
+ fi
46
56
  mkdir -p "$home" 2>/dev/null \
47
57
  && printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
48
58
  }
@@ -58,10 +68,32 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
58
68
  # directory the entry was found in is searched first: an npm global bin holds
59
69
  # the `node` that installed the package beside `mirror-hook`, while a system
60
70
  # `/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)"
71
+ #
72
+ # MIRROR_NODE is used only as an absolute path to an executable file, as every
73
+ # reader uses it (CR192): a relative one would resolve against the project the
74
+ # runtime spawned this hook from. Anything else is noted with its reason, in
75
+ # the Pi extension's words, once a day, and the search goes on without it.
76
+ # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
77
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes
78
+ # C:\Program Files\nodejs\node.exe (PR #47).
79
+ NODE=""
80
+ if [ -n "${MIRROR_NODE:-}" ]; then
81
+ refused=""
82
+ case "$MIRROR_NODE" in
83
+ /*|\\*|[A-Za-z]:[\\/]*)
84
+ if [ -d "$MIRROR_NODE" ]; then refused="a directory"
85
+ elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
86
+ elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
87
+ else NODE="$MIRROR_NODE"
88
+ fi
89
+ ;;
90
+ *) refused="relative" ;;
91
+ esac
92
+ if [ -n "$refused" ]; then
93
+ note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
94
+ fi
64
95
  fi
96
+ [ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
65
97
  for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
66
98
  [ -n "$NODE" ] && break
67
99
  [ -x "$candidate" ] && NODE="$candidate"
@@ -31,8 +31,13 @@ env_value() {
31
31
  [ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
32
32
  printf '%s' "$value"
33
33
  }
34
+ # `note <line> daily` writes at most once a day (UTC): for a setting to fix
35
+ # rather than a hook that failed, so the line stays inside diagnose's seven-day
36
+ # window while the setting stays wrong, without one per hook per turn (CR192).
37
+ # The log is read into a variable, not piped to `grep -q`: under pipefail, a
38
+ # reader that stops early can fail the pipeline and the line be written twice.
34
39
  note() {
35
- local home user
40
+ local home user seen today
36
41
  home="$(env_value MIRROR_HOME)"
37
42
  user="$(env_value MIRROR_USER)"
38
43
  if [ -n "$home" ]; then
@@ -43,6 +48,11 @@ note() {
43
48
  else
44
49
  return 0
45
50
  fi
51
+ if [ "${2:-}" = daily ]; then
52
+ today="$(date -u +%Y-%m-%d)"
53
+ seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
54
+ case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
55
+ fi
46
56
  mkdir -p "$home" 2>/dev/null \
47
57
  && printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
48
58
  }
@@ -58,10 +68,32 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
58
68
  # directory the entry was found in is searched first: an npm global bin holds
59
69
  # the `node` that installed the package beside `mirror-hook`, while a system
60
70
  # `/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)"
71
+ #
72
+ # MIRROR_NODE is used only as an absolute path to an executable file, as every
73
+ # reader uses it (CR192): a relative one would resolve against the project the
74
+ # runtime spawned this hook from. Anything else is noted with its reason, in
75
+ # the Pi extension's words, once a day, and the search goes on without it.
76
+ # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
77
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes
78
+ # C:\Program Files\nodejs\node.exe (PR #47).
79
+ NODE=""
80
+ if [ -n "${MIRROR_NODE:-}" ]; then
81
+ refused=""
82
+ case "$MIRROR_NODE" in
83
+ /*|\\*|[A-Za-z]:[\\/]*)
84
+ if [ -d "$MIRROR_NODE" ]; then refused="a directory"
85
+ elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
86
+ elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
87
+ else NODE="$MIRROR_NODE"
88
+ fi
89
+ ;;
90
+ *) refused="relative" ;;
91
+ esac
92
+ if [ -n "$refused" ]; then
93
+ note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
94
+ fi
64
95
  fi
96
+ [ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
65
97
  for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
66
98
  [ -n "$NODE" ] && break
67
99
  [ -x "$candidate" ] && NODE="$candidate"
@@ -31,8 +31,13 @@ env_value() {
31
31
  [ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
32
32
  printf '%s' "$value"
33
33
  }
34
+ # `note <line> daily` writes at most once a day (UTC): for a setting to fix
35
+ # rather than a hook that failed, so the line stays inside diagnose's seven-day
36
+ # window while the setting stays wrong, without one per hook per turn (CR192).
37
+ # The log is read into a variable, not piped to `grep -q`: under pipefail, a
38
+ # reader that stops early can fail the pipeline and the line be written twice.
34
39
  note() {
35
- local home user
40
+ local home user seen today
36
41
  home="$(env_value MIRROR_HOME)"
37
42
  user="$(env_value MIRROR_USER)"
38
43
  if [ -n "$home" ]; then
@@ -43,6 +48,11 @@ note() {
43
48
  else
44
49
  return 0
45
50
  fi
51
+ if [ "${2:-}" = daily ]; then
52
+ today="$(date -u +%Y-%m-%d)"
53
+ seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
54
+ case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
55
+ fi
46
56
  mkdir -p "$home" 2>/dev/null \
47
57
  && printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
48
58
  }
@@ -58,10 +68,32 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
58
68
  # directory the entry was found in is searched first: an npm global bin holds
59
69
  # the `node` that installed the package beside `mirror-hook`, while a system
60
70
  # `/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)"
71
+ #
72
+ # MIRROR_NODE is used only as an absolute path to an executable file, as every
73
+ # reader uses it (CR192): a relative one would resolve against the project the
74
+ # runtime spawned this hook from. Anything else is noted with its reason, in
75
+ # the Pi extension's words, once a day, and the search goes on without it.
76
+ # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
77
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes
78
+ # C:\Program Files\nodejs\node.exe (PR #47).
79
+ NODE=""
80
+ if [ -n "${MIRROR_NODE:-}" ]; then
81
+ refused=""
82
+ case "$MIRROR_NODE" in
83
+ /*|\\*|[A-Za-z]:[\\/]*)
84
+ if [ -d "$MIRROR_NODE" ]; then refused="a directory"
85
+ elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
86
+ elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
87
+ else NODE="$MIRROR_NODE"
88
+ fi
89
+ ;;
90
+ *) refused="relative" ;;
91
+ esac
92
+ if [ -n "$refused" ]; then
93
+ note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
94
+ fi
64
95
  fi
96
+ [ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
65
97
  for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
66
98
  [ -n "$NODE" ] && break
67
99
  [ -x "$candidate" ] && NODE="$candidate"
@@ -31,8 +31,13 @@ env_value() {
31
31
  [ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
32
32
  printf '%s' "$value"
33
33
  }
34
+ # `note <line> daily` writes at most once a day (UTC): for a setting to fix
35
+ # rather than a hook that failed, so the line stays inside diagnose's seven-day
36
+ # window while the setting stays wrong, without one per hook per turn (CR192).
37
+ # The log is read into a variable, not piped to `grep -q`: under pipefail, a
38
+ # reader that stops early can fail the pipeline and the line be written twice.
34
39
  note() {
35
- local home user
40
+ local home user seen today
36
41
  home="$(env_value MIRROR_HOME)"
37
42
  user="$(env_value MIRROR_USER)"
38
43
  if [ -n "$home" ]; then
@@ -43,6 +48,11 @@ note() {
43
48
  else
44
49
  return 0
45
50
  fi
51
+ if [ "${2:-}" = daily ]; then
52
+ today="$(date -u +%Y-%m-%d)"
53
+ seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
54
+ case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
55
+ fi
46
56
  mkdir -p "$home" 2>/dev/null \
47
57
  && printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
48
58
  }
@@ -58,10 +68,32 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
58
68
  # directory the entry was found in is searched first: an npm global bin holds
59
69
  # the `node` that installed the package beside `mirror-hook`, while a system
60
70
  # `/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)"
71
+ #
72
+ # MIRROR_NODE is used only as an absolute path to an executable file, as every
73
+ # reader uses it (CR192): a relative one would resolve against the project the
74
+ # runtime spawned this hook from. Anything else is noted with its reason, in
75
+ # the Pi extension's words, once a day, and the search goes on without it.
76
+ # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
77
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes
78
+ # C:\Program Files\nodejs\node.exe (PR #47).
79
+ NODE=""
80
+ if [ -n "${MIRROR_NODE:-}" ]; then
81
+ refused=""
82
+ case "$MIRROR_NODE" in
83
+ /*|\\*|[A-Za-z]:[\\/]*)
84
+ if [ -d "$MIRROR_NODE" ]; then refused="a directory"
85
+ elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
86
+ elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
87
+ else NODE="$MIRROR_NODE"
88
+ fi
89
+ ;;
90
+ *) refused="relative" ;;
91
+ esac
92
+ if [ -n "$refused" ]; then
93
+ note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
94
+ fi
64
95
  fi
96
+ [ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
65
97
  for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
66
98
  [ -n "$NODE" ] && break
67
99
  [ -x "$candidate" ] && NODE="$candidate"
@@ -31,8 +31,13 @@ env_value() {
31
31
  [ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
32
32
  printf '%s' "$value"
33
33
  }
34
+ # `note <line> daily` writes at most once a day (UTC): for a setting to fix
35
+ # rather than a hook that failed, so the line stays inside diagnose's seven-day
36
+ # window while the setting stays wrong, without one per hook per turn (CR192).
37
+ # The log is read into a variable, not piped to `grep -q`: under pipefail, a
38
+ # reader that stops early can fail the pipeline and the line be written twice.
34
39
  note() {
35
- local home user
40
+ local home user seen today
36
41
  home="$(env_value MIRROR_HOME)"
37
42
  user="$(env_value MIRROR_USER)"
38
43
  if [ -n "$home" ]; then
@@ -43,6 +48,11 @@ note() {
43
48
  else
44
49
  return 0
45
50
  fi
51
+ if [ "${2:-}" = daily ]; then
52
+ today="$(date -u +%Y-%m-%d)"
53
+ seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
54
+ case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
55
+ fi
46
56
  mkdir -p "$home" 2>/dev/null \
47
57
  && printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
48
58
  }
@@ -58,10 +68,32 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
58
68
  # directory the entry was found in is searched first: an npm global bin holds
59
69
  # the `node` that installed the package beside `mirror-hook`, while a system
60
70
  # `/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)"
71
+ #
72
+ # MIRROR_NODE is used only as an absolute path to an executable file, as every
73
+ # reader uses it (CR192): a relative one would resolve against the project the
74
+ # runtime spawned this hook from. Anything else is noted with its reason, in
75
+ # the Pi extension's words, once a day, and the search goes on without it.
76
+ # "Absolute" is the MCP locator's pattern too, and it classifies exactly as
77
+ # path.win32.isAbsolute: under Git Bash the Windows installer writes
78
+ # C:\Program Files\nodejs\node.exe (PR #47).
79
+ NODE=""
80
+ if [ -n "${MIRROR_NODE:-}" ]; then
81
+ refused=""
82
+ case "$MIRROR_NODE" in
83
+ /*|\\*|[A-Za-z]:[\\/]*)
84
+ if [ -d "$MIRROR_NODE" ]; then refused="a directory"
85
+ elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
86
+ elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
87
+ else NODE="$MIRROR_NODE"
88
+ fi
89
+ ;;
90
+ *) refused="relative" ;;
91
+ esac
92
+ if [ -n "$refused" ]; then
93
+ note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
94
+ fi
64
95
  fi
96
+ [ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
65
97
  for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
66
98
  [ -n "$NODE" ] && break
67
99
  [ -x "$candidate" ] && NODE="$candidate"
@@ -235,7 +235,7 @@ export interface NodeResolution {
235
235
  * then `NODE_CANDIDATES`. Not `process.execPath`: Pi may run on a Node older
236
236
  * than the core needs. On Windows the PATH is the last place looked.
237
237
  *
238
- * A relative `MIRROR_NODE` is refused here, though the wrappers accept one: it
238
+ * A relative `MIRROR_NODE` is refused, as every reader refuses one (CR192): it
239
239
  * would resolve against whatever project Pi was opened in.
240
240
  *
241
241
  * The absolute candidates make "nothing anywhere" a fact about the host, so
@@ -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.5 — The Node You Named, or Why Not](v0.39.5.md) — patch release of CR192, from PR #47's thread (@rodrigoimmaginario): the hook wrappers, the plugin's MCP entry, and `runtime diagnose` use `MIRROR_NODE` only as an absolute path to an executable file, as the Pi extension does; a relative value is never run, and a refused one is named with its reason, once a day in `hooks.log` and on the MCP entry's stderr; the Windows installer's drive-letter path counts as absolute, in CI under Git Bash and on the installer. No migration.
16
+ - [v0.39.4 — The Reply Finds Its Prompt](v0.39.4.md) — patch release of PR #51 (@alannascimento1), CR166 and CR196: on Claude Code, a prompt typed while a turn runs and a journey entered in plain words no longer lose the reply; `nothing_waiting` in `hooks.log` means a lost reply again. No schema change, no migration.
15
17
  - [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
18
  - [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.
17
19
  - [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.
@@ -89,11 +89,18 @@ Validated on 2026-11-03 against a scratch copy of the maintainer's database: UTF
89
89
  with a mark, and UTF-16LE as PowerShell 5.1 writes it, through `--file` and through stdin;
90
90
  the refusals writing nothing; the one-liner pipe unchanged.
91
91
 
92
+ **Post-release Windows validation, 2026-11-03.** Rodrigo Immaginario ran v0.39.2 through
93
+ npm's `mirror.ps1` shim on Windows PowerShell 5.1.26100. `--file` preserved both unmarked
94
+ UTF-8 and UTF-16LE. The measurement also corrected the pipe documentation: for an
95
+ editor-saved unmarked UTF-8 file, UTF-8 `$OutputEncoding` is not enough because
96
+ `Get-Content` has already decoded the file through the ANSI code page. The complete pipe
97
+ route is `Get-Content -Encoding UTF8` plus UTF-8 `$OutputEncoding`. Evidence:
98
+ [PR #49](https://github.com/mirror-mind-ai/mirror/pull/49#issuecomment-6610137363).
99
+
92
100
  ## Conscious Exclusions
93
101
 
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.
102
+ - At publication, Windows PowerShell 5.1 itself had not been run on the maintainer's side;
103
+ the post-release validation above closed that measurement gap.
97
104
  - Detecting text the ASCII pipe damaged: not possible without guessing.
98
105
  - The hook wrappers and the MCP locator still accept a relative `MIRROR_NODE`
99
106
  ([CR192](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr192-the-hook-wrappers-and-the-mcp-locator-accept-a-relative-mirror-node.md),
@@ -0,0 +1,81 @@
1
+ ---
2
+ digest: >
3
+ v0.39.4 is a patch release of one pull request by Alan Nascimento, his first here. On
4
+ Claude Code, two ordinary situations made the Stop hook drop a real reply: a prompt typed
5
+ while a turn was still running, whose prompt hook fires at queue time under the running
6
+ turn's id, and a journey entered in plain words, whose Builder load switches the
7
+ conversation mid-turn. Both replies are now logged beside their prompts, and
8
+ `nothing_waiting` in `hooks.log` means what it says again. No schema change, no
9
+ migration, and no model or prompt change.
10
+ ---
11
+
12
+ # v0.39.4 — The Reply Finds Its Prompt
13
+
14
+ **Date:** 2026-11-03
15
+
16
+ ## Highlights
17
+
18
+ - **A prompt typed while a turn runs keeps its reply**
19
+ ([PR #51](https://github.com/mirror-mind-ai/mirror/pull/51), by @alannascimento1;
20
+ [CR196](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr196-a-prompt-typed-while-a-turn-runs-loses-its-reply-on-claude-code.md)).
21
+ Claude Code fires the prompt hook when such a prompt enters its queue, with the running
22
+ turn's id; the prompt later runs under an id no hook sent, and its Stop found nothing
23
+ waiting. The prompt is now recorded as queued during the running turn, the running turn's
24
+ reply stays with its own question, and the Stop of the turn the queued prompt ran as
25
+ claims it. Never for a command turn, and never a prompt an ordinary one has since
26
+ followed.
27
+ - **Entering a journey in plain words keeps the turn together**
28
+ ([CR166](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr166-a-plain-language-journey-switch-strands-its-turn.md)).
29
+ "I want to enter journey X" logs the prompt in one conversation; the model's `build load`
30
+ closes it and binds the journey's; the Stop paired in the new, empty one. A hook prompt
31
+ still owed its reply now moves with the switch. A prompt logged by hand stays where it
32
+ was, so `mm-new` and the logger CLI behave as before. Pi gets this half too: its prompts
33
+ carry turn ids.
34
+ - **`nothing_waiting` means a lost reply again.** On one host, 20 of 22 such lines in a
35
+ week were these two cases; `runtime diagnose`'s `hook_failures_recorded` was mostly
36
+ counting them.
37
+ - No schema change (`queued_in_turn` is one more optional key in the `metadata` JSON
38
+ column `turn_id` already lives in), no migration, and no model or prompt change.
39
+
40
+ ## Where We Started
41
+
42
+ Alan Nascimento opened PR #51 on 2026-10-30 with the session transcript that shows the
43
+ mechanism, Claude Code's own `queue-operation` record between the prompt hook and the
44
+ Stop, and a week of `hooks.log` counted by cause. The journey-switch half had been in the
45
+ backlog as CR166 since 2026-10-24, unassigned; the queued-prompt half no record described.
46
+ The review, through four lenses, had nothing to amend: the reply rule gains one branch
47
+ between "by turn id" and "by position", each new path bounded by an explicit guard, and
48
+ ten tests of which five fail without the change.
49
+
50
+ ## What Changed
51
+
52
+ All in `ts/src/conversation/logger.ts`. `promptMetadata` records a second prompt under a
53
+ turn still owed its reply as `queued_in_turn`. `handleStopHook`, for a turn id with no
54
+ prompt of its own, claims the oldest unclaimed queued prompt that no ordinary prompt has
55
+ followed, and logs the reply as paired by `queued_prompt`. `switchConversation` moves the
56
+ old conversation's last row to the new one when it is a hook prompt with no reply after
57
+ it. The reply rule's docblock names the new branch.
58
+
59
+ ## Conscious Exclusions
60
+
61
+ Both named by the author and recorded in CR166 and CR196:
62
+
63
+ - In the queued case the running turn's reply is still written after the queued prompt,
64
+ because the prompt hook fires at queue time: the rows pair by `turn_id`, but a reader
65
+ ordering by `created_at` sees `Q1, Q2, A1, A2`. Rewriting `created_at` would make it lie
66
+ about when the prompt was typed.
67
+ - The waiting prompt moves after the old conversation's close-time extraction, which
68
+ therefore still saw it. Moving it earlier would mean starting the new conversation
69
+ before closing the old.
70
+ - The two `nothing_waiting` lines in the week that match neither pattern.
71
+
72
+ ## Upgrading
73
+
74
+ ```bash
75
+ mirror runtime update
76
+ ```
77
+
78
+ ## Next Horizon
79
+
80
+ Moacir Moda's PR #27; CR190, CR192, and CR195 in RS010's backlog. CV24 continues on its
81
+ branch.
@@ -0,0 +1,146 @@
1
+ ---
2
+ digest: >
3
+ v0.39.5 is a patch release of one Change Request, CR192, from PR #47's thread with
4
+ Rodrigo Immaginario. The hook wrappers, the plugin's MCP entry, and `runtime diagnose`
5
+ now use `MIRROR_NODE` only when it is an absolute path to an executable file, the rule
6
+ the Pi extension has followed since v0.39.1. A relative value could make a hook run a
7
+ file from the project the runtime was opened in, and a value that no longer pointed at
8
+ Node was dropped without a word. Now neither is used, and the hooks and the MCP entry
9
+ name the refused value with its reason. The Windows installer's drive-letter path counts
10
+ as absolute, proven in CI under Git Bash and on the installer itself. No schema change,
11
+ no migration, and no model or prompt change.
12
+ ---
13
+
14
+ # v0.39.5 — The Node You Named, or Why Not
15
+
16
+ **Date:** 2026-11-05
17
+
18
+ ## Highlights
19
+
20
+ - **A relative `MIRROR_NODE` is never run**
21
+ ([CR192](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr192-the-hook-wrappers-and-the-mcp-locator-accept-a-relative-mirror-node.md)).
22
+ A runtime starts its hooks and its MCP server from the project it was opened in, and the
23
+ hook wrappers and the MCP entry kept any `MIRROR_NODE` that passed `[ -x ]` there: with
24
+ `MIRROR_NODE=bin/node`, the project's own `bin/node` ran on every hook. Every reader now
25
+ follows the Pi extension's rule: an absolute path to an executable file, or nothing.
26
+ - **A value that is not used is named, with its reason.** A value that named no usable
27
+ Node used to be dropped without a word. Now the hook wrappers write
28
+ `MIRROR_NODE=bin/node is relative; searching the PATH and the usual places instead` to
29
+ `<mirror home>/hooks.log`, once a UTC day for the same value, and the MCP entry writes
30
+ the same message to its stderr. The reasons are the Pi extension's: `relative`,
31
+ `a directory`, `missing`, and `not executable`. The search then goes on through the
32
+ PATH and the usual places, as before.
33
+ - **The Windows installer's form counts as absolute.** Under Git Bash the installer's
34
+ `MIRROR_NODE` is a drive-letter path with backslashes, and a POSIX-only test would have
35
+ refused it on every Windows hook; @rodrigoimmaginario named that trap before the plan
36
+ was written. One shell pattern now classifies exactly as Node's `path.win32.isAbsolute`:
37
+ `C:\…`, `C:/…`, and `\\server\share\…` are absolute, and a drive-relative `C:node.exe`
38
+ is refused as `relative`.
39
+ - **Proven on Windows twice.** The Windows CI job runs a real wrapper under Git Bash, with
40
+ a Node under `Program Files\nodejs` named in its Windows form. And before this tag,
41
+ Rodrigo ran the installer against `main`: its `MIRROR_NODE`, under a user folder with a
42
+ space and accents, ran the hooks and the MCP server with no refusal line, and
43
+ `C:node.exe` gave exactly one.
44
+ - No schema change, no migration, and no model or prompt change.
45
+
46
+ ## Where We Started
47
+
48
+ v0.39.1 made the Pi extension strict about `MIRROR_NODE`
49
+ ([CR191](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr191-the-pi-extension-cannot-find-node-where-the-hook-wrappers-can.md)):
50
+ an absolute path to an executable file, or a warning with the reason and a search without
51
+ it. The rest of the family was not strict. The hook wrappers and the plugin's MCP locator
52
+ kept any value that passed `[ -x ]`, a test a relative path passes against the project a
53
+ hook was started in; a value that failed it was dropped, and whoever had set it to pin a
54
+ Node learned nothing. `REFERENCE.md` described one trust class for every reader, and only
55
+ one reader held to it. CR191's debt review left the family's rule as its own decision,
56
+ captured as CR192 on 2026-11-02.
57
+
58
+ On PR #47 the next day, Rodrigo Immaginario named the trap in the obvious fix: under Git
59
+ Bash the Windows installer's `MIRROR_NODE` is a drive-letter path, it passes `[ -x ]`
60
+ today, and a `/*` test for "absolute" would refuse it, so every hook on a Windows install
61
+ would log a refusal. He also asked that PR #53, which changed the same generator, locator,
62
+ and tests, merge first. It did, in v0.39.3, and the plan started from its blocks.
63
+
64
+ ## What Changed
65
+
66
+ **One rule, one pattern.** For the shell readers, "absolute" is one `case` pattern,
67
+ `/*|\\*|[A-Za-z]:[\\/]*`, which matched `path.win32.isAbsolute` on 15 values in bash,
68
+ dash, ksh, `/bin/sh`, and zsh as `sh`. `scripts/generate_hook_wrappers.sh` gives it, with
69
+ the logging helpers and the Node search, to all three wrapper forms as shared fragments,
70
+ and the 13 wrappers are regenerated. The MCP locator in `ts/src/guards/claudePlugin.ts`
71
+ carries the same text as `ABSOLUTE_PATH_CASE`, and a test fails if the two drift. The Node
72
+ readers keep their platform's `path.isAbsolute`, so on Windows every reader draws the same
73
+ line.
74
+
75
+ **A refusal that cannot forge a line.** Line breaks are removed from a value before it is
76
+ logged: in review, a value with a line break in it wrote a second `hooks.log` line, as
77
+ `claude:stop`, that `runtime diagnose` counted. The locator prints with `printf`, because
78
+ dash's `echo` turns the `\n` of a Windows path into a newline, and names `$MIRROR_NODE`
79
+ once, because Grok pastes a set variable's value into the MCP arguments it loads.
80
+
81
+ **`runtime diagnose`.** Its Node check counts `MIRROR_NODE` only under the rule, through
82
+ `isExecutableFile()`, which the front door's runtime route now shares. It adds no finding
83
+ of its own: the hooks' refusal line reaches it as `hook_failures_recorded`, for seven
84
+ days.
85
+
86
+ **Tests and CI.** `mirrorNodeRule.test.ts` holds the pattern equal across the three
87
+ wrapper forms and the locator, runs the classification table through bash and, where
88
+ installed, dash, and runs a real wrapper with a Node under `Program Files/nodejs` and
89
+ with `C:node.exe`; the Windows job runs it under Git Bash. `hooks.test.ts` and
90
+ `mcpEntry.test.ts` cover each reason, a planted `bin/node` that never runs, the daily
91
+ rule across two fixed days, a forged line break, and a server that starts past a
92
+ relative value.
93
+
94
+ **Docs.** The rule is stated once, in full, in the
95
+ [configuration reference](../reference/configuration.md#hook-node-resolution);
96
+ `REFERENCE.md`'s row points there. Troubleshooting shows the refusal line and the fix, and
97
+ says `runtime diagnose` keeps counting old lines for seven days after the fix.
98
+
99
+ Validated by the Navigator in a scratch home: two runs of the plugin's prompt hook with
100
+ `MIRROR_NODE=bin/node` wrote one line,
101
+
102
+ ```text
103
+ 2026-11-04T21:40:16Z claude:user-prompt: MIRROR_NODE=bin/node is relative; searching the PATH and the usual places instead
104
+ ```
105
+
106
+ the project's `bin/node` never ran, the prompt was logged, and `runtime diagnose`
107
+ reported `hook_failures_recorded`. CI passed on every job, the Git Bash step included,
108
+ and Rodrigo's installer run, 21 of 21 on the installer's contract, was the gate before
109
+ the tag.
110
+
111
+ **Also in this release.** Troubleshooting's PowerShell 5.1 section, from Rodrigo's PR #49
112
+ run: `Get-Content` decodes an editor-saved UTF-8 file without a mark through the ANSI code
113
+ page before the pipe runs, so a UTF-8 `$OutputEncoding` alone does not save it. The pipe
114
+ route now adds `Get-Content -Encoding UTF8`, and `--file` stays the safe one.
115
+
116
+ ## Conscious Exclusions
117
+
118
+ - `MIRROR_BIN` keeps its behavior: a relative value is still honored, and one that names
119
+ nothing is still dropped without a word.
120
+ [CR197](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr197-the-hook-wrappers-and-the-mcp-locator-accept-a-relative-mirror-bin.md)
121
+ (captured) would give it this rule, keeping accepted the forward-slash `C:/…` form a
122
+ Windows machine sets for the Claude Code forwarders.
123
+ - `runtime diagnose` does not name a refused value. It counts the hooks' line under
124
+ `hook_failures_recorded`, and the value and its reason are in `hooks.log`; Rodrigo
125
+ suggests saying it in diagnose too. A finding of its own was left out at planning.
126
+ - Whitespace: the Pi extension trims the value and the shell readers do not, so
127
+ ` /opt/node` is accepted by Pi and refused as `relative` by the hooks. Nothing writes
128
+ such a value.
129
+ - Converting MSYS `/c/…` values, and rotating `hooks.log`.
130
+ - Where Codex and Antigravity show an MCP server's stderr is not verified; the docs say
131
+ only what each CLI's own help shows.
132
+
133
+ ## Upgrading
134
+
135
+ ```bash
136
+ mirror runtime update
137
+ ```
138
+
139
+ If you set `MIRROR_NODE` by hand, make it the absolute path of the `node` binary: a
140
+ relative one is now refused, with one line a day in `hooks.log`.
141
+
142
+ ## Next Horizon
143
+
144
+ CR197, which would bring `MIRROR_BIN` under this rule with its pattern; Rodrigo's
145
+ suggestion that `runtime diagnose` name a refused value, not yet decided; and the open
146
+ RS010 backlog.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mirror-mind",
3
- "version": "0.39.3",
3
+ "version": "0.39.5",
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.3",
3
+ "version": "0.39.5",
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; }; 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"
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\"; mmwhy=\"\"; [ -n \"$mmnode\" ] && case \"$mmnode\" in /*|\\\\*|[A-Za-z]:[\\\\/]*) if [ -d \"$mmnode\" ]; then mmwhy=\"a directory\"; elif [ ! -e \"$mmnode\" ]; then mmwhy=\"missing\"; elif [ ! -f \"$mmnode\" ] || [ ! -x \"$mmnode\" ]; then mmwhy=\"not executable\"; fi ;; *) mmwhy=\"relative\" ;; esac; [ -n \"$mmwhy\" ] && { printf 'mirror-mind: MIRROR_NODE=%s is %s; searching the PATH and the usual places instead\\n' \"$(printf '%s' \"$mmnode\" | tr -d '\\r\\n')\" \"$mmwhy\" >&2; mmnode=\"\"; }; [ -n \"$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
  }