@rubytech/create-maxy 1.0.800 → 1.0.801

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.800",
3
+ "version": "1.0.801",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env bash
2
+ # Regression test for archive-ingest-surface-gate.sh (Task 855).
3
+ #
4
+ # Covers:
5
+ # Preserved-from-Task-846:
6
+ # 1. Edit on /platform/plugins/<x>/lib/* → BLOCKED
7
+ # 2. Edit on benign path → ALLOWED
8
+ # 3. Bash with `npx vitest`/`bun test`/`npm test` → BLOCKED
9
+ # 4. PostToolUse on whatsapp-export-parse with isError:true sets flag
10
+ # 5. Subsequent PreToolUse on ANY tool → BLOCKED
11
+ # 6. UserPromptSubmit clears flag → normal allow resumes
12
+ # 7. PostToolUse with isError:false → flag absent
13
+ # 8. Stale flag (>600s) auto-clears
14
+ # New (Task 855):
15
+ # A. PreToolUse mcp__memory__whatsapp-export-parse → BLOCKED
16
+ # B. PreToolUse mcp__memory__whatsapp-export-insight-write → BLOCKED
17
+ # C. PreToolUse mcp__memory__memory-archive-write w/ archiveType=whatsapp-export → BLOCKED
18
+ # D. PreToolUse mcp__memory__memory-archive-write w/ archiveType=linkedin-connections → ALLOWED
19
+ # E. PreToolUse Bash invoking whatsapp-ingest.sh → ALLOWED
20
+ # F. Default-allow emits a [archive-ingest-gate] decision=allow log line
21
+
22
+ set -u
23
+
24
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/archive-ingest-surface-gate.sh"
25
+ if [[ ! -x "$HOOK" ]]; then
26
+ echo "FAIL: $HOOK not executable" >&2
27
+ exit 1
28
+ fi
29
+
30
+ STATE_DIR=$(mktemp -d)
31
+ export ARCHIVE_INGEST_GATE_STATE_DIR="$STATE_DIR"
32
+ FLAG_FILE="$STATE_DIR/archive-ingest-parse-error.flag"
33
+
34
+ cleanup() { rm -rf "$STATE_DIR"; }
35
+ trap cleanup EXIT
36
+
37
+ PASS=0
38
+ FAIL=0
39
+
40
+ run_case() {
41
+ local name="$1" stdin="$2" expected_exit="$3"
42
+ local actual_exit
43
+ printf '%s' "$stdin" | bash "$HOOK" >/dev/null 2>/dev/null
44
+ actual_exit=$?
45
+ if [[ "$actual_exit" -eq "$expected_exit" ]]; then
46
+ echo "PASS: $name (exit=$actual_exit)"
47
+ PASS=$((PASS + 1))
48
+ else
49
+ echo "FAIL: $name (expected exit=$expected_exit, got=$actual_exit)" >&2
50
+ FAIL=$((FAIL + 1))
51
+ fi
52
+ }
53
+
54
+ # Preserved cases ---------------------------------------------------------
55
+
56
+ run_case "Edit on platform/plugins/whatsapp-import/lib/src/parse-export.ts → BLOCKED" \
57
+ '{"hook_event_name":"PreToolUse","tool_name":"Edit","tool_input":{"file_path":"/Users/x/repo/platform/plugins/whatsapp-import/lib/src/parse-export.ts","old_string":"a","new_string":"b"}}' \
58
+ 2
59
+
60
+ run_case "Edit on README.md → ALLOWED" \
61
+ '{"hook_event_name":"PreToolUse","tool_name":"Edit","tool_input":{"file_path":"/Users/x/repo/README.md","old_string":"a","new_string":"b"}}' \
62
+ 0
63
+
64
+ run_case "Bash 'npx vitest run' → BLOCKED" \
65
+ '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npx vitest run parse-export.test.ts"}}' \
66
+ 2
67
+
68
+ run_case "Bash 'ls -la' → ALLOWED" \
69
+ '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"ls -la"}}' \
70
+ 0
71
+
72
+ run_case "Bash 'bun test' → BLOCKED" \
73
+ '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"bun test"}}' \
74
+ 2
75
+
76
+ run_case "Bash 'npm test' → BLOCKED" \
77
+ '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm test"}}' \
78
+ 2
79
+
80
+ # New (Task 855) cases ----------------------------------------------------
81
+
82
+ run_case "PreToolUse mcp__memory__whatsapp-export-parse → BLOCKED" \
83
+ '{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__whatsapp-export-parse","tool_input":{"filePath":"/tmp/_chat.txt","accountId":"acct1","timezone":"Europe/London"}}' \
84
+ 2
85
+
86
+ run_case "PreToolUse mcp__memory__whatsapp-export-insight-write → BLOCKED" \
87
+ '{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__whatsapp-export-insight-write","tool_input":{"kind":"MENTIONS","name":"Joel"}}' \
88
+ 2
89
+
90
+ run_case "PreToolUse memory-archive-write w/ archiveType=whatsapp-export → BLOCKED" \
91
+ '{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__memory-archive-write","tool_input":{"archiveType":"whatsapp-export","ownerNodeId":"x","accountId":"a","rows":[]}}' \
92
+ 2
93
+
94
+ run_case "PreToolUse memory-archive-write w/ archiveType=linkedin-connections → ALLOWED" \
95
+ '{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__memory-archive-write","tool_input":{"archiveType":"linkedin-connections","ownerNodeId":"x","accountId":"a","rows":[]}}' \
96
+ 0
97
+
98
+ # Bypass attempts (Task 855 code-review C1): nested archiveType in rows[0]
99
+ # or conversation must NOT defeat the block. The gate must read the
100
+ # top-level tool_input.archiveType, not the first textual occurrence.
101
+ run_case "BYPASS: nested rows[0].archiveType=linkedin + top-level archiveType=whatsapp-export → BLOCKED" \
102
+ '{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__memory-archive-write","tool_input":{"rows":[{"archiveType":"linkedin-connections"}],"archiveType":"whatsapp-export","ownerNodeId":"x","accountId":"a"}}' \
103
+ 2
104
+
105
+ run_case "BYPASS: conversation.archiveType=linkedin + top-level archiveType=whatsapp-export → BLOCKED" \
106
+ '{"hook_event_name":"PreToolUse","tool_name":"mcp__memory__memory-archive-write","tool_input":{"conversation":{"archiveType":"linkedin-connections","conversationId":"x"},"archiveType":"whatsapp-export","ownerNodeId":"x","accountId":"a","rows":[]}}' \
107
+ 2
108
+
109
+ # Plugin-source-edit path block must read tool_input.file_path top-level,
110
+ # not a nested file_path in old_string/new_string.
111
+ run_case "BYPASS: nested file_path in old_string + top-level file_path in lib/* → BLOCKED" \
112
+ '{"hook_event_name":"PreToolUse","tool_name":"Edit","tool_input":{"file_path":"/repo/platform/plugins/whatsapp-import/lib/src/parse-export.ts","old_string":"file_path:/safe/path","new_string":"x"}}' \
113
+ 2
114
+
115
+ run_case "PreToolUse Bash invoking whatsapp-ingest.sh → ALLOWED" \
116
+ '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh /tmp/chat.zip --owner-element-id 4:abc:1 --scope admin"}}' \
117
+ 0
118
+
119
+ # Default-allow log-line check
120
+ LOG=$(printf '%s' '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' | bash "$HOOK" 2>&1 1>/dev/null)
121
+ if printf '%s' "$LOG" | grep -q '\[archive-ingest-gate\] decision=allow tool=Read reason=default'; then
122
+ echo "PASS: default-allow emits decision-log line"
123
+ PASS=$((PASS + 1))
124
+ else
125
+ echo "FAIL: default-allow missing decision-log line. Got: $LOG" >&2
126
+ FAIL=$((FAIL + 1))
127
+ fi
128
+
129
+ # Block-log line includes command field for Bash
130
+ LOG=$(printf '%s' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npx vitest run"}}' | bash "$HOOK" 2>&1 1>/dev/null)
131
+ if printf '%s' "$LOG" | grep -q '\[archive-ingest-gate\] decision=block tool=Bash reason=test-runner'; then
132
+ echo "PASS: block emits decision-log line with reason"
133
+ PASS=$((PASS + 1))
134
+ else
135
+ echo "FAIL: block missing decision-log line. Got: $LOG" >&2
136
+ FAIL=$((FAIL + 1))
137
+ fi
138
+
139
+ # Parse-error flag lifecycle (preserved)
140
+ rm -f "$FLAG_FILE"
141
+ run_case "PostToolUse parse-error sets flag (exit 0)" \
142
+ '{"hook_event_name":"PostToolUse","tool_name":"mcp__memory__whatsapp-export-parse","tool_input":{"filePath":"_chat.txt"},"tool_response":{"isError":true,"content":[{"type":"text","text":"parse-error file=_chat.txt line=1 reason=not-a-_chat.txt"}]}}' \
143
+ 0
144
+ [[ -f "$FLAG_FILE" ]] && { echo "PASS: parse-error flag created"; PASS=$((PASS+1)); } \
145
+ || { echo "FAIL: parse-error flag NOT created" >&2; FAIL=$((FAIL+1)); }
146
+
147
+ run_case "PreToolUse Read after parse-error → BLOCKED" \
148
+ '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' \
149
+ 2
150
+
151
+ run_case "UserPromptSubmit clears flag" \
152
+ '{"hook_event_name":"UserPromptSubmit","prompt":"retry"}' \
153
+ 0
154
+ [[ ! -f "$FLAG_FILE" ]] && { echo "PASS: UserPromptSubmit cleared flag"; PASS=$((PASS+1)); } \
155
+ || { echo "FAIL: UserPromptSubmit did NOT clear flag" >&2; FAIL=$((FAIL+1)); }
156
+
157
+ run_case "PreToolUse Read after clearance → ALLOWED" \
158
+ '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' \
159
+ 0
160
+
161
+ # Stale flag auto-clears
162
+ PAST=$(( $(date -u +%s) - 700 ))
163
+ echo "$PAST" > "$FLAG_FILE"
164
+ run_case "Stale flag auto-clears, PreToolUse Read → ALLOWED" \
165
+ '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' \
166
+ 0
167
+
168
+ # PostToolUse parse-success leaves flag absent
169
+ rm -f "$FLAG_FILE"
170
+ run_case "PostToolUse parse-success (isError:false) does NOT set flag" \
171
+ '{"hook_event_name":"PostToolUse","tool_name":"mcp__memory__whatsapp-export-parse","tool_input":{"filePath":"_chat.txt"},"tool_response":{"isError":false,"content":[{"type":"text","text":"{\"parsedLines\":[]}"}]}}' \
172
+ 0
173
+ [[ ! -f "$FLAG_FILE" ]] && { echo "PASS: parse-success leaves flag absent"; PASS=$((PASS+1)); } \
174
+ || { echo "FAIL: parse-success incorrectly created flag" >&2; FAIL=$((FAIL+1)); }
175
+
176
+ # Fail-closed terminal check
177
+ if grep -q '\[ -t 0 \]' "$HOOK"; then
178
+ echo "PASS: fail-closed terminal check is present"
179
+ PASS=$((PASS + 1))
180
+ else
181
+ echo "FAIL: fail-closed terminal check missing" >&2
182
+ FAIL=$((FAIL + 1))
183
+ fi
184
+
185
+ echo
186
+ echo "──────── archive-ingest-surface-gate test summary ────────"
187
+ echo "PASS: $PASS"
188
+ echo "FAIL: $FAIL"
189
+
190
+ [[ "$FAIL" -gt 0 ]] && exit 1
191
+ exit 0
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env bash
2
+ # Archive-ingest surface gate (Task 855; supersedes Task 846).
3
+ #
4
+ # Five enforcements, one script — phase decided by `hook_event_name` on stdin.
5
+ # Task 855 narrows the database-operator subagent's effective surface during
6
+ # WhatsApp archive ingestion to exactly one Bash entry
7
+ # (`whatsapp-import/bin/whatsapp-ingest.sh`) plus read-only neighbours, by
8
+ # blocking the legacy MCP deviation tools mechanically.
9
+ #
10
+ # 1. PreToolUse on the three legacy WhatsApp MCP tools — BLOCK unconditionally.
11
+ # The single deterministic Bash entry (Task 855) is the only supported path
12
+ # for `archiveType=whatsapp-export`. The legacy MCP tools' source remains
13
+ # until cleanup; the gate stops them being invoked.
14
+ # mcp__memory__whatsapp-export-parse
15
+ # mcp__memory__whatsapp-export-insight-write
16
+ # mcp__memory__memory-archive-write (only when `archiveType` is
17
+ # `whatsapp-export`; LinkedIn
18
+ # and other archiveTypes pass
19
+ # through unchanged.)
20
+ #
21
+ # 2. PreToolUse Edit/Write/NotebookEdit: deny writes under
22
+ # `*platform/plugins/*/lib/*` (parser/CSV-shape source for any *-import or
23
+ # *-export plugin). Preserved from Task 846 — defence in depth even though
24
+ # the database-operator template no longer lists Edit/Write tools.
25
+ #
26
+ # 3. PreToolUse Bash: deny commands invoking JavaScript test runners
27
+ # (vitest|bun test|npm test|npx jest|node .*vitest). Preserved from Task 846.
28
+ #
29
+ # 4. Parse-error gate: PostToolUse on any `mcp__*__*-export-parse` /
30
+ # `mcp__*__*-import-parse` tool whose `tool_response.isError == true`
31
+ # writes a flag file. Subsequent PreToolUse on ANY tool blocks until
32
+ # UserPromptSubmit clears the flag. A 600s TTL is the cross-session safety
33
+ # net. Preserved from Task 846 because LinkedIn and future per-source archive
34
+ # parsers still use the legacy MCP path until they migrate to their own
35
+ # deterministic Bash entries.
36
+ #
37
+ # 5. Logging: every PreToolUse decision emits one line in the format
38
+ # [archive-ingest-gate] decision=<allow|block> tool=<name> reason=<r> ...
39
+ # so the operator can grep the full decision trail for one ingest from
40
+ # server.log alongside the [whatsapp-ingest] script lines.
41
+ #
42
+ # Exit codes follow Claude Code hook protocol: 0 = allow, 2 = block (stderr
43
+ # message shown to the agent). Fail-closed on terminal stdin to match
44
+ # pre-tool-use.sh.
45
+
46
+ set -uo pipefail
47
+
48
+ # Read stdin — fail closed if attached to a terminal (no JSON envelope coming).
49
+ if [ -t 0 ]; then
50
+ echo "Blocked: archive-ingest-surface-gate received no stdin (cannot inspect tool call). Failing closed." >&2
51
+ exit 2
52
+ fi
53
+ INPUT=$(cat)
54
+
55
+ # ----- Resolve account dir for state file ----------------------------------
56
+ HOOK_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
57
+ PLATFORM_ROOT_RESOLVED="${HOOK_DIR}/../../.."
58
+ ACCOUNTS_DIR="${PLATFORM_ROOT_RESOLVED}/../data/accounts"
59
+ ACCOUNT_DIR=""
60
+ if [ -d "$ACCOUNTS_DIR" ]; then
61
+ ACCOUNT_DIR=$(find "$ACCOUNTS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | head -1)
62
+ fi
63
+ STATE_DIR="${ARCHIVE_INGEST_GATE_STATE_DIR:-${ACCOUNT_DIR}/state}"
64
+ FLAG_FILE="${STATE_DIR}/archive-ingest-parse-error.flag"
65
+ TTL_SECONDS=600
66
+
67
+ # ----- Parse fields from stdin ---------------------------------------------
68
+ HOOK_EVENT=$(printf '%s' "$INPUT" | grep -o '"hook_event_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
69
+ TOOL_NAME=$(printf '%s' "$INPUT" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
70
+
71
+ # ============================================================================
72
+ # UserPromptSubmit — clear the parse-error flag.
73
+ # ============================================================================
74
+ if [ "$HOOK_EVENT" = "UserPromptSubmit" ]; then
75
+ if [ -f "$FLAG_FILE" ]; then
76
+ rm -f "$FLAG_FILE" 2>/dev/null
77
+ echo "[archive-ingest-gate] cleared reason=user-prompt-submit" >&2
78
+ fi
79
+ exit 0
80
+ fi
81
+
82
+ # ============================================================================
83
+ # PostToolUse — record parse-error from any *-export-parse / *-import-parse.
84
+ # ============================================================================
85
+ if [ "$HOOK_EVENT" = "PostToolUse" ]; then
86
+ case "$TOOL_NAME" in
87
+ mcp__*__*-export-parse|mcp__*__*-import-parse)
88
+ if printf '%s' "$INPUT" | grep -Eq '"isError"[[:space:]]*:[[:space:]]*true'; then
89
+ mkdir -p "$STATE_DIR" 2>/dev/null
90
+ date -u +%s > "$FLAG_FILE" 2>/dev/null
91
+ echo "[archive-ingest-gate] surfaced reason=parse-error tool=${TOOL_NAME}" >&2
92
+ fi
93
+ ;;
94
+ esac
95
+ exit 0
96
+ fi
97
+
98
+ # ============================================================================
99
+ # PreToolUse — five independent blocks.
100
+ # ============================================================================
101
+ if [ "$HOOK_EVENT" != "PreToolUse" ]; then
102
+ exit 0
103
+ fi
104
+
105
+ # Helper: emit a single decision-log line + remediation message, then exit.
106
+ # Args: $1=decision (allow|block) $2=reason $3=remediation message
107
+ emit_decision() {
108
+ local decision="$1" reason="$2" remediation="$3" cmd="${4:-}"
109
+ local cmd_field=""
110
+ if [ -n "$cmd" ]; then
111
+ # Sanitise: strip control chars, take first 60 chars.
112
+ cmd_field=" command=\"$(printf '%s' "$cmd" | tr -d '\000-\037' | cut -c1-60)\""
113
+ fi
114
+ echo "[archive-ingest-gate] decision=${decision} tool=${TOOL_NAME} reason=${reason}${cmd_field}" >&2
115
+ if [ "$decision" = "block" ]; then
116
+ echo "$remediation" >&2
117
+ exit 2
118
+ fi
119
+ }
120
+
121
+ # --- Block 1: post-parse-error gate (applies to ALL tools) -----------------
122
+ if [ -f "$FLAG_FILE" ]; then
123
+ FLAG_TS=$(cat "$FLAG_FILE" 2>/dev/null | head -1)
124
+ NOW=$(date -u +%s)
125
+ if [ -n "$FLAG_TS" ] && [ "$FLAG_TS" -gt 0 ] 2>/dev/null; then
126
+ AGE=$(( NOW - FLAG_TS ))
127
+ if [ "$AGE" -lt "$TTL_SECONDS" ]; then
128
+ emit_decision "block" "post-parse-error age_s=${AGE}" \
129
+ "Blocked: an archive-parser MCP tool returned isError=true earlier in this turn. The subagent's next action must be a user-facing message naming the parse-error and yielding back to the operator. Further tool calls are blocked until the operator submits a new prompt."
130
+ fi
131
+ rm -f "$FLAG_FILE" 2>/dev/null
132
+ else
133
+ rm -f "$FLAG_FILE" 2>/dev/null
134
+ fi
135
+ fi
136
+
137
+ # --- Block 2: legacy WhatsApp MCP tools — denied unconditionally ----------
138
+ case "$TOOL_NAME" in
139
+ mcp__memory__whatsapp-export-parse|mcp__memory__whatsapp-export-insight-write)
140
+ emit_decision "block" "denied-mcp-legacy" \
141
+ "Blocked: ${TOOL_NAME} is the Task 804 legacy path. Task 855 ships the deterministic Bash entry — invoke 'bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --scope <admin|public>' once. Parse, archive-write, and insight all run in-process; do not call legacy MCP tools."
142
+ ;;
143
+ esac
144
+
145
+ # Helper: extract a top-level field from `tool_input` via python3 — never via
146
+ # grep+sed against the raw JSON, which would pick the first textual occurrence
147
+ # including nested-object matches (`rows[0].archiveType`,
148
+ # `conversation.archiveType`, etc.) and let a malicious payload bypass the
149
+ # block. python3 is the project standard for JSON-aware hook parsing
150
+ # (mirrors lane-gate.sh:31-46). On parse failure return empty string —
151
+ # downstream block conditions skip cleanly.
152
+ extract_tool_input_field() {
153
+ local field="$1"
154
+ printf '%s' "$INPUT" | python3 -c "
155
+ import sys, json
156
+ try:
157
+ d = json.load(sys.stdin)
158
+ print(d.get('tool_input', {}).get('$field', ''))
159
+ except Exception:
160
+ print('')
161
+ " 2>/dev/null || echo ""
162
+ }
163
+
164
+ # --- Block 3: memory-archive-write conditional on whatsapp-export -----------
165
+ # LinkedIn and future archiveTypes flow unchanged through memory-archive-write.
166
+ # Only `archiveType=whatsapp-export` is now restricted to the script path.
167
+ if [ "$TOOL_NAME" = "mcp__memory__memory-archive-write" ]; then
168
+ ARCHIVE_TYPE=$(extract_tool_input_field archiveType)
169
+ if [ "$ARCHIVE_TYPE" = "whatsapp-export" ]; then
170
+ emit_decision "block" "denied-mcp-legacy archiveType=whatsapp-export" \
171
+ "Blocked: memory-archive-write with archiveType='whatsapp-export' is the Task 804 legacy path. Task 855 ships the deterministic Bash entry — invoke 'bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --scope <admin|public>' once. Other archiveTypes (linkedin-connections, …) flow through memory-archive-write unchanged."
172
+ fi
173
+ fi
174
+
175
+ # --- Block 4: plugin-source path block (Edit/Write/NotebookEdit) -----------
176
+ case "$TOOL_NAME" in
177
+ Edit|Write|NotebookEdit)
178
+ FILE_PATH=$(extract_tool_input_field file_path)
179
+ case "$FILE_PATH" in
180
+ */platform/plugins/*/lib/*|platform/plugins/*/lib/*)
181
+ emit_decision "block" "plugin-source-edit path=${FILE_PATH}" \
182
+ "Blocked: ${TOOL_NAME} on ${FILE_PATH} is a platform plugin lib/ path. The database-operator subagent does not own plugin source; if a parser is broken, surface the parse-error to the operator and let them dispatch a code-edit task instead."
183
+ ;;
184
+ esac
185
+ ;;
186
+ esac
187
+
188
+ # --- Block 5: shell test-runner block (Bash) -------------------------------
189
+ COMMAND=""
190
+ if [ "$TOOL_NAME" = "Bash" ]; then
191
+ COMMAND=$(extract_tool_input_field command)
192
+ if printf '%s' "$COMMAND" | grep -Eq '(^|[[:space:]/])vitest($|[[:space:]])|(^|[[:space:]])bun[[:space:]]+test($|[[:space:]])|(^|[[:space:]])npm[[:space:]]+test($|[[:space:]])|(^|[[:space:]])npx[[:space:]]+jest($|[[:space:]])|node[[:space:]].*vitest'; then
193
+ emit_decision "block" "test-runner" \
194
+ "Blocked: Bash command invokes a JavaScript test runner (vitest/bun test/npm test/npx jest). The database-operator subagent does not run plugin tests; surface the parse-error to the operator." \
195
+ "$COMMAND"
196
+ fi
197
+ fi
198
+
199
+ # Default — allow. Emit a single-line decision log so successful invocations
200
+ # leave a grep-able trail too. (Bash gets command field; other tools omit.)
201
+ if [ "$TOOL_NAME" = "Bash" ]; then
202
+ emit_decision "allow" "default" "" "$COMMAND"
203
+ else
204
+ emit_decision "allow" "default" ""
205
+ fi
206
+
207
+ exit 0
@@ -40,7 +40,7 @@ These are enabled during onboarding and can be added or removed at any time. Som
40
40
  | `waitlist` | Waitlist lifecycle — extract sign-ups from conversations, review | — |
41
41
  | `replicate` | Image generation — three models for photorealistic, design, and fast draft images | Content producer, Research assistant |
42
42
  | `linkedin-import` | Import a LinkedIn Basic Data Export — Profile and Connections today, more CSVs as references land | Database operator |
43
- | `whatsapp-import` | Import a WhatsApp `_chat.txt` export as a Conversation + chronologically-chained Messages, then extract typed insights (mentions, preferences, commitments, observed relationships) — distinct from the live `whatsapp` plugin which is a Baileys QR-pairing channel | Database operator |
43
+ | `whatsapp-import` | Import a WhatsApp `_chat.txt` export as a Conversation + chronologically-chained Messages plus `:Observation` nodes for typed insights (mentions, tasks, preferences, observed relationships). Single Bash entry `bash platform/plugins/whatsapp-import/bin/whatsapp-ingest.sh <archive> --owner-element-id <id> --scope <admin\|public>` runs parse → archive-write → Haiku insight in one process. Distinct from the live `whatsapp` plugin which is a Baileys QR-pairing channel. | Database operator |
44
44
 
45
45
  ### Claude Official (marketplace)
46
46
 
@@ -14,7 +14,7 @@ Ingests a WhatsApp "Export Chat" archive (the `_chat.txt` file plus media attach
14
14
 
15
15
  ## When this applies
16
16
 
17
- The admin agent delegates to `database-operator` when the operator drops a `_chat.txt` (or its containing folder) into chat. The specialist runs the skill's archive-owner + participant confirmation flow before any line is written, then ingests the messages via `mcp__memory__memory-archive-write` with `archiveType='whatsapp-export'`.
17
+ The admin agent delegates to `database-operator` when the operator drops a `_chat.txt` (or its containing folder) into chat. The specialist runs the skill's archive-owner confirmation flow before any line is written, then invokes the deterministic Bash entry (`bin/whatsapp-ingest.sh`) once: parse, archive-write (via `memoryArchiveWrite` in-process), and Haiku insight all run in one Node process — no MCP envelope between steps (Task 855).
18
18
 
19
19
  ## Accepted export shapes
20
20
 
@@ -28,6 +28,6 @@ WhatsApp's "Export Chat" emits `[DD/MM/YYYY, HH:MM:SS]` prefixes by default in m
28
28
 
29
29
  ## Relationship to other plugins
30
30
 
31
- - **memory** — the underlying write surface used by the skill (`memory-archive-write` for bulk Conversation+Messages, `memory-write` / `memory-update` for the second-pass typed observations). The skill is parameterised so all writes carry `source='whatsapp'` + `createdByAgent='whatsapp-import'` for provenance.
31
+ - **memory** — the underlying write surface imported in-process by `bin/ingest.mjs` (`memoryArchiveWrite` for bulk Conversation+Messages; direct Cypher `:Observation` writes for the insight pass). All writes carry `source='whatsapp'` + `createdByAgent='whatsapp-import'` provenance. The legacy `mcp__memory__whatsapp-export-parse` / `whatsapp-export-insight-write` MCP tools and the direct `memory-archive-write` MCP path with `archiveType=whatsapp-export` are blocked at the harness — the Bash entry is the only supported invocation surface (Task 855).
32
32
  - **database-operator specialist** — owns execution. See [admin/IDENTITY.md](../../../platform/templates/agents/admin/IDENTITY.md) delegation clause and [database-operator.md](../../../platform/templates/specialists/agents/database-operator.md) per-source archive list.
33
33
  - **linkedin-import** — sister plugin under the same pattern (LinkedIn Basic Data Export). Reading [linkedin-import/PLUGIN.md](../linkedin-import/PLUGIN.md) is the fastest way to understand the shape this plugin follows.