@memnexus-ai/mx-agent-cli 0.1.165 → 0.1.167
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/dist/__tests__/claude-md-compose.test.d.ts +16 -0
- package/dist/__tests__/claude-md-compose.test.d.ts.map +1 -0
- package/dist/__tests__/claude-md-compose.test.js +116 -0
- package/dist/__tests__/claude-md-compose.test.js.map +1 -0
- package/dist/__tests__/claude-sync.test.js +15 -0
- package/dist/__tests__/claude-sync.test.js.map +1 -1
- package/dist/__tests__/config-generation.test.d.ts +7 -0
- package/dist/__tests__/config-generation.test.d.ts.map +1 -0
- package/dist/__tests__/config-generation.test.js +141 -0
- package/dist/__tests__/config-generation.test.js.map +1 -0
- package/dist/__tests__/config-sync.test.d.ts +10 -0
- package/dist/__tests__/config-sync.test.d.ts.map +1 -0
- package/dist/__tests__/config-sync.test.js +180 -0
- package/dist/__tests__/config-sync.test.js.map +1 -0
- package/dist/__tests__/project-config.test.js +1 -0
- package/dist/__tests__/project-config.test.js.map +1 -1
- package/dist/agent-config/.mx-config-generation.json +4 -0
- package/dist/agent-config/CLAUDE.md.template +140 -0
- package/dist/agent-config/OWNERS.md +51 -0
- package/dist/agent-config/agents/bar-raiser.md +205 -0
- package/dist/agent-config/agents/dogfood-auditor.md +243 -0
- package/dist/agent-config/agents/git-expert.md +380 -0
- package/dist/agent-config/agents/implementation-engineer.md +387 -0
- package/dist/agent-config/agents/memory-creator.md +330 -0
- package/dist/agent-config/agents/prd-writer.md +478 -0
- package/dist/agent-config/agents/prfaq-writer.md +344 -0
- package/dist/agent-config/agents/prior-art-researcher.md +264 -0
- package/dist/agent-config/agents/qa-tester.md +406 -0
- package/dist/agent-config/agents/red-team.md +330 -0
- package/dist/agent-config/agents/security-reviewer.md +300 -0
- package/dist/agent-config/agents/status-reporter.md +297 -0
- package/dist/agent-config/claude-md/CLAUDE.core.md +105 -0
- package/dist/agent-config/claude-md/CLAUDE.overlay.memnexus.md +68 -0
- package/dist/agent-config/claude-md/compose.mjs +179 -0
- package/dist/agent-config/claude-md/vars.memnexus.json +4 -0
- package/dist/agent-config/hooks/auto-capture-precompact.sh +176 -0
- package/dist/agent-config/hooks/auto-capture-session-end.sh +178 -0
- package/dist/agent-config/hooks/auto-checkpoint.sh +102 -0
- package/dist/agent-config/hooks/ci-validate-hook.sh +499 -0
- package/dist/agent-config/hooks/ci-validate-routes.conf +16 -0
- package/dist/agent-config/hooks/delegation-audit.sh +61 -0
- package/dist/agent-config/hooks/deploy-verification-reminder.sh +179 -0
- package/dist/agent-config/hooks/git-mutation-guard.sh +219 -0
- package/dist/agent-config/hooks/iteration-log-check.sh +212 -0
- package/dist/agent-config/hooks/memory-checkpoint-reminder.sh +103 -0
- package/dist/agent-config/hooks/mx-guard-hook.sh +77 -0
- package/dist/agent-config/hooks/named-memory-write-guard.sh +237 -0
- package/dist/agent-config/hooks/reload-checkpoint.sh +68 -0
- package/dist/agent-config/hooks/set-terminal-appearance.sh +34 -0
- package/dist/agent-config/hooks/workflow-dispatch-guard.sh +184 -0
- package/dist/agent-config/hooks/worktree-guard.sh +258 -0
- package/dist/agent-config/rules/eval-harness.md +33 -0
- package/dist/agent-config/rules/frontend.md +17 -0
- package/dist/agent-config/rules/mcp-protocol.md +15 -0
- package/dist/agent-config/rules/retrieval.md +17 -0
- package/dist/agent-config/settings.json +196 -0
- package/dist/agent-config/shared/eval-tiered-rigor.md +15 -0
- package/dist/agent-config/shared/team-operating-rules.md +37 -0
- package/dist/agent-config/skills/eval-claim-impact/SKILL.md +20 -0
- package/dist/agent-config/skills/eval-harness-review/SKILL.md +21 -0
- package/dist/agent-config/skills/mx-checkpoint/SKILL.md +111 -0
- package/dist/agent-config/skills/mx-fix-pr/SKILL.md +90 -0
- package/dist/agent-config/skills/mx-gap-assessment/SKILL.md +90 -0
- package/dist/agent-config/skills/mx-iteration-close/SKILL.md +81 -0
- package/dist/agent-config/skills/mx-post-merge-sync/SKILL.md +83 -0
- package/dist/agent-config/skills/mx-pre-pr-check/SKILL.md +85 -0
- package/dist/agent-config/skills/mx-save/SKILL.md +102 -0
- package/dist/agent-config/skills/mx-session-start/SKILL.md +85 -0
- package/dist/agent-config/skills/team-gate-checklist/SKILL.md +19 -0
- package/dist/commands/config-sync.d.ts +39 -0
- package/dist/commands/config-sync.d.ts.map +1 -0
- package/dist/commands/config-sync.js +212 -0
- package/dist/commands/config-sync.js.map +1 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +3 -0
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +12 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +6 -0
- package/dist/commands/start.js.map +1 -1
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/bundled-config.d.ts +44 -0
- package/dist/lib/bundled-config.d.ts.map +1 -0
- package/dist/lib/bundled-config.js +164 -0
- package/dist/lib/bundled-config.js.map +1 -0
- package/dist/lib/claude.d.ts.map +1 -1
- package/dist/lib/claude.js +9 -3
- package/dist/lib/claude.js.map +1 -1
- package/dist/lib/config-generation.d.ts +36 -0
- package/dist/lib/config-generation.d.ts.map +1 -0
- package/dist/lib/config-generation.js +73 -0
- package/dist/lib/config-generation.js.map +1 -0
- package/dist/lib/project-config.d.ts +7 -0
- package/dist/lib/project-config.d.ts.map +1 -1
- package/dist/lib/project-config.js +4 -0
- package/dist/lib/project-config.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# memory-checkpoint-reminder.sh — PreToolUse hook (all tools)
|
|
3
|
+
# Reminds agents to save a memory checkpoint when 50+ tool calls
|
|
4
|
+
# have occurred since the last memory save.
|
|
5
|
+
#
|
|
6
|
+
# Fires on: PreToolUse (empty matcher — all tool calls)
|
|
7
|
+
# Input: JSON on stdin with tool_name and tool_input
|
|
8
|
+
# Output: reminder message to stdout when counter >= 50 (non-blocking)
|
|
9
|
+
# Exit: always 0 (never blocks tool calls)
|
|
10
|
+
|
|
11
|
+
# Fail-open: any error → exit 0 silently
|
|
12
|
+
trap 'exit 0' ERR
|
|
13
|
+
set -u
|
|
14
|
+
|
|
15
|
+
# Save the real stderr to fd 3 up front. Section 2 opens the advisory lock with
|
|
16
|
+
# `exec 200>"$LOCK_FILE" 2>/dev/null`, and bash applies that `2>/dev/null` to the
|
|
17
|
+
# whole shell — permanently redirecting fd 2 to /dev/null. The threshold reminder
|
|
18
|
+
# below therefore writes to fd 3 (the preserved real stderr) so the harness can
|
|
19
|
+
# surface it (KI-HOOKS-STDERR-SYSTEMIC). Fail-open if fd 3 can't be opened.
|
|
20
|
+
exec 3>&2 || true
|
|
21
|
+
|
|
22
|
+
INPUT=$(cat)
|
|
23
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
24
|
+
|
|
25
|
+
WORKTREE_NAME="${CLAUDE_WORKTREE_NAME:-default}"
|
|
26
|
+
# Sanitize: alphanumeric, hyphens, underscores only — prevent path traversal
|
|
27
|
+
WORKTREE_NAME="${WORKTREE_NAME//[^a-zA-Z0-9_-]/}"
|
|
28
|
+
WORKTREE_NAME="${WORKTREE_NAME:-default}"
|
|
29
|
+
COUNTER_FILE="/tmp/.mx-checkpoint-counter-${WORKTREE_NAME}"
|
|
30
|
+
THRESHOLD=50
|
|
31
|
+
|
|
32
|
+
# Reset counter if it's stale (from a previous session — >30 min old)
|
|
33
|
+
if [[ -f "$COUNTER_FILE" ]]; then
|
|
34
|
+
MTIME=$(stat -c %Y "$COUNTER_FILE" 2>/dev/null || stat -f %m "$COUNTER_FILE" 2>/dev/null || echo "0")
|
|
35
|
+
NOW=$(date +%s)
|
|
36
|
+
AGE=$((NOW - MTIME))
|
|
37
|
+
if [[ "$AGE" -gt 1800 ]]; then
|
|
38
|
+
echo "0" > "$COUNTER_FILE" 2>/dev/null || true
|
|
39
|
+
fi
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# ── 1. Detect memory save operations ────────────────────────────────────────
|
|
43
|
+
# Reset counter if this tool call IS a memory save:
|
|
44
|
+
# - MCP tools: mcp__memnexus__create_memory, mcp__memnexus__manage_memory
|
|
45
|
+
# (also handle mcp__MX__ prefix used in some environments)
|
|
46
|
+
# - Bash tool: command contains "mx memories create" or "mx memories update"
|
|
47
|
+
|
|
48
|
+
IS_MEMORY_SAVE=0
|
|
49
|
+
|
|
50
|
+
if [[ "$TOOL_NAME" == *"create_memory"* || "$TOOL_NAME" == *"manage_memory"* ]]; then
|
|
51
|
+
IS_MEMORY_SAVE=1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
55
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
56
|
+
if [[ "$COMMAND" == *"mx memories create"* || "$COMMAND" == *"mx memories update"* ]]; then
|
|
57
|
+
IS_MEMORY_SAVE=1
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
if [[ "$IS_MEMORY_SAVE" -eq 1 ]]; then
|
|
62
|
+
# Reset counter — memory was just saved
|
|
63
|
+
echo "0" > "$COUNTER_FILE" 2>/dev/null || true
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# ── 2. Increment counter (use flock for atomic operation if available) ────────
|
|
68
|
+
LOCK_FILE="${COUNTER_FILE}.lock"
|
|
69
|
+
CURRENT=0
|
|
70
|
+
|
|
71
|
+
if command -v flock >/dev/null 2>&1; then
|
|
72
|
+
exec 200>"$LOCK_FILE" 2>/dev/null
|
|
73
|
+
flock -x 200 2>/dev/null || true
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
if [[ -f "$COUNTER_FILE" ]]; then
|
|
77
|
+
CURRENT=$(cat "$COUNTER_FILE" 2>/dev/null || echo "0")
|
|
78
|
+
[[ "$CURRENT" =~ ^[0-9]+$ ]] || CURRENT=0
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
CURRENT=$((CURRENT + 1))
|
|
82
|
+
echo "$CURRENT" > "$COUNTER_FILE" 2>/dev/null || true
|
|
83
|
+
|
|
84
|
+
# ── 3. Emit reminder if threshold reached ────────────────────────────────────
|
|
85
|
+
if [[ "$CURRENT" -ge "$THRESHOLD" ]]; then
|
|
86
|
+
# Reset counter — after save + retry, next 50 calls proceed unblocked
|
|
87
|
+
echo "0" > "$COUNTER_FILE" 2>/dev/null || true
|
|
88
|
+
|
|
89
|
+
# Route to the preserved real stderr (fd 3) — exit-2 PreToolUse messages
|
|
90
|
+
# surface via stderr, not stdout (KI-HOOKS-STDERR-SYSTEMIC). fd 2 may have been
|
|
91
|
+
# redirected to /dev/null by the flock exec above, so fd 3 is used here.
|
|
92
|
+
{
|
|
93
|
+
echo ""
|
|
94
|
+
echo "⚠️ Memory checkpoint required: ${CURRENT} tool calls since last memory save."
|
|
95
|
+
echo "1. Save a checkpoint now (required before continuing):"
|
|
96
|
+
echo " mx memories create --conversation-id \"<conv_id>\" --content \"[what you've done]. Next: [next step].\" --topics \"<component>,in-progress\""
|
|
97
|
+
echo "2. After saving, retry your original tool call."
|
|
98
|
+
echo ""
|
|
99
|
+
} >&3
|
|
100
|
+
exit 2 # Surface this message to the agent — exit 2 is required for PreToolUse visibility
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
exit 0
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# mx-guard pre-push enforcement hook
|
|
3
|
+
# Runs mx-guard check before allowing git push commands.
|
|
4
|
+
# Deployed by mx-agent create via syncClaudeConfig().
|
|
5
|
+
#
|
|
6
|
+
# Exit codes:
|
|
7
|
+
# 0 = allow (not a push command, or mx-guard passes)
|
|
8
|
+
# 2 = block (mx-guard found errors)
|
|
9
|
+
|
|
10
|
+
# Read hook input from stdin
|
|
11
|
+
INPUT=$(cat)
|
|
12
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
|
13
|
+
|
|
14
|
+
# Cheap literal pre-filter (ported from ci-validate-hook.sh, #3731): bail before
|
|
15
|
+
# the regex on any command that can't contain "push". Keeps the pattern match
|
|
16
|
+
# off the hot path for the vast majority of commands.
|
|
17
|
+
case "$COMMAND" in
|
|
18
|
+
*push*) ;;
|
|
19
|
+
*) exit 0 ;;
|
|
20
|
+
esac
|
|
21
|
+
|
|
22
|
+
# Segment-aware push matcher (H1, ported from ci-validate-hook.sh #3731; SF-3).
|
|
23
|
+
# The old `^git\ push` matcher was start-anchored, so compound commands
|
|
24
|
+
# (cd $CLAUDE_WORKTREE_PATH/<pkg> && git push) and other routine forms bypassed
|
|
25
|
+
# validation entirely. Match "git push" when it BEGINS a command segment.
|
|
26
|
+
# Segment starts: start-of-command (^); a separator ; & | { ( or a control char
|
|
27
|
+
# (newline/tab — [:cntrl:] matches newline but NOT space); a shell keyword
|
|
28
|
+
# then/do/else. SF-3 added `(` to the start class and `)` to the trailing group
|
|
29
|
+
# so the subshell form (git push) matches and closes correctly. Also tolerated
|
|
30
|
+
# before git: leading redirections (`2>/dev/null `), env-var assignments
|
|
31
|
+
# (VAR=1 git push), and launcher/wrapper words (sudo/env/nice/nohup/time/
|
|
32
|
+
# timeout/ionice/stdbuf/command/xargs/setsid, optional numeric arg) so
|
|
33
|
+
# wrapper-prefixed pushes aren't skipped. GIT GLOBAL OPTIONS between `git` and
|
|
34
|
+
# `push` are consumed (High, #3934): `-C <path>`, `-c <k>=<v>`,
|
|
35
|
+
# `--git-dir[=| ]<p>`, `--work-tree[=| ]<p>`, `--namespace[=| ]<p>`,
|
|
36
|
+
# `--no-pager`, and generic single/double-dash forms with or without values.
|
|
37
|
+
# Pre-#3934 only `-C <path>` was consumed, so `git --no-pager push` and
|
|
38
|
+
# `git -c core.sshCommand=x push` skipped validation. Kept consistent with
|
|
39
|
+
# git-mutation-guard GITSEG.
|
|
40
|
+
PUSH_RE='(^|[;&|{([:cntrl:]]|[[:space:]](then|do|else)[[:space:]])[[:space:]]*([0-9]*[<>][^[:space:]]*[[:space:]]+|[A-Za-z_][A-Za-z0-9_]*=[^[:space:];&|]*[[:space:]]+|(sudo|env|nice|nohup|time|timeout|ionice|stdbuf|command|xargs|setsid)[[:space:]]+([0-9]+[[:space:]]+)?)*git([[:space:]]+(-C[[:space:]]+[^[:space:];&|]+|-c[[:space:]]+[^[:space:];&|]+|--(git-dir|work-tree|namespace|super-prefix)(=[^[:space:];&|]*|[[:space:]]+[^[:space:];&|]+)|--[A-Za-z][A-Za-z0-9-]*=[^[:space:];&|]*|--[A-Za-z][A-Za-z0-9-]*|-[A-Za-z]))*[[:space:]]+push([[:space:]]|[;&|)]|$)'
|
|
41
|
+
if [[ ! "$COMMAND" =~ $PUSH_RE ]]; then
|
|
42
|
+
exit 0
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# All hook-authored messages go to stderr — the harness surfaces a blocking
|
|
46
|
+
# hook's stderr, not its stdout (KI-HOOKS-STDERR-SYSTEMIC).
|
|
47
|
+
|
|
48
|
+
# Check if mx-guard is available
|
|
49
|
+
if ! command -v mx-guard &>/dev/null; then
|
|
50
|
+
{
|
|
51
|
+
echo "⚠ mx-guard not installed — skipping pre-push validation"
|
|
52
|
+
echo " Install: pnpm install -g @memnexus-ai/mx-guard"
|
|
53
|
+
} >&2
|
|
54
|
+
exit 0
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Run mx-guard check (change-aware — only checks files on current branch).
|
|
58
|
+
# Route its output to stderr so its findings are visible on the block path.
|
|
59
|
+
{
|
|
60
|
+
echo "Running mx-guard check before push..."
|
|
61
|
+
echo ""
|
|
62
|
+
} >&2
|
|
63
|
+
|
|
64
|
+
mx-guard check >&2
|
|
65
|
+
GUARD_EXIT=$?
|
|
66
|
+
|
|
67
|
+
if [ $GUARD_EXIT -ne 0 ]; then
|
|
68
|
+
{
|
|
69
|
+
echo ""
|
|
70
|
+
echo "mx-guard: push blocked. Fix the errors above before pushing."
|
|
71
|
+
echo " To check specific rules: mx-guard check --rule <category>"
|
|
72
|
+
echo " To see all rules: mx-guard rules"
|
|
73
|
+
} >&2
|
|
74
|
+
exit 2
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
exit 0
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# named-memory-write-guard — PreToolUse(Bash) guard against TRUNCATING full-content
|
|
3
|
+
# writes to NAMED MemNexus memories.
|
|
4
|
+
#
|
|
5
|
+
# Contract: reads the tool-call JSON on stdin, extracts .tool_input.command.
|
|
6
|
+
# exit 0 = allow, exit 2 = block (non-zero exit blocks the Bash command).
|
|
7
|
+
# Fails OPEN (exit 0) on any parse ambiguity, missing tool, fetch failure, or
|
|
8
|
+
# timeout — it never wedges legitimate work.
|
|
9
|
+
#
|
|
10
|
+
# OUTPUT ROUTING (KI-HOOKS-STDERR-SYSTEMIC, 2026-07-04): the harness surfaces a
|
|
11
|
+
# blocking hook's STDERR, not its stdout, on exit 2. Every block message goes to
|
|
12
|
+
# stderr (>&2). The allow paths stay silent.
|
|
13
|
+
#
|
|
14
|
+
# WHY: two real incidents where an agent's full-content replacement silently
|
|
15
|
+
# destroyed most of a shared NAMED memory —
|
|
16
|
+
# - 2026-07-07: memnexus-<team>-known-issues dropped ~60% of its content.
|
|
17
|
+
# - 2026-07-09: memnexus-cross-team-escalations went 49,995 → 3,280 chars.
|
|
18
|
+
# `mx memories update --name X --content <NEW>` REPLACES the whole document. When
|
|
19
|
+
# <NEW> is much smaller than the current head, the write is almost always an
|
|
20
|
+
# accidental clobber of a shared doc that should have been fetch-then-edited.
|
|
21
|
+
# This guard compares the new content length against the current head and blocks
|
|
22
|
+
# a shrink past a threshold unless the caller opts in.
|
|
23
|
+
#
|
|
24
|
+
# SCOPE — this is a length heuristic, not a content-integrity check. Truncations
|
|
25
|
+
# that keep >=60% of characters, and same-length-or-longer replacements that
|
|
26
|
+
# discard prior entries, are NOT caught. Server-side append/CAS (#3697) is the
|
|
27
|
+
# durable fix. This is a guardrail, not a security boundary. It measures the
|
|
28
|
+
# INLINE literal content on the command line. When the content is assembled via
|
|
29
|
+
# shell expansion (`$(...)`, `${VAR}`, `$VAR`, backticks) its real length is
|
|
30
|
+
# unknown at hook time, so the guard FAILS OPEN for that command (never a false
|
|
31
|
+
# block).
|
|
32
|
+
# Indirection via a script file, variable/eval assembly, or the MCP write path
|
|
33
|
+
# (which is not a Bash tool call) is out of scope and NOT defended here.
|
|
34
|
+
#
|
|
35
|
+
# ESCAPE HATCH: set MX_SHRINK_OK=1 to allow an intentional rotation/archival that
|
|
36
|
+
# genuinely shrinks a named memory (e.g. moving resolved threads to an archive).
|
|
37
|
+
#
|
|
38
|
+
# Precedent / conventions mirrored: git-mutation-guard.sh (stdin JSON parse,
|
|
39
|
+
# refuse()->stderr+exit2, fail-open), iteration-log-check.sh (best-effort
|
|
40
|
+
# emit_telemetry with the MX_HOOK_TELEMETRY kill-switch and mx-on-PATH guard).
|
|
41
|
+
|
|
42
|
+
set -euo pipefail
|
|
43
|
+
|
|
44
|
+
# ── Config ───────────────────────────────────────────────────────────────────
|
|
45
|
+
# SHRINK_THRESHOLD: allow the write only when new_length is at least this percent
|
|
46
|
+
# of the current head length. Below it (a shrink of more than 100-THRESHOLD%) the
|
|
47
|
+
# write is blocked unless MX_SHRINK_OK=1. Chosen at 60 so both real incidents
|
|
48
|
+
# (2026-07-07 ~60% drop, 2026-07-09 ~93% drop) are caught while normal edits —
|
|
49
|
+
# which rarely remove >40% at once — pass untouched.
|
|
50
|
+
SHRINK_THRESHOLD=60
|
|
51
|
+
# GET_TIMEOUT: seconds to wait for the head fetch. A slow API must never stall a
|
|
52
|
+
# Bash tool call; on timeout we fail open.
|
|
53
|
+
GET_TIMEOUT=10
|
|
54
|
+
|
|
55
|
+
# ── Recursion guard ──────────────────────────────────────────────────────────
|
|
56
|
+
# The hook itself shells out to `mx memories get` (and, best-effort, `mx memories
|
|
57
|
+
# create` for telemetry). Those are subprocesses, not new tool calls, so they do
|
|
58
|
+
# not re-enter this hook. We still gate on MX_WRITE_GUARD_ACTIVE defensively in
|
|
59
|
+
# case of any nested invocation, and we export it around our own mx calls.
|
|
60
|
+
if [[ "${MX_WRITE_GUARD_ACTIVE:-}" == "1" ]]; then
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
INPUT=$(cat)
|
|
65
|
+
COMMAND=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // .command // empty' 2>/dev/null || true)
|
|
66
|
+
|
|
67
|
+
# Can't parse a command — fail open.
|
|
68
|
+
if [[ -z "$COMMAND" ]]; then
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# ── Cheap non-matching fast path (no python, no network) ─────────────────────
|
|
73
|
+
# This hook runs on EVERY Bash tool call. Reject the overwhelming majority of
|
|
74
|
+
# commands with a few substring greps before doing any structural parse or
|
|
75
|
+
# network fetch. A candidate must mention `memories`, an `update` or `create`
|
|
76
|
+
# subcommand, AND carry both `--name` and `--content` (any order, `=` forms
|
|
77
|
+
# included). Anything missing one of these exits 0 immediately and silently.
|
|
78
|
+
if ! printf '%s' "$COMMAND" | grep -q -- 'memories'; then exit 0; fi
|
|
79
|
+
if ! printf '%s' "$COMMAND" | grep -qE -- '(update|create)'; then exit 0; fi
|
|
80
|
+
if ! printf '%s' "$COMMAND" | grep -q -- '--name'; then exit 0; fi
|
|
81
|
+
if ! printf '%s' "$COMMAND" | grep -q -- '--content'; then exit 0; fi
|
|
82
|
+
|
|
83
|
+
# Structural parse needs python3 (shlex tokenizes respecting quotes WITHOUT
|
|
84
|
+
# executing anything). If it is absent, fail open — a guardrail must not block
|
|
85
|
+
# work just because a helper is missing.
|
|
86
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
87
|
+
exit 0
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# ── Structural match + inline-content length via shlex ───────────────────────
|
|
91
|
+
# PARSE is one of:
|
|
92
|
+
# "MATCH\t<name>\t<new_len>" — a real `mx memories update|create` with both
|
|
93
|
+
# --name and inline literal --content.
|
|
94
|
+
# "SKIP" — matched shape but content is shell-expanded
|
|
95
|
+
# (length unknown) or quotes are unbalanced;
|
|
96
|
+
# fail open.
|
|
97
|
+
# "NOMATCH" — the greps tripped but there is no real
|
|
98
|
+
# update|create --name --content invocation.
|
|
99
|
+
PY_PARSE='
|
|
100
|
+
import sys, re, shlex
|
|
101
|
+
data = sys.stdin.read()
|
|
102
|
+
try:
|
|
103
|
+
toks = shlex.split(data, posix=True)
|
|
104
|
+
except ValueError:
|
|
105
|
+
print("SKIP"); sys.exit(0)
|
|
106
|
+
name = None; content = None; found = False
|
|
107
|
+
seps = {";", "|", "||", "&&", "&", "(", ")", "{", "}"}
|
|
108
|
+
i, n = 0, len(toks)
|
|
109
|
+
while i < n:
|
|
110
|
+
t = toks[i]
|
|
111
|
+
if (t == "mx" or t.endswith("/mx")) and i + 2 < n and toks[i+1] == "memories" and toks[i+2] in ("update", "create"):
|
|
112
|
+
found = True
|
|
113
|
+
j = i + 3
|
|
114
|
+
while j < n:
|
|
115
|
+
a = toks[j]
|
|
116
|
+
if a in seps or a == "mx":
|
|
117
|
+
break
|
|
118
|
+
if a == "--name" and j + 1 < n:
|
|
119
|
+
name = toks[j+1]; j += 2; continue
|
|
120
|
+
if a.startswith("--name="):
|
|
121
|
+
name = a[len("--name="):]; j += 1; continue
|
|
122
|
+
if a == "--content" and j + 1 < n:
|
|
123
|
+
content = toks[j+1]; j += 2; continue
|
|
124
|
+
if a.startswith("--content="):
|
|
125
|
+
content = a[len("--content="):]; j += 1; continue
|
|
126
|
+
j += 1
|
|
127
|
+
break
|
|
128
|
+
i += 1
|
|
129
|
+
if not found or not name or content is None:
|
|
130
|
+
print("NOMATCH"); sys.exit(0)
|
|
131
|
+
# Content assembled via shell expansion -> real length unknown at hook time.
|
|
132
|
+
# Fail open rather than risk a false block. Covers $(...), ${VAR}, $VAR, `...`.
|
|
133
|
+
if re.search(r"\$[A-Za-z_{(]", content) or "`" in content:
|
|
134
|
+
print("SKIP"); sys.exit(0)
|
|
135
|
+
print("MATCH\t%s\t%d" % (name, len(content)))
|
|
136
|
+
'
|
|
137
|
+
PARSE=$(printf '%s' "$COMMAND" | MX_WRITE_GUARD_ACTIVE=1 python3 -c "$PY_PARSE" 2>/dev/null || true)
|
|
138
|
+
|
|
139
|
+
# ── Telemetry (best-effort, non-blocking) ────────────────────────────────────
|
|
140
|
+
# Mirrors iteration-log-check.sh emit_telemetry: one `hook-fired:` memory per
|
|
141
|
+
# decision, time-boxed, honours the MX_HOOK_TELEMETRY=0 kill-switch, no-ops when
|
|
142
|
+
# mx is absent. The create carries NO --name, so it cannot re-trigger this guard
|
|
143
|
+
# (the fast path requires both --name and --content); we also set
|
|
144
|
+
# MX_WRITE_GUARD_ACTIVE=1 around it. Any failure here is swallowed.
|
|
145
|
+
emit_telemetry() {
|
|
146
|
+
if [[ "${MX_HOOK_TELEMETRY:-1}" == "0" ]]; then return 0; fi
|
|
147
|
+
command -v mx >/dev/null 2>&1 || return 0
|
|
148
|
+
local mem="$1" old_len="$2" new_len="$3" blocked="$4" override="$5"
|
|
149
|
+
local team ts
|
|
150
|
+
team="${CLAUDE_TEAM_NAME:-}"
|
|
151
|
+
if [[ -z "$team" && -n "${CLAUDE_WORKTREE_NAME:-}" ]]; then
|
|
152
|
+
team=$(printf '%s' "$CLAUDE_WORKTREE_NAME" | sed 's/-[0-9]\{8\}$//')
|
|
153
|
+
fi
|
|
154
|
+
team="${team//[^a-zA-Z0-9-]/}"
|
|
155
|
+
team="${team:-unknown}"
|
|
156
|
+
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "")
|
|
157
|
+
MX_WRITE_GUARD_ACTIVE=1 timeout 5 mx memories create \
|
|
158
|
+
--content "hook-fired: named-memory-write-guard memory=${mem} old_len=${old_len} new_len=${new_len} blocked=${blocked} override=${override} team=${team} ts=${ts}" \
|
|
159
|
+
--topics "hook-telemetry,platform" \
|
|
160
|
+
--code-context '{"product":"memnexus","team":"platform","role":"hook-telemetry"}' \
|
|
161
|
+
>/dev/null 2>&1 || true
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# NOMATCH / SKIP / empty → allow. SKIP is the shell-expansion fail-open case; log
|
|
165
|
+
# it so the fail-open rate is visible, but do not block.
|
|
166
|
+
if [[ "$PARSE" != MATCH* ]]; then
|
|
167
|
+
if [[ "$PARSE" == "SKIP" ]]; then
|
|
168
|
+
emit_telemetry "unknown" "unknown" "unknown" "no" "skip-expansion"
|
|
169
|
+
fi
|
|
170
|
+
exit 0
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
NAME=$(printf '%s' "$PARSE" | cut -f2)
|
|
174
|
+
NEW_LEN=$(printf '%s' "$PARSE" | cut -f3)
|
|
175
|
+
|
|
176
|
+
# ── Fetch current head ───────────────────────────────────────────────────────
|
|
177
|
+
if ! command -v mx >/dev/null 2>&1; then
|
|
178
|
+
exit 0
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
HEAD_JSON=$(MX_WRITE_GUARD_ACTIVE=1 timeout "$GET_TIMEOUT" mx memories get --name "$NAME" --format json 2>/dev/null || true)
|
|
182
|
+
|
|
183
|
+
# Parse tolerantly: the CLI may print status lines before the JSON body, so read
|
|
184
|
+
# from the first line containing `{` onward, then take .content length.
|
|
185
|
+
HEAD_LEN=$(printf '%s' "$HEAD_JSON" | sed -n '/{/,$p' | jq -r '(.content // "") | length' 2>/dev/null || true)
|
|
186
|
+
|
|
187
|
+
# Fetch failed, memory not found, timeout, or empty/zero head → FAIL OPEN, but
|
|
188
|
+
# log the event so we can see how often the head is unreadable.
|
|
189
|
+
if [[ -z "$HEAD_LEN" || ! "$HEAD_LEN" =~ ^[0-9]+$ || "$HEAD_LEN" -eq 0 ]]; then
|
|
190
|
+
emit_telemetry "$NAME" "fetch-fail" "$NEW_LEN" "no" "fetch-fail"
|
|
191
|
+
exit 0
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# ── Threshold decision (integer math) ────────────────────────────────────────
|
|
195
|
+
# Allow when new_len >= SHRINK_THRESHOLD% of head_len, i.e.
|
|
196
|
+
# new_len * 100 >= SHRINK_THRESHOLD * head_len
|
|
197
|
+
if (( NEW_LEN * 100 >= SHRINK_THRESHOLD * HEAD_LEN )); then
|
|
198
|
+
emit_telemetry "$NAME" "$HEAD_LEN" "$NEW_LEN" "no" "no"
|
|
199
|
+
exit 0
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
# Below threshold: a truncating write. Honour the explicit opt-out.
|
|
203
|
+
if [[ "${MX_SHRINK_OK:-}" == "1" ]]; then
|
|
204
|
+
emit_telemetry "$NAME" "$HEAD_LEN" "$NEW_LEN" "no" "yes"
|
|
205
|
+
exit 0
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# ── Block ────────────────────────────────────────────────────────────────────
|
|
209
|
+
DROP_PCT=$(( (HEAD_LEN - NEW_LEN) * 100 / HEAD_LEN ))
|
|
210
|
+
emit_telemetry "$NAME" "$HEAD_LEN" "$NEW_LEN" "yes" "no"
|
|
211
|
+
{
|
|
212
|
+
echo ""
|
|
213
|
+
echo "BLOCKED: truncating write to named memory \"${NAME}\""
|
|
214
|
+
echo "This is a local guardrail (named-memory-write-guard hook), not a MemNexus"
|
|
215
|
+
echo "service error — do not file a P0."
|
|
216
|
+
echo " head ${HEAD_LEN} chars → new ${NEW_LEN} chars (-${DROP_PCT}%)"
|
|
217
|
+
echo ""
|
|
218
|
+
echo "A full --content replacement this much smaller than the current head is"
|
|
219
|
+
echo "almost always an accidental clobber of a shared named memory (two real"
|
|
220
|
+
echo "incidents: 2026-07-07 known-issues ~60% loss, 2026-07-09 escalations"
|
|
221
|
+
echo "49,995 → 3,280 chars)."
|
|
222
|
+
echo ""
|
|
223
|
+
echo "Known sanctioned case: composer archival/rotation passes on"
|
|
224
|
+
echo "memnexus-cross-team-escalations legitimately shrink content — use the"
|
|
225
|
+
echo "inline override for those."
|
|
226
|
+
echo ""
|
|
227
|
+
echo "If this is an intentional rotation/archival, override INLINE for this one"
|
|
228
|
+
echo "command only (do NOT export MX_SHRINK_OK — that disables the guard for the"
|
|
229
|
+
echo "rest of the session). Before overriding, re-fetch the head"
|
|
230
|
+
echo "(mx memories get --name \"${NAME}\") and confirm the shrink is intentional:"
|
|
231
|
+
echo " MX_SHRINK_OK=1 mx memories update --name \"${NAME}\" --content ..."
|
|
232
|
+
echo ""
|
|
233
|
+
echo "Otherwise fetch the head first and edit it, preserving existing content:"
|
|
234
|
+
echo " mx memories get --name \"${NAME}\" # then append/edit, don't replace"
|
|
235
|
+
echo ""
|
|
236
|
+
} >&2
|
|
237
|
+
exit 2
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# reload-checkpoint.sh — Claude Code SessionStart hook (matcher: compact)
|
|
3
|
+
# Re-injects the auto-checkpoint memory after context compaction.
|
|
4
|
+
#
|
|
5
|
+
# Fires on: SessionStart with source=compact (after compaction)
|
|
6
|
+
# Input: JSON on stdin with session_id, source
|
|
7
|
+
# Output: stdout text is injected into Claude's context
|
|
8
|
+
# Exit: always 0
|
|
9
|
+
|
|
10
|
+
# Fail-open: any error → start session without checkpoint injection
|
|
11
|
+
trap 'exit 0' ERR
|
|
12
|
+
|
|
13
|
+
# ── 1. Determine worktree name ──────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
WORKTREE_NAME="${CLAUDE_WORKTREE_NAME:-}"
|
|
16
|
+
|
|
17
|
+
# If no worktree env var, try to detect from cwd
|
|
18
|
+
if [[ -z "$WORKTREE_NAME" ]]; then
|
|
19
|
+
INPUT=$(cat)
|
|
20
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // ""' 2>/dev/null) || true
|
|
21
|
+
if [[ "$CWD" == */.worktrees/* ]]; then
|
|
22
|
+
WORKTREE_NAME=$(echo "$CWD" | sed 's|.*/.worktrees/||' | cut -d'/' -f1)
|
|
23
|
+
fi
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Can't identify worktree → nothing to reload
|
|
27
|
+
if [[ -z "$WORKTREE_NAME" ]]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
command -v mx >/dev/null 2>&1 || exit 0
|
|
32
|
+
|
|
33
|
+
# ── 2. Retrieve checkpoint memory ───────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
CHECKPOINT=$(mx memories get --name "auto-checkpoint-${WORKTREE_NAME}" --format json --detail minimal 2>/dev/null)
|
|
36
|
+
|
|
37
|
+
if [[ -z "$CHECKPOINT" || "$CHECKPOINT" == *"not found"* || "$CHECKPOINT" == *"404"* ]]; then
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Extract just the content field from the JSON response
|
|
42
|
+
CONTENT=$(echo "$CHECKPOINT" | jq -r '.content // empty' 2>/dev/null)
|
|
43
|
+
|
|
44
|
+
if [[ -z "$CONTENT" ]]; then
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# ── 3. Output checkpoint for context injection ──────────────────────────
|
|
49
|
+
# Text written to stdout by SessionStart hooks is added to Claude's context.
|
|
50
|
+
|
|
51
|
+
cat <<EOF
|
|
52
|
+
## Auto-Checkpoint Recovery
|
|
53
|
+
|
|
54
|
+
The following checkpoint was automatically saved before the last context compaction.
|
|
55
|
+
Use it to resume your work. The checkpoint captures what you were working on,
|
|
56
|
+
which files were involved, and the recent conversation context.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
${CONTENT}
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
**Action:** Review this checkpoint and continue where you left off.
|
|
65
|
+
If you need more context, search memories with: mx memories search --query "<topic>" --recent 24h
|
|
66
|
+
EOF
|
|
67
|
+
|
|
68
|
+
exit 0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# set-terminal-appearance.sh — Claude Code SessionStart hook
|
|
3
|
+
# Sets the VS Code terminal tab title and color for the agent team.
|
|
4
|
+
#
|
|
5
|
+
# Fires on: SessionStart (all — no matcher filter)
|
|
6
|
+
# Input: JSON on stdin (ignored)
|
|
7
|
+
# Output: none (side-effect: writes OSC sequences to /dev/tty)
|
|
8
|
+
# Exit: always 0
|
|
9
|
+
#
|
|
10
|
+
# Reads CLAUDE_TEAM_NAME and CLAUDE_TEAM_COLOR from environment
|
|
11
|
+
# (set by mx-agent start via launchClaudeSession).
|
|
12
|
+
# Writes escape sequences to /dev/tty to bypass Claude's stdin/stdout,
|
|
13
|
+
# ensuring the tab title/color persist after Claude's TUI initializes.
|
|
14
|
+
|
|
15
|
+
TEAM_NAME="${CLAUDE_TEAM_NAME:-}"
|
|
16
|
+
TEAM_COLOR="${CLAUDE_TEAM_COLOR:-}"
|
|
17
|
+
|
|
18
|
+
# Nothing to set — not launched via mx-agent
|
|
19
|
+
if [[ -z "$TEAM_NAME" ]]; then
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# Drain stdin (hook contract requires reading input)
|
|
24
|
+
cat > /dev/null 2>&1 || true
|
|
25
|
+
|
|
26
|
+
# OSC 0: Set terminal tab title
|
|
27
|
+
printf '\033]0;%s\007' "$TEAM_NAME" > /dev/tty 2>/dev/null || true
|
|
28
|
+
|
|
29
|
+
# OSC 633;P;tabColor: Set VS Code tab color (VS Code 1.94+ shell integration)
|
|
30
|
+
if [[ -n "$TEAM_COLOR" ]]; then
|
|
31
|
+
printf '\033]633;P;tabColor=%s\007' "$TEAM_COLOR" > /dev/tty 2>/dev/null || true
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
exit 0
|