@rubytech/create-maxy-code 0.1.147 → 0.1.150

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 (119) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-search/dist/index.d.ts +34 -11
  3. package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/graph-search/dist/index.js +42 -13
  5. package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
  6. package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +75 -40
  7. package/payload/platform/lib/graph-search/src/index.ts +45 -12
  8. package/payload/platform/lib/graph-style/dist/index.d.ts +5 -5
  9. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  10. package/payload/platform/lib/graph-style/dist/index.js +11 -9
  11. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  12. package/payload/platform/lib/graph-style/src/index.ts +11 -9
  13. package/payload/platform/lib/graph-trash/dist/index.js +6 -6
  14. package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
  15. package/payload/platform/lib/graph-trash/src/index.ts +6 -6
  16. package/payload/platform/neo4j/edge-annotations.json +4 -4
  17. package/payload/platform/neo4j/schema.cypher +143 -58
  18. package/payload/platform/plugins/admin/PLUGIN.md +1 -2
  19. package/payload/platform/plugins/admin/hooks/__tests__/pre-turn-graph-pass.test.sh +544 -0
  20. package/payload/platform/plugins/admin/hooks/__tests__/session-end-retrospective.test.sh +2 -2
  21. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
  22. package/payload/platform/plugins/admin/hooks/{post-turn-graph-pass.sh → pre-turn-graph-pass.sh} +83 -53
  23. package/payload/platform/plugins/admin/hooks/session-end-retrospective.sh +6 -3
  24. package/payload/platform/plugins/docs/references/linkedin-extension.md +1 -1
  25. package/payload/platform/plugins/docs/references/platform.md +1 -1
  26. package/payload/platform/plugins/docs/references/plugins-guide.md +2 -2
  27. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +5 -5
  28. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  29. package/payload/platform/plugins/memory/PLUGIN.md +4 -4
  30. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +5 -5
  31. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +1 -1
  32. package/payload/platform/plugins/memory/mcp/dist/index.js +35 -33
  33. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  34. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +4 -1
  35. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-rewriter-operator-hint.test.d.ts +2 -0
  37. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-rewriter-operator-hint.test.d.ts.map +1 -0
  38. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-rewriter-operator-hint.test.js +90 -0
  39. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-rewriter-operator-hint.test.js.map +1 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts +14 -0
  41. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts.map +1 -1
  42. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js +27 -4
  43. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js.map +1 -1
  44. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts +1 -1
  45. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js +2 -2
  46. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts +1 -1
  47. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js +1 -1
  48. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +2 -2
  49. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
  50. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +3 -4
  51. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
  52. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.d.ts.map +1 -1
  53. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +6 -0
  54. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -1
  55. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +2 -2
  56. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -1
  57. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +17 -17
  58. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
  59. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-search-fields.test.js +1 -0
  60. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-search-fields.test.js.map +1 -1
  61. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-operator-hint.test.d.ts +2 -0
  62. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-operator-hint.test.d.ts.map +1 -0
  63. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-operator-hint.test.js +329 -0
  64. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-operator-hint.test.js.map +1 -0
  65. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-compiled-truth-rejection.test.d.ts +2 -0
  66. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-compiled-truth-rejection.test.d.ts.map +1 -0
  67. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-compiled-truth-rejection.test.js +39 -0
  68. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-compiled-truth-rejection.test.js.map +1 -0
  69. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +3 -3
  70. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +6 -6
  71. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
  72. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts +1 -1
  73. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +9 -3
  74. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
  75. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +2 -0
  76. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
  77. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
  78. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +6 -5
  79. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  80. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +30 -29
  81. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  82. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.d.ts +1 -1
  83. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js +4 -1
  84. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js.map +1 -1
  85. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
  86. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +1 -11
  87. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
  88. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts +27 -18
  89. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  90. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +83 -70
  91. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  92. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  93. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +27 -0
  94. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  95. package/payload/platform/plugins/memory/mcp/vitest.config.ts +2 -0
  96. package/payload/platform/plugins/memory/references/schema-base.md +7 -6
  97. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +12 -12
  98. package/payload/platform/plugins/scheduling/skills/daily-prep/SKILL.md +2 -2
  99. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  100. package/payload/platform/plugins/x-import/PLUGIN.md +3 -3
  101. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +7 -7
  102. package/payload/platform/scripts/seed-neo4j.sh +3 -3
  103. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  104. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +0 -1
  105. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  106. package/payload/platform/templates/agents/admin/IDENTITY.md +7 -7
  107. package/payload/platform/templates/specialists/agents/database-operator.md +6 -6
  108. package/payload/platform/templates/specialists/agents/librarian.md +2 -2
  109. package/payload/server/public/assets/{admin-_pm0RRTE.js → admin-CQMFJD81.js} +1 -1
  110. package/payload/server/public/assets/{data-b6q64Ot4.js → data-k-sUX-rP.js} +1 -1
  111. package/payload/server/public/assets/{graph-D1SVlBLb.js → graph-By7i71WM.js} +1 -1
  112. package/payload/server/public/assets/{graph-labels-BiDGzVs2.js → graph-labels-DemDBXA3.js} +1 -1
  113. package/payload/server/public/assets/{page-DwFigpyR.js → page-CHBpFW3X.js} +1 -1
  114. package/payload/server/public/assets/{page-BV6E4J2q.js → page-jqA-w9gF.js} +1 -1
  115. package/payload/server/public/data.html +3 -3
  116. package/payload/server/public/graph.html +3 -3
  117. package/payload/server/public/index.html +4 -4
  118. package/payload/server/server.js +29 -20
  119. package/payload/platform/plugins/admin/skills/commitment-followthrough/SKILL.md +0 -60
@@ -0,0 +1,544 @@
1
+ #!/usr/bin/env bash
2
+ # Task 425 — pre-turn-graph-pass UserPromptSubmit hook test suite.
3
+ #
4
+ # Supersedes the Task 419 Stop-hook test suite. A local Python HTTP server
5
+ # stands in for the admin server:
6
+ # POST /api/admin/log-ingest → records every line emit_log sent.
7
+ # GET /api/admin/post-turn-context → returns the contents of $CTX_FILE,
8
+ # or 500 if $CTX_FILE is missing/empty.
9
+ # Both kinds of request are appended to $REQ_LOG so assertions can see
10
+ # every POST body AND whether the GET was attempted at all.
11
+ #
12
+ # Contract under test:
13
+ # - Role/specialist gates: exit 0, single trigger-skipped log line, empty
14
+ # stdout (no additionalContext).
15
+ # - MAXY_UI_INTERNAL_PORT unset: exit 0, empty stdout. The hook emits its
16
+ # missing-env diagnostic on its own stderr channel because
17
+ # /api/admin/log-ingest is unreachable; loud-fail is acceptable.
18
+ # - Missing-transcript: exit 0, single trigger-skipped log line, empty
19
+ # stdout.
20
+ # - Dispatch path (admin + populated JSONL): exit 0, stdout is a JSON
21
+ # envelope `{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit",
22
+ # "additionalContext":"..."}}` whose additionalContext begins with
23
+ # `[system: pre-turn-graph-pass-hook]` and contains `<conversation>` +
24
+ # `<prior-writes>` blocks, with conversationId interpolated from
25
+ # CONVERSATION_NODE_ID.
26
+ # - Prior-writes fetch fails (context endpoint 500): exit 0, dispatch
27
+ # still fires, `<prior-writes>` block resolves to `(none)`.
28
+ # - CONVERSATION_NODE_ID unset: exit 0, additionalContext names
29
+ # `conversationId: <unset>`, and no GET to /api/admin/post-turn-context
30
+ # is attempted.
31
+ # - Multi-block assistant turn: conversation walk concatenates `text`
32
+ # blocks; `tool_use` blocks dropped.
33
+ # - User turn whose only content is `tool_result` blocks: skipped in
34
+ # the conversation walk.
35
+ # - UserPromptSubmit stdin-shape regression: a synthetic UserPromptSubmit
36
+ # payload produces the documented stdout envelope; a stale Stop-shaped
37
+ # payload (no hook_event_name, no prompt) also produces the
38
+ # UserPromptSubmit envelope because the hook keys on
39
+ # session_id + transcript_path, not on input shape sniffing.
40
+
41
+ set -u
42
+
43
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/pre-turn-graph-pass.sh"
44
+ if [[ ! -x "$HOOK" ]]; then
45
+ echo "FAIL: $HOOK not executable" >&2
46
+ exit 1
47
+ fi
48
+
49
+ OP_ID='aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb'
50
+ CONV_ID='conv-deadbeef-0001'
51
+
52
+ TMPFILES=()
53
+ LISTENER_PIDS=()
54
+ cleanup_test_state() {
55
+ for pid in "${LISTENER_PIDS[@]:-}"; do
56
+ if [[ -n "$pid" ]]; then
57
+ kill "$pid" 2>/dev/null || true
58
+ wait "$pid" 2>/dev/null || true
59
+ fi
60
+ done
61
+ for f in "${TMPFILES[@]:-}"; do
62
+ [[ -n "$f" ]] && rm -f "$f" 2>/dev/null || true
63
+ done
64
+ }
65
+ trap cleanup_test_state EXIT
66
+
67
+ PASS=0
68
+ FAIL=0
69
+ pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
70
+ fail() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
71
+
72
+ start_listener() {
73
+ REQ_LOG=$(mktemp); TMPFILES+=("$REQ_LOG")
74
+ CTX_FILE=$(mktemp); TMPFILES+=("$CTX_FILE")
75
+ : > "$CTX_FILE"
76
+ LISTENER_PORT=$((39700 + RANDOM % 100))
77
+ python3 - "$LISTENER_PORT" "$REQ_LOG" "$CTX_FILE" <<'PY' &
78
+ import sys, http.server, json
79
+ port = int(sys.argv[1])
80
+ log_path = sys.argv[2]
81
+ ctx_path = sys.argv[3]
82
+
83
+ class H(http.server.BaseHTTPRequestHandler):
84
+ def log_message(self, *a, **k): pass
85
+ def do_POST(self):
86
+ n = int(self.headers.get('Content-Length','0') or 0)
87
+ body = self.rfile.read(n).decode('utf-8','replace')
88
+ with open(log_path, 'a', encoding='utf-8') as f:
89
+ f.write('POST\t' + self.path + '\t' + body + '\n')
90
+ self.send_response(200)
91
+ self.send_header('Content-Type','application/json')
92
+ self.end_headers()
93
+ self.wfile.write(json.dumps({"ok": True}).encode('utf-8'))
94
+ def do_GET(self):
95
+ with open(log_path, 'a', encoding='utf-8') as f:
96
+ f.write('GET\t' + self.path + '\t\n')
97
+ if self.path.startswith('/api/admin/post-turn-context'):
98
+ try:
99
+ with open(ctx_path, 'r', encoding='utf-8') as f:
100
+ body = f.read()
101
+ except FileNotFoundError:
102
+ body = ''
103
+ if not body.strip():
104
+ self.send_response(500)
105
+ self.end_headers()
106
+ return
107
+ self.send_response(200)
108
+ self.send_header('Content-Type','application/json')
109
+ self.end_headers()
110
+ self.wfile.write(body.encode('utf-8'))
111
+ else:
112
+ self.send_response(404)
113
+ self.end_headers()
114
+
115
+ http.server.HTTPServer(('127.0.0.1', port), H).serve_forever()
116
+ PY
117
+ LISTENER_PIDS+=("$!")
118
+ for _ in $(seq 1 20); do
119
+ if curl -sS --max-time 1 -X POST "http://127.0.0.1:${LISTENER_PORT}/ping" -d '{}' >/dev/null 2>&1; then
120
+ break
121
+ fi
122
+ sleep 0.1
123
+ done
124
+ : > "$REQ_LOG"
125
+ }
126
+
127
+ run_hook() {
128
+ # Args: role, specialist, ui_port, conversation_node_id, stdin_json
129
+ local role="$1"; local specialist="$2"; local ui_port="$3"
130
+ local conv="$4"; local stdin_json="$5"
131
+ local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
132
+ local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
133
+ local env_cmd=(env)
134
+ env_cmd+=("MAXY_SESSION_ROLE=${role}")
135
+ env_cmd+=("MAXY_SPECIALIST=${specialist}")
136
+ if [[ -n "$ui_port" ]]; then
137
+ env_cmd+=("MAXY_UI_INTERNAL_PORT=${ui_port}")
138
+ fi
139
+ if [[ -n "$conv" ]]; then
140
+ env_cmd+=("CONVERSATION_NODE_ID=${conv}")
141
+ fi
142
+ env_cmd+=("ACCOUNT_ID=acct-test-0001")
143
+ printf '%s' "$stdin_json" | "${env_cmd[@]}" bash "$HOOK" >"$stdout_file" 2>"$stderr_file"
144
+ HOOK_RC=$?
145
+ HOOK_STDERR=$(cat "$stderr_file")
146
+ HOOK_STDOUT=$(cat "$stdout_file")
147
+ sleep 0.1
148
+ }
149
+
150
+ ingest_lines() {
151
+ grep -E '^POST\s+/api/admin/log-ingest\s+' "$REQ_LOG" 2>/dev/null | python3 -c '
152
+ import sys, json
153
+ for raw in sys.stdin:
154
+ try:
155
+ parts = raw.rstrip("\n").split("\t", 2)
156
+ if len(parts) < 3: continue
157
+ body = parts[2]
158
+ d = json.loads(body)
159
+ if isinstance(d, dict) and isinstance(d.get("line"), str):
160
+ print(d["line"])
161
+ except Exception:
162
+ pass
163
+ ' || true
164
+ }
165
+
166
+ context_get_count() {
167
+ grep -E '^GET\s+/api/admin/post-turn-context' "$REQ_LOG" 2>/dev/null | wc -l | tr -d ' '
168
+ }
169
+
170
+ additional_context() {
171
+ printf '%s' "$HOOK_STDOUT" | python3 -c '
172
+ import sys, json
173
+ try:
174
+ d = json.load(sys.stdin)
175
+ out = (d.get("hookSpecificOutput") or {}).get("additionalContext", "")
176
+ sys.stdout.write(out)
177
+ except Exception:
178
+ pass
179
+ '
180
+ }
181
+
182
+ envelope_for() {
183
+ python3 -c '
184
+ import json, sys
185
+ print(json.dumps({
186
+ "session_id": sys.argv[1],
187
+ "transcript_path": sys.argv[2],
188
+ "cwd": "/tmp",
189
+ "hook_event_name": "UserPromptSubmit",
190
+ "prompt": "test prompt body",
191
+ }))
192
+ ' "$1" "$2"
193
+ }
194
+
195
+ write_simple_transcript() {
196
+ local out="$1"; shift
197
+ python3 - "$out" "$@" <<'PY'
198
+ import sys, json
199
+ out = sys.argv[1]
200
+ records = []
201
+ i = 2
202
+ while i + 1 < len(sys.argv):
203
+ u, a = sys.argv[i], sys.argv[i+1]
204
+ records.append({
205
+ "type": "user",
206
+ "timestamp": "2026-05-26T10:00:00.000Z",
207
+ "message": {"role": "user", "content": u},
208
+ })
209
+ records.append({
210
+ "type": "assistant",
211
+ "timestamp": "2026-05-26T10:00:01.000Z",
212
+ "message": {
213
+ "id": "msg_a",
214
+ "role": "assistant",
215
+ "content": [{"type": "text", "text": a}],
216
+ },
217
+ })
218
+ i += 2
219
+ with open(out, "w", encoding="utf-8") as f:
220
+ for r in records:
221
+ f.write(json.dumps(r) + "\n")
222
+ PY
223
+ }
224
+
225
+ DIRECTIVE_TOKEN='[system: pre-turn-graph-pass-hook]'
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # Case 1: role != admin → trigger-skipped reason=role-not-admin
229
+ # ---------------------------------------------------------------------------
230
+ start_listener
231
+ TRANSCRIPT_BASIC=$(mktemp); TMPFILES+=("$TRANSCRIPT_BASIC")
232
+ write_simple_transcript "$TRANSCRIPT_BASIC" "hello" "hi there"
233
+ ENV_BASIC=$(envelope_for "$OP_ID" "$TRANSCRIPT_BASIC")
234
+ : > "$REQ_LOG"
235
+ run_hook "public" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
236
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-1 rc=$HOOK_RC"
237
+ if [[ -n "$HOOK_STDOUT" ]]; then
238
+ fail "case-1 stdout must be empty on role-not-admin, got: $HOOK_STDOUT"
239
+ fi
240
+ if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=role-not-admin$"; then
241
+ pass "case-1 role=public → trigger-skipped reason=role-not-admin"
242
+ else
243
+ fail "case-1 expected role-not-admin, got: $(ingest_lines)"
244
+ fi
245
+
246
+ # ---------------------------------------------------------------------------
247
+ # Case 2: MAXY_SPECIALIST set → trigger-skipped reason=is-specialist
248
+ # ---------------------------------------------------------------------------
249
+ : > "$REQ_LOG"
250
+ run_hook "admin" "database-operator" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
251
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-2 rc=$HOOK_RC"
252
+ if [[ -n "$HOOK_STDOUT" ]]; then
253
+ fail "case-2 stdout must be empty on is-specialist, got: $HOOK_STDOUT"
254
+ fi
255
+ if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=is-specialist specialist=database-operator$"; then
256
+ pass "case-2 specialist=database-operator → trigger-skipped reason=is-specialist"
257
+ else
258
+ fail "case-2 expected is-specialist, got: $(ingest_lines)"
259
+ fi
260
+
261
+ # ---------------------------------------------------------------------------
262
+ # Case 3: MAXY_UI_INTERNAL_PORT unset → exit 0, empty stdout, loud stderr.
263
+ # ---------------------------------------------------------------------------
264
+ : > "$REQ_LOG"
265
+ run_hook "admin" "" "" "$CONV_ID" "$ENV_BASIC"
266
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-3 rc=$HOOK_RC"
267
+ if [[ -n "$HOOK_STDOUT" ]]; then
268
+ fail "case-3 stdout must be empty when MAXY_UI_INTERNAL_PORT unset"
269
+ fi
270
+ if [[ "$(ingest_lines | wc -l | tr -d ' ')" == "0" ]]; then
271
+ pass "case-3 missing MAXY_UI_INTERNAL_PORT → exit 0, no dispatch"
272
+ else
273
+ fail "case-3 expected no ingest lines (port unset), got: $(ingest_lines)"
274
+ fi
275
+
276
+ # ---------------------------------------------------------------------------
277
+ # Case 4: transcript_path missing on disk → trigger-skipped reason=missing-transcript
278
+ # ---------------------------------------------------------------------------
279
+ MISSING_PATH="/tmp/pre-turn-graph-pass-does-not-exist-$$.jsonl"
280
+ rm -f "$MISSING_PATH"
281
+ ENV_MISSING=$(envelope_for "$OP_ID" "$MISSING_PATH")
282
+ : > "$REQ_LOG"
283
+ run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_MISSING"
284
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-4 rc=$HOOK_RC"
285
+ if [[ -n "$HOOK_STDOUT" ]]; then
286
+ fail "case-4 stdout must be empty on missing-transcript"
287
+ fi
288
+ if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=missing-transcript$"; then
289
+ pass "case-4 missing transcript_path → trigger-skipped reason=missing-transcript"
290
+ else
291
+ fail "case-4 expected missing-transcript, got: $(ingest_lines)"
292
+ fi
293
+
294
+ # ---------------------------------------------------------------------------
295
+ # Case 5: Dispatch path → exit 0, UserPromptSubmit JSON envelope, blocks
296
+ # interpolated, dispatch log emitted.
297
+ # ---------------------------------------------------------------------------
298
+ cat > "$CTX_FILE" <<'JSON'
299
+ {"writes":[{"elementId":"4:abc:1","labels":["KnowledgeDocument"],"properties":{"title":"prior note"}}]}
300
+ JSON
301
+ : > "$REQ_LOG"
302
+ run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
303
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-5 expected rc=0, got $HOOK_RC"
304
+ STDOUT_OK=$(printf '%s' "$HOOK_STDOUT" | python3 -c '
305
+ import sys, json
306
+ try:
307
+ d = json.load(sys.stdin)
308
+ out = d["hookSpecificOutput"]
309
+ assert out["hookEventName"] == "UserPromptSubmit"
310
+ assert isinstance(out["additionalContext"], str)
311
+ print("OK")
312
+ except Exception as e:
313
+ print(f"BAD: {e}")
314
+ ')
315
+ if [[ "$STDOUT_OK" != "OK" ]]; then
316
+ fail "case-5 stdout envelope malformed: $STDOUT_OK"
317
+ fi
318
+ CTX_BODY=$(additional_context)
319
+ if ! printf '%s' "$CTX_BODY" | head -n 1 | grep -qF "$DIRECTIVE_TOKEN"; then
320
+ fail "case-5 additionalContext must begin with $DIRECTIVE_TOKEN"
321
+ fi
322
+ if ! printf '%s' "$CTX_BODY" | grep -q '<conversation>'; then
323
+ fail "case-5 additionalContext missing <conversation> block"
324
+ fi
325
+ if ! printf '%s' "$CTX_BODY" | grep -q '<prior-writes>'; then
326
+ fail "case-5 additionalContext missing <prior-writes> block"
327
+ fi
328
+ if ! printf '%s' "$CTX_BODY" | grep -q "conversationId: ${CONV_ID}"; then
329
+ fail "case-5 additionalContext must interpolate conversationId=${CONV_ID}"
330
+ fi
331
+ if ! printf '%s' "$CTX_BODY" | grep -q '\[user: hello\]'; then
332
+ fail "case-5 additionalContext must carry user turn '[user: hello]'"
333
+ fi
334
+ if ! printf '%s' "$CTX_BODY" | grep -q '\[assistant: hi there\]'; then
335
+ fail "case-5 additionalContext must carry assistant turn '[assistant: hi there]'"
336
+ fi
337
+ if ! printf '%s' "$CTX_BODY" | grep -q 'KnowledgeDocument'; then
338
+ fail "case-5 additionalContext <prior-writes> must include fetched labels"
339
+ fi
340
+ if ingest_lines | grep -qE "^dispatch sessionId=${OP_ID} conversationId=${CONV_ID} "; then
341
+ pass "case-5 dispatch path → exit 0 + JSON envelope with conversation, prior-writes, conversationId"
342
+ else
343
+ fail "case-5 expected dispatch log line, got: $(ingest_lines)"
344
+ fi
345
+
346
+ # ---------------------------------------------------------------------------
347
+ # Case 6: Prior-writes fetch fails (500) → exit 0, prior-writes=(none).
348
+ # ---------------------------------------------------------------------------
349
+ : > "$CTX_FILE"
350
+ : > "$REQ_LOG"
351
+ run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
352
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-6 expected rc=0, got $HOOK_RC"
353
+ CTX_BODY=$(additional_context)
354
+ PRIOR_BODY=$(printf '%s' "$CTX_BODY" | awk '/<prior-writes>/{flag=1;next}/<\/prior-writes>/{flag=0}flag' | tr -d '[:space:]')
355
+ if [[ "$PRIOR_BODY" == "(none)" ]]; then
356
+ pass "case-6 context endpoint 500 → dispatch still fires, prior-writes = (none)"
357
+ else
358
+ fail "case-6 expected prior-writes=(none), got [$PRIOR_BODY]"
359
+ fi
360
+
361
+ # ---------------------------------------------------------------------------
362
+ # Case 7: CONVERSATION_NODE_ID unset → conversationId:<unset>, no context GET.
363
+ # ---------------------------------------------------------------------------
364
+ cat > "$CTX_FILE" <<'JSON'
365
+ {"writes":[]}
366
+ JSON
367
+ : > "$REQ_LOG"
368
+ run_hook "admin" "" "$LISTENER_PORT" "" "$ENV_BASIC"
369
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-7 expected rc=0, got $HOOK_RC"
370
+ CTX_BODY=$(additional_context)
371
+ if ! printf '%s' "$CTX_BODY" | grep -q 'conversationId: <unset>'; then
372
+ fail "case-7 additionalContext must contain 'conversationId: <unset>'"
373
+ fi
374
+ GET_COUNT=$(context_get_count)
375
+ if [[ "$GET_COUNT" == "0" ]]; then
376
+ pass "case-7 CONVERSATION_NODE_ID unset → no GET /api/admin/post-turn-context attempted"
377
+ else
378
+ fail "case-7 expected zero context GETs, got: $GET_COUNT"
379
+ fi
380
+
381
+ # ---------------------------------------------------------------------------
382
+ # Case 8: Multi-block assistant turn → text concatenated, tool_use dropped.
383
+ # ---------------------------------------------------------------------------
384
+ T_MULTI=$(mktemp); TMPFILES+=("$T_MULTI")
385
+ python3 - "$T_MULTI" <<'PY'
386
+ import sys, json
387
+ out = sys.argv[1]
388
+ records = [
389
+ {
390
+ "type": "user",
391
+ "timestamp": "2026-05-26T10:00:00.000Z",
392
+ "message": {"role": "user", "content": "go"},
393
+ },
394
+ {
395
+ "type": "assistant",
396
+ "timestamp": "2026-05-26T10:00:01.000Z",
397
+ "message": {
398
+ "id": "msg_a",
399
+ "role": "assistant",
400
+ "content": [
401
+ {"type": "text", "text": "part-one "},
402
+ {"type": "tool_use", "id": "tu_1", "name": "Read", "input": {}},
403
+ {"type": "text", "text": "part-two"},
404
+ ],
405
+ },
406
+ },
407
+ ]
408
+ with open(out, "w", encoding="utf-8") as f:
409
+ for r in records:
410
+ f.write(json.dumps(r) + "\n")
411
+ PY
412
+ ENV_MULTI=$(envelope_for "$OP_ID" "$T_MULTI")
413
+ cat > "$CTX_FILE" <<'JSON'
414
+ {"writes":[]}
415
+ JSON
416
+ : > "$REQ_LOG"
417
+ run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_MULTI"
418
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-8 expected rc=0, got $HOOK_RC"
419
+ CTX_BODY=$(additional_context)
420
+ if ! printf '%s' "$CTX_BODY" | grep -q '\[assistant: part-one part-two\]'; then
421
+ fail "case-8 expected concatenated assistant text '[assistant: part-one part-two]'"
422
+ fi
423
+ if printf '%s' "$CTX_BODY" | grep -q '"tool_use"'; then
424
+ fail "case-8 tool_use block must not appear in additionalContext"
425
+ fi
426
+ if printf '%s' "$CTX_BODY" | grep -q '\[assistant: part-one part-two\]'; then
427
+ pass "case-8 multi-block assistant turn → text concatenated, tool_use dropped"
428
+ fi
429
+
430
+ # ---------------------------------------------------------------------------
431
+ # Case 9: User turn with only tool_result blocks → skipped in conversation walk.
432
+ # ---------------------------------------------------------------------------
433
+ T_TRONLY=$(mktemp); TMPFILES+=("$T_TRONLY")
434
+ python3 - "$T_TRONLY" <<'PY'
435
+ import sys, json
436
+ out = sys.argv[1]
437
+ records = [
438
+ {
439
+ "type": "user",
440
+ "timestamp": "2026-05-26T10:00:00.000Z",
441
+ "message": {"role": "user", "content": "real user turn"},
442
+ },
443
+ {
444
+ "type": "assistant",
445
+ "timestamp": "2026-05-26T10:00:01.000Z",
446
+ "message": {
447
+ "id": "msg_a",
448
+ "role": "assistant",
449
+ "content": [{"type": "text", "text": "assistant reply"}],
450
+ },
451
+ },
452
+ {
453
+ "type": "user",
454
+ "timestamp": "2026-05-26T10:00:02.000Z",
455
+ "message": {
456
+ "role": "user",
457
+ "content": [
458
+ {"type": "tool_result", "tool_use_id": "tu_x", "content": "tool plumbing payload"},
459
+ ],
460
+ },
461
+ },
462
+ ]
463
+ with open(out, "w", encoding="utf-8") as f:
464
+ for r in records:
465
+ f.write(json.dumps(r) + "\n")
466
+ PY
467
+ ENV_TRONLY=$(envelope_for "$OP_ID" "$T_TRONLY")
468
+ : > "$REQ_LOG"
469
+ run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_TRONLY"
470
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-9 expected rc=0, got $HOOK_RC"
471
+ CTX_BODY=$(additional_context)
472
+ CONV_BLOCK=$(printf '%s' "$CTX_BODY" | awk '/<conversation>/{flag=1;next}/<\/conversation>/{flag=0}flag')
473
+ if ! printf '%s' "$CONV_BLOCK" | grep -q '\[user: real user turn\]'; then
474
+ fail "case-9 conversation block must include real user turn"
475
+ fi
476
+ if printf '%s' "$CONV_BLOCK" | grep -q 'tool plumbing payload'; then
477
+ fail "case-9 tool_result-only user record must not appear in conversation walk"
478
+ fi
479
+ TR_LINES=$(printf '%s\n' "$CONV_BLOCK" | grep -cE '\[user:' || true)
480
+ if [[ "$TR_LINES" == "1" ]]; then
481
+ pass "case-9 tool_result-only user record skipped in conversation walk"
482
+ else
483
+ fail "case-9 expected exactly one [user: ...] line, got $TR_LINES"
484
+ fi
485
+
486
+ # ---------------------------------------------------------------------------
487
+ # Case 10: UserPromptSubmit shape — directive on stdout only, never stderr;
488
+ # stdout envelope has hookEventName="UserPromptSubmit".
489
+ # ---------------------------------------------------------------------------
490
+ cat > "$CTX_FILE" <<'JSON'
491
+ {"writes":[]}
492
+ JSON
493
+ : > "$REQ_LOG"
494
+ run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
495
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-10 expected rc=0, got $HOOK_RC"
496
+ if printf '%s' "$HOOK_STDERR" | grep -qF "$DIRECTIVE_TOKEN"; then
497
+ fail "case-10 directive must not appear on stderr (UserPromptSubmit contract is stdout JSON)"
498
+ fi
499
+ HEN=$(printf '%s' "$HOOK_STDOUT" | python3 -c '
500
+ import sys, json
501
+ try:
502
+ d = json.load(sys.stdin)
503
+ sys.stdout.write(d["hookSpecificOutput"]["hookEventName"])
504
+ except Exception:
505
+ pass
506
+ ')
507
+ if [[ "$HEN" == "UserPromptSubmit" ]]; then
508
+ pass "case-10 stdout envelope is well-formed JSON with hookEventName=UserPromptSubmit"
509
+ else
510
+ fail "case-10 expected hookEventName=UserPromptSubmit, got [$HEN]"
511
+ fi
512
+
513
+ # ---------------------------------------------------------------------------
514
+ # Case 11: Stop-shaped stdin regression — a CC Stop-hook payload carries
515
+ # hook_event_name="Stop". The hook must not silently emit a
516
+ # UserPromptSubmit envelope on a Stop fire. Expect exit 0 with
517
+ # empty stdout and a `wrong-hook-event` skip line.
518
+ # ---------------------------------------------------------------------------
519
+ ENV_STOP_SHAPED=$(python3 -c '
520
+ import json, sys
521
+ print(json.dumps({
522
+ "session_id": sys.argv[1],
523
+ "transcript_path": sys.argv[2],
524
+ "hook_event_name": "Stop",
525
+ }))
526
+ ' "$OP_ID" "$TRANSCRIPT_BASIC")
527
+ : > "$REQ_LOG"
528
+ run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_STOP_SHAPED"
529
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-11 expected rc=0, got $HOOK_RC"
530
+ if [[ -n "$HOOK_STDOUT" ]]; then
531
+ fail "case-11 stdout must be empty on wrong-hook-event Stop, got: $HOOK_STDOUT"
532
+ fi
533
+ if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=wrong-hook-event event=Stop$"; then
534
+ pass "case-11 Stop-shaped stdin rejected with wrong-hook-event skip"
535
+ else
536
+ fail "case-11 expected wrong-hook-event skip, got: $(ingest_lines)"
537
+ fi
538
+
539
+ # ---------------------------------------------------------------------------
540
+ # Summary
541
+ # ---------------------------------------------------------------------------
542
+ echo
543
+ echo "pre-turn-graph-pass tests: $PASS passed, $FAIL failed"
544
+ [[ "$FAIL" -eq 0 ]]
@@ -294,7 +294,7 @@ write_transcript "$T_RELEASE" \
294
294
  "assistant|hello|" \
295
295
  "user|/end|" \
296
296
  "assistant|running retrospective|" \
297
- "assistant||tool_use:session-retrospective-mark-complete"
297
+ "assistant||tool_use:mcp__admin__session-retrospective-mark-complete"
298
298
  ENV_REL=$(envelope_for "$OP_ID" "$T_RELEASE")
299
299
  : > "$REQ_LOG"
300
300
  run_hook "admin" "" "$ENV_REL"
@@ -325,7 +325,7 @@ fi
325
325
  T_OTHER=$(mktemp); TMPFILES+=("$T_OTHER")
326
326
  write_transcript "$T_OTHER" \
327
327
  "user|/end|" \
328
- "assistant||tool_use:session-retrospective-mark-complete"
328
+ "assistant||tool_use:mcp__admin__session-retrospective-mark-complete"
329
329
  # Active gate inspects only T_ACTIVE (no sentinel inside).
330
330
  T_ACTIVE=$(mktemp); TMPFILES+=("$T_ACTIVE")
331
331
  write_transcript "$T_ACTIVE" "user|/end|"
@@ -8,7 +8,7 @@
8
8
  # blocking the legacy MCP deviation tools mechanically. The retired
9
9
  # `whatsapp-export-insight-pass` tool is replaced by Phase 2:
10
10
  # `mcp__memory__conversation-archive-derive-insights` — a read-only tool that
11
- # walks :Section chunks of one named conversation-document :KnowledgeDocument in
11
+ # walks :Section chunks of one named :ConversationArchive in
12
12
  # pages and emits per-row proposals. The new tool is NOT in any BLOCK list
13
13
  # (the gate is allow-by-default for unrecognised tools) — its writes go
14
14
  # through the existing graph-cypher-write surface, gated by the operator per
@@ -154,7 +154,7 @@ fi
154
154
  case "$TOOL_NAME" in
155
155
  mcp__memory__whatsapp-export-parse|mcp__memory__whatsapp-export-insight-write|mcp__memory__whatsapp-export-insight-pass|mcp__memory__whatsapp-export-preview)
156
156
  emit_decision "block" "denied-mcp-legacy" \
157
- "Blocked: ${TOOL_NAME} is a retired path. generalised conversation-archive ingest — invoke 'bash platform/plugins/memory/bin/conversation-archive-ingest.sh <archive> --source whatsapp --owner-element-id <id> --participant-person-ids <csv> --scope <admin|public>' once. Normalise, sessionize, classify (mode=chat), and memory-ingest (writes :KnowledgeDocument keyed on conversationIdentity, source=<enum>) all run in-process."
157
+ "Blocked: ${TOOL_NAME} is a retired path. generalised conversation-archive ingest — invoke 'bash platform/plugins/memory/bin/conversation-archive-ingest.sh <archive> --source whatsapp --owner-element-id <id> --participant-person-ids <csv> --scope <admin|public>' once. Normalise, sessionize, classify (mode=chat), and memory-ingest (writes :ConversationArchive keyed on conversationIdentity, source=<enum>) all run in-process."
158
158
  ;;
159
159
  esac
160
160