mirror-mind 0.34.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/skills/mm-build/SKILL.md +1 -1
- package/.pi/skills/mm-explore/SKILL.md +1 -1
- package/.pi/skills/mm-help/SKILL.md +2 -2
- package/.pi/skills/mm-soul/SKILL.md +1 -1
- package/AGENTS.md +7 -6
- package/README.md +5 -6
- package/docs/releases/index.md +2 -0
- package/docs/releases/v0.35.0.md +199 -0
- package/docs/releases/v0.36.0.md +222 -0
- package/package.json +2 -4
- package/plugins/mirror-mind/.claude-plugin/plugin.json +1 -1
- package/plugins/mirror-mind/agy/pre-invocation.sh +92 -0
- package/plugins/mirror-mind/agy/session-start.sh +92 -0
- package/plugins/mirror-mind/agy/stop.sh +92 -0
- package/plugins/mirror-mind/hooks.json +26 -0
- package/plugins/mirror-mind/mcp_config.json +11 -0
- package/plugins/mirror-mind/plugin.json +14 -0
- package/plugins/mirror-mind/rules/AGENTS.md +189 -0
- package/plugins/mirror-mind/skills/mm-build/SKILL.md +1 -1
- package/ts/src/conversation/agyTranscript.ts +105 -0
- package/ts/src/conversation/commandTurn.ts +1 -1
- package/ts/src/conversation/deferredCloseTail.ts +2 -2
- package/ts/src/conversation/logger.ts +1 -1
- package/ts/src/conversation/runtimePolicy.ts +36 -4
- package/ts/src/docs/docsLint.ts +14 -1
- package/ts/src/extensions/bindings.ts +53 -2
- package/ts/src/extensions/catalogCommands.ts +82 -23
- package/ts/src/extensions/catalogWrites.ts +12 -0
- package/ts/src/extensions/contextRuntime.ts +36 -1
- package/ts/src/extensions/dispatch.ts +43 -10
- package/ts/src/extensions/extensionPlugin.ts +325 -0
- package/ts/src/extensions/manifest.ts +7 -1
- package/ts/src/extensions/settings.ts +147 -0
- package/ts/src/frontDoor/cli.ts +16 -1
- package/ts/src/frontDoor/extensionCatalogRoute.ts +8 -2
- package/ts/src/frontDoor/runtimeRoute.ts +11 -0
- package/ts/src/frontDoor/stdinSource.ts +2 -2
- package/ts/src/guards/claudePlugin.ts +91 -0
- package/ts/src/guards/packContents.ts +3 -2
- package/ts/src/guards/piSourcedSkills.ts +1 -1
- package/ts/src/guards/retiredSurfaces.ts +31 -0
- package/ts/src/hooks/agy.ts +229 -0
- package/ts/src/hooks/claude.ts +6 -0
- package/ts/src/hooks/main.ts +23 -16
- package/ts/src/hooks/payload.ts +8 -15
- package/ts/src/hooks/runtime.ts +11 -10
- package/ts/src/runtime/brokenPipe.ts +52 -0
- package/ts/src/runtime/diagnose.ts +122 -13
- package/ts/src/runtime/extensionPluginWiring.ts +222 -0
- package/ts/src/runtime/pluginWiring.ts +168 -29
- package/ts/src/runtime/status.ts +28 -1
- package/ts/src/runtime/update.ts +25 -11
- package/ts/src/runtime/wiring.ts +149 -35
- package/.gemini/hooks/log-assistant.sh +0 -85
- package/.gemini/hooks/log-user.sh +0 -85
- package/.gemini/hooks/session-end.sh +0 -85
- package/.gemini/hooks/session-start.sh +0 -85
- package/ts/src/hooks/gemini.ts +0 -128
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Mirror Mind hook wrapper — GENERATED by scripts/generate_hook_wrappers.sh.
|
|
3
|
+
# Edit the generator, never this file: every agy wrapper shares these bytes
|
|
4
|
+
# and differs only in the hook name and its stdin gate (CV21.DS7.TS2).
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
HOOK="agy:pre-invocation"
|
|
7
|
+
GATE='"invocationNum"[[:space:]]*:[[:space:]]*-?[0-9]*[1-9]'
|
|
8
|
+
|
|
9
|
+
# agy sends the payload on stdin and blocks its agent loop on this process.
|
|
10
|
+
# Read the payload once; when the gate says this invocation is not ours,
|
|
11
|
+
# answer the empty object without resolving Node at all.
|
|
12
|
+
PAYLOAD="$(cat 2>/dev/null || true)"
|
|
13
|
+
if [ -n "$GATE" ] && printf '%s' "$PAYLOAD" | grep -qE "$GATE"; then
|
|
14
|
+
printf '{}\n'
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# This wrapper is the Claude plugin's, and a plugin is copied into Claude
|
|
19
|
+
# Code's own cache: no relative path from here reaches the Mirror tree. It
|
|
20
|
+
# runs the installed `mirror-hook` bin instead, and reads no tree .env.
|
|
21
|
+
REPO_ROOT=""
|
|
22
|
+
|
|
23
|
+
# A hook never fails the user's turn, and never fails in silence: when it
|
|
24
|
+
# cannot do its job it appends ONE line to <mirror home>/hooks.log and exits
|
|
25
|
+
# 0 — after answering agy's contract, so the agent loop never stalls.
|
|
26
|
+
env_file_value() {
|
|
27
|
+
[ -f "$2" ] || return 0
|
|
28
|
+
sed -n -E "s/^[[:space:]]*(export[[:space:]]+)?$1[[:space:]]*=[[:space:]]*//p" "$2" \
|
|
29
|
+
| tail -n 1 | tr -d "\"'\r"
|
|
30
|
+
}
|
|
31
|
+
env_value() {
|
|
32
|
+
local value=""
|
|
33
|
+
if [ -n "${!1+set}" ]; then printf '%s' "${!1}"; return; fi
|
|
34
|
+
[ -z "$REPO_ROOT" ] || value="$(env_file_value "$1" "$REPO_ROOT/.env")"
|
|
35
|
+
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
36
|
+
printf '%s' "$value"
|
|
37
|
+
}
|
|
38
|
+
note() {
|
|
39
|
+
local home user
|
|
40
|
+
home="$(env_value MIRROR_HOME)"
|
|
41
|
+
user="$(env_value MIRROR_USER)"
|
|
42
|
+
if [ -n "$home" ]; then
|
|
43
|
+
home="${home/#\~/$HOME}"
|
|
44
|
+
elif [ -n "$user" ]; then
|
|
45
|
+
home="$HOME/.mirror-minds/$user"
|
|
46
|
+
[ -d "$home" ] || [ ! -d "$HOME/.mirror/$user" ] || home="$HOME/.mirror/$user"
|
|
47
|
+
else
|
|
48
|
+
return 0
|
|
49
|
+
fi
|
|
50
|
+
mkdir -p "$home" 2>/dev/null \
|
|
51
|
+
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Find the bin. MIRROR_BIN names the directory holding `mirror` and
|
|
55
|
+
# `mirror-hook` (`$(npm prefix -g)/bin`), the same trust class as MIRROR_NODE;
|
|
56
|
+
# then the PATH; then the global bin directories a GUI launch leaves off it.
|
|
57
|
+
ENTRY=""
|
|
58
|
+
[ -n "${MIRROR_BIN:-}" ] && [ -x "$MIRROR_BIN/mirror-hook" ] && ENTRY="$MIRROR_BIN/mirror-hook"
|
|
59
|
+
[ -n "$ENTRY" ] || ENTRY="$(command -v mirror-hook 2>/dev/null || true)"
|
|
60
|
+
for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
|
|
61
|
+
[ -n "$ENTRY" ] && break
|
|
62
|
+
[ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
if [ -z "$ENTRY" ]; then
|
|
66
|
+
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."
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
NODE="${MIRROR_NODE:-}"
|
|
71
|
+
if [ -z "$NODE" ] || [ ! -x "$NODE" ]; then
|
|
72
|
+
NODE="$(command -v node 2>/dev/null || true)"
|
|
73
|
+
fi
|
|
74
|
+
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
75
|
+
[ -n "$NODE" ] && break
|
|
76
|
+
[ -x "$candidate" ] && NODE="$candidate"
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
if [ -z "$NODE" ]; then
|
|
80
|
+
printf '{}\n'
|
|
81
|
+
note "node not found on PATH; hook skipped. Set MIRROR_NODE."
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# The payload was consumed by the gate read; re-feed it to the entry.
|
|
86
|
+
printf '%s' "$PAYLOAD" | "$NODE" "$ENTRY" "$HOOK" "$@"
|
|
87
|
+
status=$?
|
|
88
|
+
if [ "$status" -ne 0 ]; then
|
|
89
|
+
printf '{}\n'
|
|
90
|
+
note "node exited $status before the hook finished ($NODE, $("$NODE" --version 2>/dev/null || echo 'version unknown')); Mirror needs Node 24 or later. Set MIRROR_NODE."
|
|
91
|
+
fi
|
|
92
|
+
exit 0
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Mirror Mind hook wrapper — GENERATED by scripts/generate_hook_wrappers.sh.
|
|
3
|
+
# Edit the generator, never this file: every agy wrapper shares these bytes
|
|
4
|
+
# and differs only in the hook name and its stdin gate (CV21.DS7.TS2).
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
HOOK="agy:session-start"
|
|
7
|
+
GATE=''
|
|
8
|
+
|
|
9
|
+
# agy sends the payload on stdin and blocks its agent loop on this process.
|
|
10
|
+
# Read the payload once; when the gate says this invocation is not ours,
|
|
11
|
+
# answer the empty object without resolving Node at all.
|
|
12
|
+
PAYLOAD="$(cat 2>/dev/null || true)"
|
|
13
|
+
if [ -n "$GATE" ] && printf '%s' "$PAYLOAD" | grep -qE "$GATE"; then
|
|
14
|
+
printf '{}\n'
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# This wrapper is the Claude plugin's, and a plugin is copied into Claude
|
|
19
|
+
# Code's own cache: no relative path from here reaches the Mirror tree. It
|
|
20
|
+
# runs the installed `mirror-hook` bin instead, and reads no tree .env.
|
|
21
|
+
REPO_ROOT=""
|
|
22
|
+
|
|
23
|
+
# A hook never fails the user's turn, and never fails in silence: when it
|
|
24
|
+
# cannot do its job it appends ONE line to <mirror home>/hooks.log and exits
|
|
25
|
+
# 0 — after answering agy's contract, so the agent loop never stalls.
|
|
26
|
+
env_file_value() {
|
|
27
|
+
[ -f "$2" ] || return 0
|
|
28
|
+
sed -n -E "s/^[[:space:]]*(export[[:space:]]+)?$1[[:space:]]*=[[:space:]]*//p" "$2" \
|
|
29
|
+
| tail -n 1 | tr -d "\"'\r"
|
|
30
|
+
}
|
|
31
|
+
env_value() {
|
|
32
|
+
local value=""
|
|
33
|
+
if [ -n "${!1+set}" ]; then printf '%s' "${!1}"; return; fi
|
|
34
|
+
[ -z "$REPO_ROOT" ] || value="$(env_file_value "$1" "$REPO_ROOT/.env")"
|
|
35
|
+
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
36
|
+
printf '%s' "$value"
|
|
37
|
+
}
|
|
38
|
+
note() {
|
|
39
|
+
local home user
|
|
40
|
+
home="$(env_value MIRROR_HOME)"
|
|
41
|
+
user="$(env_value MIRROR_USER)"
|
|
42
|
+
if [ -n "$home" ]; then
|
|
43
|
+
home="${home/#\~/$HOME}"
|
|
44
|
+
elif [ -n "$user" ]; then
|
|
45
|
+
home="$HOME/.mirror-minds/$user"
|
|
46
|
+
[ -d "$home" ] || [ ! -d "$HOME/.mirror/$user" ] || home="$HOME/.mirror/$user"
|
|
47
|
+
else
|
|
48
|
+
return 0
|
|
49
|
+
fi
|
|
50
|
+
mkdir -p "$home" 2>/dev/null \
|
|
51
|
+
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Find the bin. MIRROR_BIN names the directory holding `mirror` and
|
|
55
|
+
# `mirror-hook` (`$(npm prefix -g)/bin`), the same trust class as MIRROR_NODE;
|
|
56
|
+
# then the PATH; then the global bin directories a GUI launch leaves off it.
|
|
57
|
+
ENTRY=""
|
|
58
|
+
[ -n "${MIRROR_BIN:-}" ] && [ -x "$MIRROR_BIN/mirror-hook" ] && ENTRY="$MIRROR_BIN/mirror-hook"
|
|
59
|
+
[ -n "$ENTRY" ] || ENTRY="$(command -v mirror-hook 2>/dev/null || true)"
|
|
60
|
+
for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
|
|
61
|
+
[ -n "$ENTRY" ] && break
|
|
62
|
+
[ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
if [ -z "$ENTRY" ]; then
|
|
66
|
+
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."
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
NODE="${MIRROR_NODE:-}"
|
|
71
|
+
if [ -z "$NODE" ] || [ ! -x "$NODE" ]; then
|
|
72
|
+
NODE="$(command -v node 2>/dev/null || true)"
|
|
73
|
+
fi
|
|
74
|
+
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
75
|
+
[ -n "$NODE" ] && break
|
|
76
|
+
[ -x "$candidate" ] && NODE="$candidate"
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
if [ -z "$NODE" ]; then
|
|
80
|
+
printf '{}\n'
|
|
81
|
+
note "node not found on PATH; hook skipped. Set MIRROR_NODE."
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# The payload was consumed by the gate read; re-feed it to the entry.
|
|
86
|
+
printf '%s' "$PAYLOAD" | "$NODE" "$ENTRY" "$HOOK" "$@"
|
|
87
|
+
status=$?
|
|
88
|
+
if [ "$status" -ne 0 ]; then
|
|
89
|
+
printf '{}\n'
|
|
90
|
+
note "node exited $status before the hook finished ($NODE, $("$NODE" --version 2>/dev/null || echo 'version unknown')); Mirror needs Node 24 or later. Set MIRROR_NODE."
|
|
91
|
+
fi
|
|
92
|
+
exit 0
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Mirror Mind hook wrapper — GENERATED by scripts/generate_hook_wrappers.sh.
|
|
3
|
+
# Edit the generator, never this file: every agy wrapper shares these bytes
|
|
4
|
+
# and differs only in the hook name and its stdin gate (CV21.DS7.TS2).
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
HOOK="agy:stop"
|
|
7
|
+
GATE=''
|
|
8
|
+
|
|
9
|
+
# agy sends the payload on stdin and blocks its agent loop on this process.
|
|
10
|
+
# Read the payload once; when the gate says this invocation is not ours,
|
|
11
|
+
# answer the empty object without resolving Node at all.
|
|
12
|
+
PAYLOAD="$(cat 2>/dev/null || true)"
|
|
13
|
+
if [ -n "$GATE" ] && printf '%s' "$PAYLOAD" | grep -qE "$GATE"; then
|
|
14
|
+
printf '{}\n'
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# This wrapper is the Claude plugin's, and a plugin is copied into Claude
|
|
19
|
+
# Code's own cache: no relative path from here reaches the Mirror tree. It
|
|
20
|
+
# runs the installed `mirror-hook` bin instead, and reads no tree .env.
|
|
21
|
+
REPO_ROOT=""
|
|
22
|
+
|
|
23
|
+
# A hook never fails the user's turn, and never fails in silence: when it
|
|
24
|
+
# cannot do its job it appends ONE line to <mirror home>/hooks.log and exits
|
|
25
|
+
# 0 — after answering agy's contract, so the agent loop never stalls.
|
|
26
|
+
env_file_value() {
|
|
27
|
+
[ -f "$2" ] || return 0
|
|
28
|
+
sed -n -E "s/^[[:space:]]*(export[[:space:]]+)?$1[[:space:]]*=[[:space:]]*//p" "$2" \
|
|
29
|
+
| tail -n 1 | tr -d "\"'\r"
|
|
30
|
+
}
|
|
31
|
+
env_value() {
|
|
32
|
+
local value=""
|
|
33
|
+
if [ -n "${!1+set}" ]; then printf '%s' "${!1}"; return; fi
|
|
34
|
+
[ -z "$REPO_ROOT" ] || value="$(env_file_value "$1" "$REPO_ROOT/.env")"
|
|
35
|
+
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
36
|
+
printf '%s' "$value"
|
|
37
|
+
}
|
|
38
|
+
note() {
|
|
39
|
+
local home user
|
|
40
|
+
home="$(env_value MIRROR_HOME)"
|
|
41
|
+
user="$(env_value MIRROR_USER)"
|
|
42
|
+
if [ -n "$home" ]; then
|
|
43
|
+
home="${home/#\~/$HOME}"
|
|
44
|
+
elif [ -n "$user" ]; then
|
|
45
|
+
home="$HOME/.mirror-minds/$user"
|
|
46
|
+
[ -d "$home" ] || [ ! -d "$HOME/.mirror/$user" ] || home="$HOME/.mirror/$user"
|
|
47
|
+
else
|
|
48
|
+
return 0
|
|
49
|
+
fi
|
|
50
|
+
mkdir -p "$home" 2>/dev/null \
|
|
51
|
+
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Find the bin. MIRROR_BIN names the directory holding `mirror` and
|
|
55
|
+
# `mirror-hook` (`$(npm prefix -g)/bin`), the same trust class as MIRROR_NODE;
|
|
56
|
+
# then the PATH; then the global bin directories a GUI launch leaves off it.
|
|
57
|
+
ENTRY=""
|
|
58
|
+
[ -n "${MIRROR_BIN:-}" ] && [ -x "$MIRROR_BIN/mirror-hook" ] && ENTRY="$MIRROR_BIN/mirror-hook"
|
|
59
|
+
[ -n "$ENTRY" ] || ENTRY="$(command -v mirror-hook 2>/dev/null || true)"
|
|
60
|
+
for candidate in "$HOME/.nvm/current/bin" /opt/homebrew/bin /usr/local/bin; do
|
|
61
|
+
[ -n "$ENTRY" ] && break
|
|
62
|
+
[ -x "$candidate/mirror-hook" ] && ENTRY="$candidate/mirror-hook"
|
|
63
|
+
done
|
|
64
|
+
|
|
65
|
+
if [ -z "$ENTRY" ]; then
|
|
66
|
+
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."
|
|
67
|
+
exit 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
NODE="${MIRROR_NODE:-}"
|
|
71
|
+
if [ -z "$NODE" ] || [ ! -x "$NODE" ]; then
|
|
72
|
+
NODE="$(command -v node 2>/dev/null || true)"
|
|
73
|
+
fi
|
|
74
|
+
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
75
|
+
[ -n "$NODE" ] && break
|
|
76
|
+
[ -x "$candidate" ] && NODE="$candidate"
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
if [ -z "$NODE" ]; then
|
|
80
|
+
printf '{}\n'
|
|
81
|
+
note "node not found on PATH; hook skipped. Set MIRROR_NODE."
|
|
82
|
+
exit 0
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# The payload was consumed by the gate read; re-feed it to the entry.
|
|
86
|
+
printf '%s' "$PAYLOAD" | "$NODE" "$ENTRY" "$HOOK" "$@"
|
|
87
|
+
status=$?
|
|
88
|
+
if [ "$status" -ne 0 ]; then
|
|
89
|
+
printf '{}\n'
|
|
90
|
+
note "node exited $status before the hook finished ($NODE, $("$NODE" --version 2>/dev/null || echo 'version unknown')); Mirror needs Node 24 or later. Set MIRROR_NODE."
|
|
91
|
+
fi
|
|
92
|
+
exit 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mirror-mind": {
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"type": "command",
|
|
7
|
+
"command": "agy/session-start.sh",
|
|
8
|
+
"timeout": 60
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"PreInvocation": [
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": "agy/pre-invocation.sh",
|
|
15
|
+
"timeout": 15
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"Stop": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "agy/stop.sh",
|
|
22
|
+
"timeout": 30
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"mirror-mind": {
|
|
4
|
+
"command": "/bin/sh",
|
|
5
|
+
"args": [
|
|
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"
|
|
8
|
+
]
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mirror-mind",
|
|
3
|
+
"description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
|
|
4
|
+
"version": "0.36.0",
|
|
5
|
+
"mcpServers": {
|
|
6
|
+
"mirror-mind": {
|
|
7
|
+
"command": "/bin/sh",
|
|
8
|
+
"args": [
|
|
9
|
+
"-c",
|
|
10
|
+
"mmbin=\"\"; [ -n \"$MIRROR_BIN\" ] && [ -x \"$MIRROR_BIN/mirror\" ] && mmbin=\"$MIRROR_BIN/mirror\"; [ -n \"$mmbin\" ] || mmbin=\"$(command -v mirror 2>/dev/null)\"; for mmdir in \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmbin\" ] && break; [ -x \"$mmdir/mirror\" ] && mmbin=\"$mmdir/mirror\"; done; [ -n \"$mmbin\" ] || { echo \"mirror-mind: mirror not found on PATH; the MCP server cannot start. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory.\" >&2; exit 1; }; mmnode=\"$MIRROR_NODE\"; [ -n \"$mmnode\" ] && [ -x \"$mmnode\" ] || mmnode=\"$(command -v node 2>/dev/null)\"; for mmdir in \"$(dirname \"$mmbin\")\" \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmnode\" ] && break; [ -x \"$mmdir/node\" ] && mmnode=\"$mmdir/node\"; done; [ -n \"$mmnode\" ] || { echo \"mirror-mind: node not found on PATH; the MCP server cannot start. Set MIRROR_NODE.\" >&2; exit 1; }; exec \"$mmnode\" \"$mmbin\" mcp"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Mirror Mind — Operating Instructions
|
|
2
|
+
|
|
3
|
+
<!-- This file ships in the mirror-mind package and is read by every runtime:
|
|
4
|
+
Pi and Codex read it from the project root natively, Claude Code through
|
|
5
|
+
the @AGENTS.md import in CLAUDE.md, Antigravity as the plugin's rules, and
|
|
6
|
+
an installed package's Pi session receives it from the extension
|
|
7
|
+
(CV22.DS10.US3, D13).
|
|
8
|
+
It holds nothing about this repository; that is CLAUDE.md's part. -->
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Mirror Operating Instructions
|
|
13
|
+
|
|
14
|
+
Applies to all sessions, regardless of project.
|
|
15
|
+
|
|
16
|
+
### Operating Modes
|
|
17
|
+
|
|
18
|
+
The mirror operates in four modes, chosen automatically based on context:
|
|
19
|
+
Mirror, Builder, Explorer, and Soul.
|
|
20
|
+
|
|
21
|
+
Each skill below is named three ways: `/mm-<skill>` in Pi and Grok Build,
|
|
22
|
+
`$mm-<skill>` in Codex, `/mm:<skill>` in Claude Code. A **plugin** install prefixes the plugin's
|
|
23
|
+
name — `/mirror-mind:mm:<skill>` in Claude Code's menu and in Antigravity,
|
|
24
|
+
`$mirror-mind:mm:<skill>` in Codex, `mirror-mind:mm-<skill>` as the skill's own
|
|
25
|
+
name — and the forms below refer to the same skill.
|
|
26
|
+
|
|
27
|
+
**Mirror Mode** — activate for: life decisions, feelings, business strategy,
|
|
28
|
+
writing, mentoring, health, existential questions, sensemaking, psychological
|
|
29
|
+
tensions, class preparation, product launches, or any topic asking for personal
|
|
30
|
+
reflection or positioning.
|
|
31
|
+
|
|
32
|
+
How to operate: load the mirror skill (`/mm-mirror`, `$mm-mirror`, or
|
|
33
|
+
`/mm:mirror`). Load identity, route persona, search attachments, answer in first
|
|
34
|
+
person, and record the response.
|
|
35
|
+
|
|
36
|
+
**Builder Mode** — activate for: code, project structure, YAML editing, bugs,
|
|
37
|
+
implementation, architecture, or any software engineering task.
|
|
38
|
+
|
|
39
|
+
How to operate: read code, edit files, run commands, propose technical
|
|
40
|
+
solutions, keep docs updated when code changes. For a journey, use `/mm-build
|
|
41
|
+
<slug>` / `$mm-build <slug>` / `/mm:build <slug>` — loads journey context and
|
|
42
|
+
project docs.
|
|
43
|
+
|
|
44
|
+
**Explorer Mode** — activate for the uncertain middle before construction:
|
|
45
|
+
exploring a possibility space, holding uncertainty before committing to build,
|
|
46
|
+
developing an Exploratory Story, surfacing attractors, proposing small
|
|
47
|
+
experiments, or preparing a Builder handoff.
|
|
48
|
+
|
|
49
|
+
How to operate: use `/mm-explore <slug>` / `$mm-explore <slug>` / `/mm:explore
|
|
50
|
+
<slug>` to activate Explorer Mode for a journey and resume the active Exploratory
|
|
51
|
+
Story when one exists. Follow `.pi/skills/mm-explore/SKILL.md`: render required
|
|
52
|
+
story surfaces before interpretation, thicken the story as the narrative changes,
|
|
53
|
+
and keep exploration durable. Explorer Mode preserves uncertainty — it does not
|
|
54
|
+
implement. For operational mutation requests, name the `△ EXPLORER → BUILDER
|
|
55
|
+
BOUNDARY` and route them through Builder Mode via an explicit handoff. Deactivate
|
|
56
|
+
with `/mm-explore deactivate`, which returns to Mirror Mode while preserving
|
|
57
|
+
sticky journey context.
|
|
58
|
+
|
|
59
|
+
**Soul Mode** — activate when the user asks to enter Soul Mode, open Soul Mode,
|
|
60
|
+
or continue an active Soul Mode ritual.
|
|
61
|
+
|
|
62
|
+
How to operate: use `/mm-soul [slug]` / `$mm-soul [slug]` / `/mm:soul [slug]`
|
|
63
|
+
to render the entry surface. While Soul Mode is active, follow
|
|
64
|
+
`.pi/skills/mm-soul/SKILL.md`: listen to the user's answer as living field,
|
|
65
|
+
withhold Possible Listenings when the material is thin, and when living matter
|
|
66
|
+
appears call the front door's `soul listen ...` (the exact invocation is in the
|
|
67
|
+
skill) to render the Possible Listenings surface at the end of the response. This renderer call is required
|
|
68
|
+
Soul Mode behavior, not optional tool use. Soul Mode must not mutate files,
|
|
69
|
+
implement stories, run implementation commands, package releases, or change
|
|
70
|
+
project state. For operational requests, name the `☾ SOUL → BUILDER BOUNDARY`
|
|
71
|
+
and ask whether to switch to Builder Mode for the same journey.
|
|
72
|
+
|
|
73
|
+
Builder activation boundary: activating Builder Mode or loading a journey is
|
|
74
|
+
context setup only. After loading the context and required docs, stop and ask
|
|
75
|
+
what work should be done next. Do not edit files, create tests, run
|
|
76
|
+
implementation, start TDD, or mutate project state until the user gives an
|
|
77
|
+
explicit implementation or documentation instruction, such as implement, fix,
|
|
78
|
+
edit, create, run tests, or names a specific story to execute. Context
|
|
79
|
+
activation is not execution consent.
|
|
80
|
+
|
|
81
|
+
Ariad surface transport invariant: when command output contains marked Ariad
|
|
82
|
+
surface blocks (`<<<ARIAD:...>>>` through `<<<END:...>>>`), preserve every marked
|
|
83
|
+
block verbatim in the assistant response before commentary. Do not summarize,
|
|
84
|
+
translate, reorder, trim, rewrap, re-indent, reformat, or replace marked surfaces
|
|
85
|
+
with prose. This is a general rendering rule for Ariad surfaces, not a
|
|
86
|
+
phase-specific behavior.
|
|
87
|
+
|
|
88
|
+
**Ambiguity:** if the mode is unclear, ask whether the user wants personal
|
|
89
|
+
reflection or project construction.
|
|
90
|
+
|
|
91
|
+
**Journey Status** — shortcut within Mirror Mode. Activate when the user asks
|
|
92
|
+
"How are we doing?", "What's the status of X?", or any question about progress
|
|
93
|
+
or roadmap. Dispatch to `/mm-journey` or `/mm-journeys`. When the user asks for
|
|
94
|
+
the journey list in natural language, use `/mm-journeys` and preserve its
|
|
95
|
+
hierarchical rendering without flattening or reformatting it.
|
|
96
|
+
|
|
97
|
+
**Commits:** use descriptive English commit messages. Explain the WHY, not just
|
|
98
|
+
what was done. Prefer small commits with clear review boundaries.
|
|
99
|
+
|
|
100
|
+
### Ego-Persona Model
|
|
101
|
+
|
|
102
|
+
The mirror has one voice: the ego. Personas are specialized lenses activated by
|
|
103
|
+
the ego according to context.
|
|
104
|
+
|
|
105
|
+
**Automatic routing:** activate a persona when the topic clearly belongs to a
|
|
106
|
+
specialized domain, the depth required exceeds the generic ego repertoire, or
|
|
107
|
+
the user explicitly asks for a persona.
|
|
108
|
+
|
|
109
|
+
**Routing protocol:** persona routing is data-driven. Each persona in the
|
|
110
|
+
database carries `routing_keywords` and a routing descriptor. At runtime,
|
|
111
|
+
`detectPersona` (`ts/src/persona/detectPersona.ts`) scores the query against
|
|
112
|
+
those keywords. If no persona scores above threshold, the ego answers alone. To
|
|
113
|
+
inspect active routing:
|
|
114
|
+
`mirror detect-persona "<query>"`.
|
|
115
|
+
|
|
116
|
+
**Signature format:**
|
|
117
|
+
|
|
118
|
+
When the ego answers alone — no signature.
|
|
119
|
+
|
|
120
|
+
When a persona is active:
|
|
121
|
+
```text
|
|
122
|
+
◇ persona-name
|
|
123
|
+
|
|
124
|
+
[first-person answer, unified voice]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
When switching personas:
|
|
128
|
+
```text
|
|
129
|
+
◇ product-designer
|
|
130
|
+
|
|
131
|
+
[analysis...]
|
|
132
|
+
|
|
133
|
+
◇ therapist
|
|
134
|
+
|
|
135
|
+
[reflection...]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Rules: `◇` plus persona name on its own line; voice stays first person and unified.
|
|
139
|
+
|
|
140
|
+
### Hard Constraints
|
|
141
|
+
|
|
142
|
+
- **Truth:** do not invent data. If uncertain, say so.
|
|
143
|
+
- **Service:** intellectual partner, not task executor. Question, refine, align —
|
|
144
|
+
do not execute without thinking.
|
|
145
|
+
|
|
146
|
+
### Available Skills
|
|
147
|
+
|
|
148
|
+
**Core modes:**
|
|
149
|
+
- `mm-mirror` — activates Mirror Mode — `.pi/skills/mm-mirror/SKILL.md`
|
|
150
|
+
- `mm-build` — activates Builder Mode for a journey — `.pi/skills/mm-build/SKILL.md`
|
|
151
|
+
- `mm-explore` — activates Explorer Mode for a journey — `.pi/skills/mm-explore/SKILL.md`
|
|
152
|
+
- `mm-soul` — activates Soul Mode ritual entry — `.pi/skills/mm-soul/SKILL.md`
|
|
153
|
+
|
|
154
|
+
**Journeys and tasks:**
|
|
155
|
+
- `mm-journeys` — compact journey list — `.pi/skills/mm-journeys/SKILL.md`
|
|
156
|
+
- `mm-journey` — detailed journey status — `.pi/skills/mm-journey/SKILL.md`
|
|
157
|
+
- `mm-tasks` — task management by journey — `.pi/skills/mm-tasks/SKILL.md`
|
|
158
|
+
- `mm-week` — weekly planning — `.pi/skills/mm-week/SKILL.md`
|
|
159
|
+
|
|
160
|
+
**Memory and inspection:**
|
|
161
|
+
- `mm-memories` — recorded memories — `.pi/skills/mm-memories/SKILL.md`
|
|
162
|
+
- `mm-conversations` — recent conversations list — `.pi/skills/mm-conversations/SKILL.md`
|
|
163
|
+
- `mm-recall` — load messages from a previous conversation — `.pi/skills/mm-recall/SKILL.md`
|
|
164
|
+
|
|
165
|
+
**Content:**
|
|
166
|
+
- `mm-journal` — personal journal entry — `.pi/skills/mm-journal/SKILL.md`
|
|
167
|
+
|
|
168
|
+
**Identity:**
|
|
169
|
+
- `mm-seed` — seed identity YAML files into the database — `.pi/skills/mm-seed/SKILL.md`
|
|
170
|
+
- `mm-identity` — read and update identity in the database — `.pi/skills/mm-identity/SKILL.md`
|
|
171
|
+
|
|
172
|
+
**Session control:**
|
|
173
|
+
- `mm-mute` — toggle conversation logging — `.pi/skills/mm-mute/SKILL.md`
|
|
174
|
+
- `mm-new` — start a new conversation — `.pi/skills/mm-new/SKILL.md`
|
|
175
|
+
- `mm-discard` — discard the current conversation before quitting — `.pi/skills/mm-discard/SKILL.md`
|
|
176
|
+
|
|
177
|
+
**Memory cultivation:**
|
|
178
|
+
- `mm-consolidate` — scan memories for patterns and propose consolidation — `.pi/skills/mm-consolidate/SKILL.md`
|
|
179
|
+
- `mm-shadow` — surface and promote shadow-layer observations — `.pi/skills/mm-shadow/SKILL.md`
|
|
180
|
+
|
|
181
|
+
**Utilities:**
|
|
182
|
+
- `mm-consult` — ask other LLMs through OpenRouter — `.pi/skills/mm-consult/SKILL.md`
|
|
183
|
+
- `mm-backup` — memory database backup — `.pi/skills/mm-backup/SKILL.md`
|
|
184
|
+
- `mm-welcome` — render the state-aware welcome card on demand — `.pi/skills/mm-welcome/SKILL.md`
|
|
185
|
+
- `mm-release-notes` — show Mirror Mind release notes — `.pi/skills/mm-release-notes/SKILL.md`
|
|
186
|
+
- `mm-update` — update the local Mirror runtime — `.pi/skills/mm-update/SKILL.md`
|
|
187
|
+
- `mm-help` — list available commands — `.pi/skills/mm-help/SKILL.md`
|
|
188
|
+
|
|
189
|
+
Full command reference: [REFERENCE.md](REFERENCE.md)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The agy transcript reader (CV21.DS7.TS3).
|
|
2
|
+
//
|
|
3
|
+
// Antigravity's hook payloads carry no prompt or reply text (DS12.TS3 L7),
|
|
4
|
+
// so logging reads the JSONL transcript agy writes. The step shapes are
|
|
5
|
+
// probed from real agy 1.4.2 transcripts (the US1/TS2/TS3 probe discipline;
|
|
6
|
+
// evidence/transcript-shape-20261018.md):
|
|
7
|
+
//
|
|
8
|
+
// * a user turn is a `USER_INPUT` step (`source: USER_EXPLICIT`) whose
|
|
9
|
+
// content string wraps the request in `<USER_REQUEST>…</USER_REQUEST>`,
|
|
10
|
+
// followed by `<ADDITIONAL_METADATA>…`;
|
|
11
|
+
// * the model's answer is the `PLANNER_RESPONSE` step(s) (`source: MODEL`).
|
|
12
|
+
// MOST of these are EMPTY — intermediate tool steps — so the reply is
|
|
13
|
+
// the non-empty response text in the turn, not every PLANNER_RESPONSE.
|
|
14
|
+
// (The plan guessed `AGENT_MESSAGE`; the probe corrected both the type
|
|
15
|
+
// and the emptiness.)
|
|
16
|
+
//
|
|
17
|
+
// A turn runs from one `USER_INPUT` to just before the next. `step_index` is
|
|
18
|
+
// a monotonic per-conversation ordinal; `lastStepIndex` is the turn's
|
|
19
|
+
// high-water mark, which the Stop handler stores so a re-fired Stop logs
|
|
20
|
+
// nothing new.
|
|
21
|
+
|
|
22
|
+
import { parseJsonl } from "#conversation/transcriptBackfill.ts";
|
|
23
|
+
|
|
24
|
+
export interface AgyTurn {
|
|
25
|
+
readonly userText: string;
|
|
26
|
+
readonly replyText: string;
|
|
27
|
+
/** The highest step_index this turn logged: the idempotency high-water mark. */
|
|
28
|
+
readonly lastStepIndex: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stringField(entry: unknown, key: string): string {
|
|
32
|
+
if (typeof entry !== "object" || entry === null) return "";
|
|
33
|
+
const value = (entry as Record<string, unknown>)[key];
|
|
34
|
+
return typeof value === "string" ? value : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function numberField(entry: unknown, key: string): number {
|
|
38
|
+
if (typeof entry !== "object" || entry === null) return -1;
|
|
39
|
+
const value = (entry as Record<string, unknown>)[key];
|
|
40
|
+
return typeof value === "number" ? value : -1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const USER_REQUEST = /<USER_REQUEST>\s*([\s\S]*?)\s*<\/USER_REQUEST>/;
|
|
44
|
+
|
|
45
|
+
/** The request body agy wraps the user's words in, or the whole string. */
|
|
46
|
+
export function extractUserRequest(content: string): string {
|
|
47
|
+
const match = USER_REQUEST.exec(content);
|
|
48
|
+
return (match ? match[1] : content).trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Join the turn's non-empty reply parts into one string, the same rule the
|
|
53
|
+
* Claude reader's `assistantText` uses (trimmed, blank-line separated).
|
|
54
|
+
* Shared so "how parts become one reply" lives in one place.
|
|
55
|
+
*/
|
|
56
|
+
export function joinReplyParts(parts: readonly string[]): string {
|
|
57
|
+
return parts
|
|
58
|
+
.map((part) => part.trim())
|
|
59
|
+
.filter((part) => part.length > 0)
|
|
60
|
+
.join("\n\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The transcript's completed turns, in order. Each pairs the user's request
|
|
65
|
+
* with the model's reply and carries the turn's high-water `step_index`.
|
|
66
|
+
* A turn with no reply text yet (the model has not answered) is omitted:
|
|
67
|
+
* the Stop handler logs it on the Stop that follows the answer.
|
|
68
|
+
*/
|
|
69
|
+
export function readAgyTurns(transcriptPath: string): AgyTurn[] {
|
|
70
|
+
let entries: unknown[];
|
|
71
|
+
try {
|
|
72
|
+
entries = parseJsonl(transcriptPath);
|
|
73
|
+
} catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const turns: AgyTurn[] = [];
|
|
78
|
+
let userText: string | null = null;
|
|
79
|
+
let replyParts: string[] = [];
|
|
80
|
+
let lastStepIndex = -1;
|
|
81
|
+
|
|
82
|
+
const flush = () => {
|
|
83
|
+
if (userText === null) return;
|
|
84
|
+
const replyText = joinReplyParts(replyParts);
|
|
85
|
+
if (replyText) turns.push({ userText, replyText, lastStepIndex });
|
|
86
|
+
userText = null;
|
|
87
|
+
replyParts = [];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const type = stringField(entry, "type");
|
|
92
|
+
if (type === "USER_INPUT") {
|
|
93
|
+
flush();
|
|
94
|
+
userText = extractUserRequest(stringField(entry, "content"));
|
|
95
|
+
lastStepIndex = numberField(entry, "step_index");
|
|
96
|
+
} else if (type === "PLANNER_RESPONSE" && userText !== null) {
|
|
97
|
+
const content = stringField(entry, "content").trim();
|
|
98
|
+
const stepIndex = numberField(entry, "step_index");
|
|
99
|
+
if (stepIndex > lastStepIndex) lastStepIndex = stepIndex;
|
|
100
|
+
if (content) replyParts.push(content);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
flush();
|
|
104
|
+
return turns;
|
|
105
|
+
}
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
// One rule for every runtime: a Claude Code user who types `$mm-journeys`,
|
|
30
30
|
// which Claude Code does not understand, is still invoking a skill, and that
|
|
31
31
|
// turn is not conversation either. The rule has one home for every hook that
|
|
32
|
-
// runs in our process (the Claude-family logger,
|
|
32
|
+
// runs in our process (the Claude-family logger, agy's Stop); the Pi
|
|
33
33
|
// extension, a separate process in Pi's file, keeps its own `/` check.
|
|
34
34
|
|
|
35
35
|
const SKILL_SEGMENT = "[a-z][a-z0-9_-]*";
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// (extraction, then title/summary/tags). Some runtimes give a SessionEnd hook
|
|
6
6
|
// less time than one model call: Codex runs it synchronously under a 1 s
|
|
7
7
|
// default (DS12.TS2 L2; DS2's live `/hooks` table), Grok under 1.5 s
|
|
8
|
-
// (DS12.TS4). Pi
|
|
9
|
-
//
|
|
8
|
+
// (DS12.TS4). Pi already closes without extraction at exit and
|
|
9
|
+
// lets the next session start extract (`session-end-pi`). On a runtime that
|
|
10
10
|
// bounds the hook, the shared Claude-family `session-end` does the same, and a
|
|
11
11
|
// little more: it also skips the metadata finalization, and flags the
|
|
12
12
|
// conversation so the next start's maintenance finishes it.
|