@memnexus-ai/mx-agent-cli 0.1.166 → 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.
Files changed (96) hide show
  1. package/dist/__tests__/claude-sync.test.js +15 -0
  2. package/dist/__tests__/claude-sync.test.js.map +1 -1
  3. package/dist/__tests__/config-generation.test.d.ts +7 -0
  4. package/dist/__tests__/config-generation.test.d.ts.map +1 -0
  5. package/dist/__tests__/config-generation.test.js +141 -0
  6. package/dist/__tests__/config-generation.test.js.map +1 -0
  7. package/dist/__tests__/config-sync.test.d.ts +10 -0
  8. package/dist/__tests__/config-sync.test.d.ts.map +1 -0
  9. package/dist/__tests__/config-sync.test.js +180 -0
  10. package/dist/__tests__/config-sync.test.js.map +1 -0
  11. package/dist/__tests__/project-config.test.js +1 -0
  12. package/dist/__tests__/project-config.test.js.map +1 -1
  13. package/dist/agent-config/.mx-config-generation.json +4 -0
  14. package/dist/agent-config/CLAUDE.md.template +140 -0
  15. package/dist/agent-config/OWNERS.md +51 -0
  16. package/dist/agent-config/agents/bar-raiser.md +205 -0
  17. package/dist/agent-config/agents/dogfood-auditor.md +243 -0
  18. package/dist/agent-config/agents/git-expert.md +380 -0
  19. package/dist/agent-config/agents/implementation-engineer.md +387 -0
  20. package/dist/agent-config/agents/memory-creator.md +330 -0
  21. package/dist/agent-config/agents/prd-writer.md +478 -0
  22. package/dist/agent-config/agents/prfaq-writer.md +344 -0
  23. package/dist/agent-config/agents/prior-art-researcher.md +264 -0
  24. package/dist/agent-config/agents/qa-tester.md +406 -0
  25. package/dist/agent-config/agents/red-team.md +330 -0
  26. package/dist/agent-config/agents/security-reviewer.md +300 -0
  27. package/dist/agent-config/agents/status-reporter.md +297 -0
  28. package/dist/agent-config/claude-md/CLAUDE.core.md +105 -0
  29. package/dist/agent-config/claude-md/CLAUDE.overlay.memnexus.md +68 -0
  30. package/dist/agent-config/claude-md/compose.mjs +179 -0
  31. package/dist/agent-config/claude-md/vars.memnexus.json +4 -0
  32. package/dist/agent-config/hooks/auto-capture-precompact.sh +176 -0
  33. package/dist/agent-config/hooks/auto-capture-session-end.sh +178 -0
  34. package/dist/agent-config/hooks/auto-checkpoint.sh +102 -0
  35. package/dist/agent-config/hooks/ci-validate-hook.sh +499 -0
  36. package/dist/agent-config/hooks/ci-validate-routes.conf +16 -0
  37. package/dist/agent-config/hooks/delegation-audit.sh +61 -0
  38. package/dist/agent-config/hooks/deploy-verification-reminder.sh +179 -0
  39. package/dist/agent-config/hooks/git-mutation-guard.sh +219 -0
  40. package/dist/agent-config/hooks/iteration-log-check.sh +212 -0
  41. package/dist/agent-config/hooks/memory-checkpoint-reminder.sh +103 -0
  42. package/dist/agent-config/hooks/mx-guard-hook.sh +77 -0
  43. package/dist/agent-config/hooks/named-memory-write-guard.sh +237 -0
  44. package/dist/agent-config/hooks/reload-checkpoint.sh +68 -0
  45. package/dist/agent-config/hooks/set-terminal-appearance.sh +34 -0
  46. package/dist/agent-config/hooks/workflow-dispatch-guard.sh +184 -0
  47. package/dist/agent-config/hooks/worktree-guard.sh +258 -0
  48. package/dist/agent-config/rules/eval-harness.md +33 -0
  49. package/dist/agent-config/rules/frontend.md +17 -0
  50. package/dist/agent-config/rules/mcp-protocol.md +15 -0
  51. package/dist/agent-config/rules/retrieval.md +17 -0
  52. package/dist/agent-config/settings.json +196 -0
  53. package/dist/agent-config/shared/eval-tiered-rigor.md +15 -0
  54. package/dist/agent-config/shared/team-operating-rules.md +37 -0
  55. package/dist/agent-config/skills/eval-claim-impact/SKILL.md +20 -0
  56. package/dist/agent-config/skills/eval-harness-review/SKILL.md +21 -0
  57. package/dist/agent-config/skills/mx-checkpoint/SKILL.md +111 -0
  58. package/dist/agent-config/skills/mx-fix-pr/SKILL.md +90 -0
  59. package/dist/agent-config/skills/mx-gap-assessment/SKILL.md +90 -0
  60. package/dist/agent-config/skills/mx-iteration-close/SKILL.md +81 -0
  61. package/dist/agent-config/skills/mx-post-merge-sync/SKILL.md +83 -0
  62. package/dist/agent-config/skills/mx-pre-pr-check/SKILL.md +85 -0
  63. package/dist/agent-config/skills/mx-save/SKILL.md +102 -0
  64. package/dist/agent-config/skills/mx-session-start/SKILL.md +85 -0
  65. package/dist/agent-config/skills/team-gate-checklist/SKILL.md +19 -0
  66. package/dist/commands/config-sync.d.ts +39 -0
  67. package/dist/commands/config-sync.d.ts.map +1 -0
  68. package/dist/commands/config-sync.js +212 -0
  69. package/dist/commands/config-sync.js.map +1 -0
  70. package/dist/commands/create.d.ts.map +1 -1
  71. package/dist/commands/create.js +3 -0
  72. package/dist/commands/create.js.map +1 -1
  73. package/dist/commands/init.d.ts.map +1 -1
  74. package/dist/commands/init.js +12 -4
  75. package/dist/commands/init.js.map +1 -1
  76. package/dist/commands/start.d.ts.map +1 -1
  77. package/dist/commands/start.js +6 -0
  78. package/dist/commands/start.js.map +1 -1
  79. package/dist/index.js +23 -0
  80. package/dist/index.js.map +1 -1
  81. package/dist/lib/bundled-config.d.ts +44 -0
  82. package/dist/lib/bundled-config.d.ts.map +1 -0
  83. package/dist/lib/bundled-config.js +164 -0
  84. package/dist/lib/bundled-config.js.map +1 -0
  85. package/dist/lib/claude.d.ts.map +1 -1
  86. package/dist/lib/claude.js +9 -3
  87. package/dist/lib/claude.js.map +1 -1
  88. package/dist/lib/config-generation.d.ts +36 -0
  89. package/dist/lib/config-generation.d.ts.map +1 -0
  90. package/dist/lib/config-generation.js +73 -0
  91. package/dist/lib/config-generation.js.map +1 -0
  92. package/dist/lib/project-config.d.ts +7 -0
  93. package/dist/lib/project-config.d.ts.map +1 -1
  94. package/dist/lib/project-config.js +4 -0
  95. package/dist/lib/project-config.js.map +1 -1
  96. 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