@rubytech/create-maxy 1.0.794 → 1.0.796

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 (69) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/neo4j/migrations/003-person-name-eradicate.cypher +24 -0
  3. package/payload/platform/plugins/admin/hooks/__tests__/archive-ingest-gate.test.sh +166 -0
  4. package/payload/platform/plugins/admin/hooks/archive-ingest-gate.sh +147 -0
  5. package/payload/platform/plugins/docs/references/internals.md +4 -0
  6. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +2 -0
  7. package/payload/platform/plugins/memory/PLUGIN.md +4 -2
  8. package/payload/platform/plugins/memory/mcp/dist/index.js +2 -2
  9. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  10. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +11 -0
  11. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
  12. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js +124 -1
  13. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js.map +1 -1
  14. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts +12 -0
  15. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
  16. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +41 -2
  17. package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
  18. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts +9 -0
  19. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts.map +1 -1
  20. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js +44 -0
  21. package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js.map +1 -1
  22. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write.test.js +20 -2
  23. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-archive-write.test.js.map +1 -1
  24. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts +1 -1
  25. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +0 -1
  27. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
  28. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-parse.d.ts +2 -1
  29. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-parse.d.ts.map +1 -1
  30. package/payload/platform/plugins/memory/mcp/dist/tools/whatsapp-export-parse.js.map +1 -1
  31. package/payload/platform/plugins/memory/references/schema-base.md +11 -1
  32. package/payload/platform/plugins/whatsapp-import/PLUGIN.md +4 -0
  33. package/payload/platform/plugins/whatsapp-import/lib/dist/parse-export.d.ts +8 -2
  34. package/payload/platform/plugins/whatsapp-import/lib/dist/parse-export.d.ts.map +1 -1
  35. package/payload/platform/plugins/whatsapp-import/lib/dist/parse-export.js +66 -15
  36. package/payload/platform/plugins/whatsapp-import/lib/dist/parse-export.js.map +1 -1
  37. package/payload/platform/plugins/whatsapp-import/lib/src/__tests__/parse-export.test.ts +175 -0
  38. package/payload/platform/plugins/whatsapp-import/lib/src/parse-export.ts +78 -17
  39. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/SKILL.md +2 -0
  40. package/payload/platform/plugins/whatsapp-import/skills/whatsapp-import/references/export-parse.md +8 -6
  41. package/payload/platform/scripts/seed-neo4j.sh +43 -20
  42. package/payload/platform/templates/specialists/agents/database-operator.md +2 -0
  43. package/payload/server/chunk-BURNRCKP.js +3405 -0
  44. package/payload/server/chunk-JSBRDJBE.js +30 -0
  45. package/payload/server/chunk-KM23Y7SY.js +9896 -0
  46. package/payload/server/client-pool-PV45NUTN.js +29 -0
  47. package/payload/server/maxy-edge.js +3 -2
  48. package/payload/server/neo4j-migrations-IUSBODOP.js +51 -0
  49. package/payload/server/public/assets/{Checkbox-DHsoNPeM.js → Checkbox-BruL6MSR.js} +1 -1
  50. package/payload/server/public/assets/{admin-DEhQ1wNO.js → admin-StzFnTQB.js} +60 -60
  51. package/payload/server/public/assets/data-BvV94XHO.js +1 -0
  52. package/payload/server/public/assets/graph-BOKpKqLw.js +1 -0
  53. package/payload/server/public/assets/{jsx-runtime-lOmSwjvd.css → jsx-runtime-foO6ZMix.css} +1 -1
  54. package/payload/server/public/assets/page-DItB4skl.js +50 -0
  55. package/payload/server/public/assets/page-DM19J3ur.js +1 -0
  56. package/payload/server/public/assets/{public-Bn-gEWOv.js → public-CfjzDdUe.js} +1 -1
  57. package/payload/server/public/assets/useAdminFetch-iYCQ9lT0.js +1 -0
  58. package/payload/server/public/assets/{useVoiceRecorder-B1S_t3Hq.js → useVoiceRecorder-D_8P7xJU.js} +1 -1
  59. package/payload/server/public/data.html +5 -5
  60. package/payload/server/public/graph.html +6 -6
  61. package/payload/server/public/index.html +8 -8
  62. package/payload/server/public/public.html +5 -5
  63. package/payload/server/server.js +99 -131
  64. package/payload/server/public/assets/data-bIkywng-.js +0 -1
  65. package/payload/server/public/assets/graph-DwzwJvlu.js +0 -1
  66. package/payload/server/public/assets/page-BuoQU1c6.js +0 -50
  67. package/payload/server/public/assets/page-DU8F3OGU.js +0 -1
  68. package/payload/server/public/assets/share-2-0IDKUUq9.js +0 -1
  69. /package/payload/server/public/assets/{jsx-runtime-Br2bU3EJ.js → jsx-runtime-DJER3a7U.js} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.794",
3
+ "version": "1.0.796",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -0,0 +1,24 @@
1
+ // ============================================================
2
+ // Migration 003 — Person.name eradication (Task 849)
3
+ //
4
+ // Removes the denormalised `name` property from every Person node.
5
+ // `Person.name` is forbidden by schema-base.md "Forbidden Properties":
6
+ // the canonical fields are `givenName` + `familyName`, composed at
7
+ // read time by display-helpers.ts. Persisted `name` was a divergence
8
+ // trap — LLM extraction emitted `name = givenName` ("Dan") alongside
9
+ // the structured pair, so the canvas rendered "Dan" instead of
10
+ // "Dan Brett".
11
+ //
12
+ // Idempotent: subsequent runs find no Persons with `name` set and
13
+ // return removed=0.
14
+ //
15
+ // Applied at boot by platform/ui/app/lib/neo4j-migrations.ts. Safe to
16
+ // run manually:
17
+ // cypher-shell -u neo4j -p <password> -a $NEO4J_URI \
18
+ // -f platform/neo4j/migrations/003-person-name-eradicate.cypher
19
+ // ============================================================
20
+
21
+ MATCH (p:Person)
22
+ WHERE p.name IS NOT NULL
23
+ REMOVE p.name
24
+ RETURN count(p) AS removed
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env bash
2
+ # Regression test for archive-ingest-gate.sh (Task 846).
3
+ #
4
+ # Six cases cover the contract:
5
+ # 1. Edit on /platform/plugins/<x>/lib/* is BLOCKED (exit 2).
6
+ # 2. Edit on a benign path is ALLOWED (exit 0).
7
+ # 3. Bash with `npx vitest` is BLOCKED.
8
+ # 4. PostToolUse on whatsapp-export-parse with isError:true sets the flag.
9
+ # 5. Subsequent PreToolUse on ANY tool is BLOCKED (post-parse-error gate).
10
+ # 6. UserPromptSubmit clears the flag, restoring normal allow behavior.
11
+ #
12
+ # Tests use ARCHIVE_INGEST_GATE_STATE_DIR to point at a tmp dir so they run
13
+ # without a real account layout.
14
+
15
+ set -u
16
+
17
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/archive-ingest-gate.sh"
18
+ if [[ ! -x "$HOOK" ]]; then
19
+ echo "FAIL: $HOOK not executable" >&2
20
+ exit 1
21
+ fi
22
+
23
+ # Per-run isolated state dir
24
+ STATE_DIR=$(mktemp -d)
25
+ export ARCHIVE_INGEST_GATE_STATE_DIR="$STATE_DIR"
26
+ FLAG_FILE="$STATE_DIR/archive-ingest-parse-error.flag"
27
+
28
+ cleanup() { rm -rf "$STATE_DIR"; }
29
+ trap cleanup EXIT
30
+
31
+ PASS=0
32
+ FAIL=0
33
+
34
+ run_case() {
35
+ local name="$1" stdin="$2" expected_exit="$3"
36
+ local actual_exit
37
+ printf '%s' "$stdin" | bash "$HOOK" >/dev/null 2>/dev/null
38
+ actual_exit=$?
39
+ if [[ "$actual_exit" -eq "$expected_exit" ]]; then
40
+ echo "PASS: $name (exit=$actual_exit)"
41
+ PASS=$((PASS + 1))
42
+ else
43
+ echo "FAIL: $name (expected exit=$expected_exit, got=$actual_exit)" >&2
44
+ FAIL=$((FAIL + 1))
45
+ fi
46
+ }
47
+
48
+ # Case 1 — Edit on plugin lib path: BLOCKED
49
+ run_case "Edit on platform/plugins/whatsapp-import/lib/src/parse-export.ts → BLOCKED" \
50
+ '{"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"}}' \
51
+ 2
52
+
53
+ # Case 2 — Edit on a benign path: ALLOWED
54
+ run_case "Edit on README.md → ALLOWED" \
55
+ '{"hook_event_name":"PreToolUse","tool_name":"Edit","tool_input":{"file_path":"/Users/x/repo/README.md","old_string":"a","new_string":"b"}}' \
56
+ 0
57
+
58
+ # Case 3 — Bash with `npx vitest`: BLOCKED
59
+ run_case "Bash 'npx vitest run parse-export.test.ts' → BLOCKED" \
60
+ '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npx vitest run parse-export.test.ts"}}' \
61
+ 2
62
+
63
+ # Case 3b — Bash with benign command: ALLOWED
64
+ run_case "Bash 'ls -la' → ALLOWED" \
65
+ '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"ls -la"}}' \
66
+ 0
67
+
68
+ # Case 3c — Bash with `bun test`: BLOCKED
69
+ run_case "Bash 'bun test' → BLOCKED" \
70
+ '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"bun test"}}' \
71
+ 2
72
+
73
+ # Case 3d — Bash with `npm test`: BLOCKED
74
+ run_case "Bash 'npm test' → BLOCKED" \
75
+ '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm test"}}' \
76
+ 2
77
+
78
+ # Make sure flag is absent before parse-error simulation
79
+ rm -f "$FLAG_FILE"
80
+
81
+ # Case 4 — PostToolUse on whatsapp-export-parse with isError:true sets flag
82
+ run_case "PostToolUse parse-error sets flag (exit 0, flag side-effect)" \
83
+ '{"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"}]}}' \
84
+ 0
85
+
86
+ if [[ -f "$FLAG_FILE" ]]; then
87
+ echo "PASS: parse-error flag created at $FLAG_FILE"
88
+ PASS=$((PASS + 1))
89
+ else
90
+ echo "FAIL: parse-error flag NOT created at $FLAG_FILE" >&2
91
+ FAIL=$((FAIL + 1))
92
+ fi
93
+
94
+ # Case 5 — Subsequent PreToolUse on ANY tool BLOCKED while flag is fresh
95
+ run_case "PreToolUse Read after parse-error → BLOCKED" \
96
+ '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' \
97
+ 2
98
+
99
+ run_case "PreToolUse Bash after parse-error → BLOCKED" \
100
+ '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"echo hi"}}' \
101
+ 2
102
+
103
+ # Case 6 — UserPromptSubmit clears flag
104
+ run_case "UserPromptSubmit clears flag (exit 0)" \
105
+ '{"hook_event_name":"UserPromptSubmit","prompt":"retry"}' \
106
+ 0
107
+
108
+ if [[ ! -f "$FLAG_FILE" ]]; then
109
+ echo "PASS: UserPromptSubmit cleared flag"
110
+ PASS=$((PASS + 1))
111
+ else
112
+ echo "FAIL: UserPromptSubmit did NOT clear flag" >&2
113
+ FAIL=$((FAIL + 1))
114
+ fi
115
+
116
+ # Case 7 — After clearance, normal allow resumes
117
+ run_case "PreToolUse Read after clearance → ALLOWED" \
118
+ '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' \
119
+ 0
120
+
121
+ # Case 8 — PostToolUse with isError:false does NOT set flag
122
+ rm -f "$FLAG_FILE"
123
+ run_case "PostToolUse parse-success (isError:false) does NOT set flag" \
124
+ '{"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\":[]}"}]}}' \
125
+ 0
126
+
127
+ if [[ ! -f "$FLAG_FILE" ]]; then
128
+ echo "PASS: parse-success leaves flag absent"
129
+ PASS=$((PASS + 1))
130
+ else
131
+ echo "FAIL: parse-success incorrectly created flag" >&2
132
+ FAIL=$((FAIL + 1))
133
+ fi
134
+
135
+ # Case 9 — Stale flag (>600s) auto-clears + allows
136
+ PAST=$(( $(date -u +%s) - 700 ))
137
+ echo "$PAST" > "$FLAG_FILE"
138
+ run_case "Stale flag auto-clears, PreToolUse Read → ALLOWED" \
139
+ '{"hook_event_name":"PreToolUse","tool_name":"Read","tool_input":{"file_path":"/tmp/foo"}}' \
140
+ 0
141
+
142
+ # Case 10 — No stdin (terminal) fails closed
143
+ echo "Probing fail-closed behaviour (no stdin)..."
144
+ bash "$HOOK" </dev/null >/dev/null 2>/dev/null
145
+ ACTUAL=$?
146
+ # /dev/null IS a stdin — the `[ -t 0 ]` check tests for terminal, not file.
147
+ # A file/pipe stdin reads as empty, which produces empty hook_event_name and
148
+ # falls through to default `exit 0` (allow). The terminal-only fail-closed
149
+ # branch can't be tested non-interactively; verify the script reads `[ -t 0 ]`.
150
+ if grep -q '\[ -t 0 \]' "$HOOK"; then
151
+ echo "PASS: fail-closed terminal check is present"
152
+ PASS=$((PASS + 1))
153
+ else
154
+ echo "FAIL: fail-closed terminal check missing" >&2
155
+ FAIL=$((FAIL + 1))
156
+ fi
157
+
158
+ echo
159
+ echo "──────── archive-ingest-gate test summary ────────"
160
+ echo "PASS: $PASS"
161
+ echo "FAIL: $FAIL"
162
+
163
+ if [[ "$FAIL" -gt 0 ]]; then
164
+ exit 1
165
+ fi
166
+ exit 0
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env bash
2
+ # Archive-ingest gate (Task 846).
3
+ #
4
+ # Three enforcements, one script — phase decided by `hook_event_name` on stdin:
5
+ #
6
+ # 1. PreToolUse Edit/Write/NotebookEdit: deny writes under
7
+ # `*platform/plugins/*/lib/*` (parser/CSV-shape source for any *-import or
8
+ # *-export plugin). The database-operator subagent has Read+Bash but not
9
+ # Edit/Write per its `tools:` frontmatter — yet it can still mutate files
10
+ # via Bash heredoc. The path block applies to every tool that takes a
11
+ # `file_path` and has been observed as the improvisation surface.
12
+ #
13
+ # 2. PreToolUse Bash: deny commands invoking JavaScript test runners
14
+ # (vitest|bun test|npm test|npx jest|node .*vitest). The reproducer
15
+ # incident (conv 47c6a590) saw the operator run all four variants in
16
+ # sequence after parse-export returned isError.
17
+ #
18
+ # 3. Parse-error gate: PostToolUse on any `mcp__*__*-export-parse` /
19
+ # `mcp__*__*-import-parse` tool whose `tool_response.isError == true`
20
+ # writes a flag file. Subsequent PreToolUse on ANY tool blocks until
21
+ # UserPromptSubmit clears the flag (semantics: "subagent's next action
22
+ # must be a user-facing message; further tool calls blocked"). A 600s
23
+ # TTL is the cross-session safety net.
24
+ #
25
+ # Exit codes follow Claude Code hook protocol: 0 = allow, 2 = block (stderr
26
+ # message shown to the agent). Fail-closed on stdin read failure to match
27
+ # pre-tool-use.sh.
28
+
29
+ set -uo pipefail
30
+
31
+ # Read stdin — fail closed if unavailable
32
+ if [ -t 0 ]; then
33
+ echo "Blocked: archive-ingest-gate received no stdin (cannot inspect tool call). Failing closed." >&2
34
+ exit 2
35
+ fi
36
+ INPUT=$(cat)
37
+
38
+ # ----- Resolve account dir for state file ----------------------------------
39
+ # Mirrors pre-tool-use.sh lines 100-107: walk from this hook's location to
40
+ # platform/, then `../data/accounts/<single-account>/`. Phase 0 has exactly
41
+ # one account directory.
42
+ HOOK_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)"
43
+ PLATFORM_ROOT_RESOLVED="${HOOK_DIR}/../../.."
44
+ ACCOUNTS_DIR="${PLATFORM_ROOT_RESOLVED}/../data/accounts"
45
+ ACCOUNT_DIR=""
46
+ if [ -d "$ACCOUNTS_DIR" ]; then
47
+ ACCOUNT_DIR=$(find "$ACCOUNTS_DIR" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | head -1)
48
+ fi
49
+ # Test harness override — tests pass `ARCHIVE_INGEST_GATE_STATE_DIR` to point
50
+ # at a tmp dir without needing a real account layout.
51
+ STATE_DIR="${ARCHIVE_INGEST_GATE_STATE_DIR:-${ACCOUNT_DIR}/state}"
52
+ FLAG_FILE="${STATE_DIR}/archive-ingest-parse-error.flag"
53
+ TTL_SECONDS=600
54
+
55
+ # ----- Parse fields from stdin ---------------------------------------------
56
+ # Hook protocol: every event includes `hook_event_name`. PreToolUse +
57
+ # PostToolUse include `tool_name`. PostToolUse adds `tool_response`.
58
+ # UserPromptSubmit has neither. Use grep/sed against the JSON envelope —
59
+ # no jq dependency to match the rest of the hook fleet.
60
+ HOOK_EVENT=$(printf '%s' "$INPUT" | grep -o '"hook_event_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
61
+ TOOL_NAME=$(printf '%s' "$INPUT" | grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
62
+
63
+ # ============================================================================
64
+ # UserPromptSubmit — clear the parse-error flag.
65
+ # ============================================================================
66
+ if [ "$HOOK_EVENT" = "UserPromptSubmit" ]; then
67
+ if [ -f "$FLAG_FILE" ]; then
68
+ rm -f "$FLAG_FILE" 2>/dev/null
69
+ echo "[archive-ingest-gate] cleared reason=user-prompt-submit" >&2
70
+ fi
71
+ exit 0
72
+ fi
73
+
74
+ # ============================================================================
75
+ # PostToolUse — record parse-error from any *-export-parse / *-import-parse.
76
+ # ============================================================================
77
+ if [ "$HOOK_EVENT" = "PostToolUse" ]; then
78
+ case "$TOOL_NAME" in
79
+ mcp__*__*-export-parse|mcp__*__*-import-parse)
80
+ # Inspect tool_response for isError true. Body is a JSON object; match
81
+ # `"isError":true` with optional whitespace.
82
+ if printf '%s' "$INPUT" | grep -Eq '"isError"[[:space:]]*:[[:space:]]*true'; then
83
+ mkdir -p "$STATE_DIR" 2>/dev/null
84
+ date -u +%s > "$FLAG_FILE" 2>/dev/null
85
+ echo "[archive-ingest-gate] surfaced reason=parse-error tool=${TOOL_NAME}" >&2
86
+ fi
87
+ ;;
88
+ esac
89
+ # PostToolUse must always allow the tool result through.
90
+ exit 0
91
+ fi
92
+
93
+ # ============================================================================
94
+ # PreToolUse — three independent blocks.
95
+ # ============================================================================
96
+ if [ "$HOOK_EVENT" != "PreToolUse" ]; then
97
+ # Unknown event, or hook fired in a context without an event name. Allow.
98
+ exit 0
99
+ fi
100
+
101
+ # --- Block 1: post-parse-error gate (applies to ALL tools) -----------------
102
+ if [ -f "$FLAG_FILE" ]; then
103
+ FLAG_TS=$(cat "$FLAG_FILE" 2>/dev/null | head -1)
104
+ NOW=$(date -u +%s)
105
+ if [ -n "$FLAG_TS" ] && [ "$FLAG_TS" -gt 0 ] 2>/dev/null; then
106
+ AGE=$(( NOW - FLAG_TS ))
107
+ if [ "$AGE" -lt "$TTL_SECONDS" ]; then
108
+ echo "[archive-ingest-gate] block tool=${TOOL_NAME} reason=post-parse-error age_s=${AGE}" >&2
109
+ echo "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." >&2
110
+ exit 2
111
+ fi
112
+ # Stale flag — clean up so we don't keep blocking on a corpse.
113
+ rm -f "$FLAG_FILE" 2>/dev/null
114
+ else
115
+ # Unparseable flag — remove rather than block on garbage.
116
+ rm -f "$FLAG_FILE" 2>/dev/null
117
+ fi
118
+ fi
119
+
120
+ # --- Block 2: plugin-source path block (Edit/Write/NotebookEdit) -----------
121
+ case "$TOOL_NAME" in
122
+ Edit|Write|NotebookEdit)
123
+ FILE_PATH=$(printf '%s' "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
124
+ case "$FILE_PATH" in
125
+ */platform/plugins/*/lib/*|platform/plugins/*/lib/*)
126
+ echo "[archive-ingest-gate] block tool=${TOOL_NAME} reason=plugin-source-edit path=${FILE_PATH}" >&2
127
+ echo "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." >&2
128
+ exit 2
129
+ ;;
130
+ esac
131
+ ;;
132
+ esac
133
+
134
+ # --- Block 3: shell test-runner block (Bash) -------------------------------
135
+ if [ "$TOOL_NAME" = "Bash" ]; then
136
+ COMMAND=$(printf '%s' "$INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
137
+ # Match any of: `vitest`, `bun test`, `npm test`, `npx jest`, `node ... vitest`.
138
+ # Word-boundary checks via grep -E with explicit token boundaries — POSIX
139
+ # `[[:space:]]` covers leading-token edge cases, end-of-string covers tail.
140
+ 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
141
+ echo "[archive-ingest-gate] block tool=Bash reason=test-runner command=${COMMAND}" >&2
142
+ echo "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." >&2
143
+ exit 2
144
+ fi
145
+ fi
146
+
147
+ exit 0
@@ -317,6 +317,10 @@ This tool is read-only and available to both public and admin agents.
317
317
 
318
318
  Each row in the Conversations modal exposes a `View logs` row-action that opens a popover with three links — **Stream**, **Errors**, **SSE** — each of which targets `/api/admin/logs?type={stream|error|sse}&conversationId={full-id}` in a new tab. The row's 8-char id chip is click-to-copy; hover reveals the full `conversationId` as a tooltip. See `.docs/web-chat.md` "In-chat retrieval" for the route contract and `console.debug` observability (Task 686).
319
319
 
320
+ ### Cross-tab session rotation (Task 848)
321
+
322
+ When you click "New conversation" in the chat tab, {{productName}} mints a fresh admin session key on the server and clears the old one. Sibling admin tabs (`/graph`, `/data`) opened in the same browser keep working without re-login: the chat tab broadcasts the new key on a same-origin channel so each sibling tab updates its captured key instantly, and any in-flight admin request that 401s with the rotation-orphan code retries once after re-reading the latest key from per-tab storage. If neither path recovers (browser locked down, second 401 after retry, session expired), the tab shows a single banner — "Your admin session was renewed in another tab. Click to reload." — and one click sends you back through login. No silent 401s; no re-clicking through the same trash icon hoping it sticks. See `.docs/web-chat.md` "Cross-tab rotation contract (Task 848)" for the wire-level `code` taxonomy and observability surfaces.
323
+
320
324
  ---
321
325
 
322
326
  ## Context Assembly — How Retrieved Knowledge Reaches the Agent
@@ -68,6 +68,8 @@ When the owner is an external Person (non-operator archive), the anchor is the c
68
68
 
69
69
  **Doctrine:** raw Cypher and `cypher-shell` invocations are forbidden in this skill and its references. Writes route through `mcp__memory__memory-archive-write` (bulk archives) or `mcp__memory__memory-write` / `mcp__memory__memory-update` (single-node enrichments like `profile.md`). If a CSV needs a write shape no current MCP tool supports, file a task to extend `memory-archive-write` with a new `archiveType` handler — never improvise via Bash. See [database-operator's LOUD-FAIL prerogative](../../../../templates/specialists/agents/database-operator.md#prerogatives).
70
70
 
71
+ **LOUD-FAIL on parse errors (structurally enforced, Task 846).** When LinkedIn parser tools land that follow the `mcp__*__*-import-parse` naming convention, the harness-level `platform/plugins/admin/hooks/archive-ingest-gate.sh` will record an `isError: true` response and block every subsequent tool call this turn until the operator submits the next prompt. The hook also denies edits to `platform/plugins/*/lib/*` and JavaScript test runners (`vitest`, `bun test`, `npm test`, `npx jest`) unconditionally. The skill's "no Bash improvisation" doctrine above is the contract; the hook is the enforcement. See [.docs/hooks.md](../../../../../.docs/hooks.md) for the full gate surface.
72
+
71
73
  ## Selective-ingest threshold (bulk archives)
72
74
 
73
75
  A LinkedIn export typically contains 3,000–10,000 connections. Writing all of them in one shot defeats compression-on-write — most rows will never be queried, and the noise compounds with every subsequent ingest. The skill compresses by interrogating the operator before bulk writes.
@@ -94,9 +94,11 @@ Restricted fields (`accountId`, `embedding`, `profileVersion`) cannot be set via
94
94
 
95
95
  ## Schema References
96
96
 
97
- Before any structured write, load `references/schema-base.md` via `plugin-read`. This defines property naming rules, required-property groups for documented types, and relationship patterns. If the `LocalBusiness` node has a `businessType` property, also load the matching vertical schema (`references/schema-{businessType}.md`) — it extends the base with vertical-specific types. Confirm which schemas were consulted before writing.
97
+ Before any structured write, load `references/schema-base.md` via `plugin-read`. This defines property naming rules, required-property groups for documented types, forbidden-property rules, and relationship patterns. If the `LocalBusiness` node has a `businessType` property, also load the matching vertical schema (`references/schema-{businessType}.md`) — it extends the base with vertical-specific types. Confirm which schemas were consulted before writing.
98
98
 
99
- **Validation surface (Task 736).** `memory-write` validates labels against `db.labels()` ∪ `schema.cypher` declarations — not against this markdown. A label is recognised if it appears in either set. The markdown defines property *shape* (required-property groups, naming rules) for documented labels only; recognised-but-undocumented labels (e.g. `LocalBusiness`, `AdminUser`, `KnowledgeDocument`) accept any property shape and emit `[schema-validator] markdown-undocumented label=<X>` so the doc gap is visible to operators. If `memory-write` rejects a label as unknown, the rejection lists both source sets — the agent can call `maxy-graph-get_neo4j_schema` to refresh its view.
99
+ **Validation surface (Task 736).** `memory-write` validates labels against `db.labels()` ∪ `schema.cypher` declarations — not against this markdown. A label is recognised if it appears in either set. The markdown defines property *shape* (required-property groups, naming rules, forbidden-property rules) for documented labels only; recognised-but-undocumented labels (e.g. `LocalBusiness`, `AdminUser`, `KnowledgeDocument`) accept any property shape and emit `[schema-validator] markdown-undocumented label=<X>` so the doc gap is visible to operators. If `memory-write` rejects a label as unknown, the rejection lists both source sets — the agent can call `maxy-graph-get_neo4j_schema` to refresh its view.
100
+
101
+ **Forbidden properties (Task 849).** A "Forbidden Properties" table in any schema markdown file declares per-label deny-listed property keys. The validator rejects writes that include a forbidden key for the matching primary label and emits `[schema-validator] outcome=rejected label=<X> property=<Y> reason=forbidden writer=<agentName>`. The first row is `Person | name` — use `givenName` + `familyName` (rendered display name composed at read time). The check fires after the synonym pass, so `firstName → givenName` rewrites still happen first.
100
102
 
101
103
  ## Document Ingestion
102
104
 
@@ -860,9 +860,9 @@ if (!readOnly) {
860
860
  .min(1)
861
861
  .describe("IANA timezone the operator confirmed (e.g. 'Europe/London', 'America/New_York', 'UTC'). Each parsed timestamp is emitted as ISO 8601 with the offset that this zone holds for the wall-clock instant — DST is handled correctly."),
862
862
  dateFormat: z
863
- .enum(["DD/MM/YY", "MM/DD/YY"])
863
+ .enum(["DD/MM/YY", "MM/DD/YY", "DD/MM/YYYY", "MM/DD/YYYY"])
864
864
  .optional()
865
- .describe("Date ordering the export uses. Defaults to DD/MM/YY (WhatsApp's default in most locales). Pass MM/DD/YY for US-locale exports confirm with the operator before passing this."),
865
+ .describe("Date ordering and year shape. Omit for auto-detect (Task 845): the parser probes the first matched line as DD/MM and locks that ordering if range-valid; otherwise locks MM/DD. Year shape is independent — both 2-digit (legacy) and 4-digit (modern) years are accepted within the same file. Pass an explicit value only when the operator confirms a US-locale export or when auto-detect would mis-lock on a manually concatenated multi-locale archive."),
866
866
  }, async ({ filePath, timezone, dateFormat }) => {
867
867
  try {
868
868
  const result = await whatsappExportParse({