@rubytech/create-maxy-code 0.1.148 → 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 (76) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-style/dist/index.d.ts +5 -5
  3. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/graph-style/dist/index.js +11 -9
  5. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  6. package/payload/platform/lib/graph-style/src/index.ts +11 -9
  7. package/payload/platform/lib/graph-trash/dist/index.js +6 -6
  8. package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
  9. package/payload/platform/lib/graph-trash/src/index.ts +6 -6
  10. package/payload/platform/neo4j/edge-annotations.json +4 -4
  11. package/payload/platform/neo4j/schema.cypher +106 -45
  12. package/payload/platform/plugins/admin/PLUGIN.md +1 -1
  13. package/payload/platform/plugins/admin/hooks/__tests__/{post-turn-graph-pass.test.sh → pre-turn-graph-pass.test.sh} +184 -112
  14. package/payload/platform/plugins/admin/hooks/archive-ingest-surface-gate.sh +2 -2
  15. package/payload/platform/plugins/admin/hooks/{post-turn-graph-pass.sh → pre-turn-graph-pass.sh} +83 -53
  16. package/payload/platform/plugins/docs/references/linkedin-extension.md +1 -1
  17. package/payload/platform/plugins/docs/references/platform.md +1 -1
  18. package/payload/platform/plugins/docs/references/plugins-guide.md +2 -2
  19. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +5 -5
  20. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  21. package/payload/platform/plugins/memory/PLUGIN.md +4 -4
  22. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.mjs +5 -5
  23. package/payload/platform/plugins/memory/bin/conversation-archive-ingest.sh +1 -1
  24. package/payload/platform/plugins/memory/mcp/dist/index.js +14 -14
  25. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  26. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts +1 -1
  27. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js +2 -2
  28. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts +1 -1
  29. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js +1 -1
  30. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts +2 -2
  31. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
  32. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +3 -4
  33. package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
  34. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.d.ts.map +1 -1
  35. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js +6 -0
  36. package/payload/platform/plugins/memory/mcp/dist/lib/typed-edge-schema.js.map +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js +2 -2
  38. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-derive-insights.test.js.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js +17 -17
  40. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest.test.js.map +1 -1
  41. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts +3 -3
  42. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +6 -6
  43. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
  44. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts +1 -1
  45. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +9 -3
  46. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
  47. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts +2 -0
  48. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
  49. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
  50. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +6 -5
  51. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  52. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +30 -29
  53. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  54. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.d.ts +1 -1
  55. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js +4 -1
  56. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js.map +1 -1
  57. package/payload/platform/plugins/memory/references/schema-base.md +2 -2
  58. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +12 -12
  59. package/payload/platform/plugins/scheduling/skills/daily-prep/SKILL.md +2 -2
  60. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  61. package/payload/platform/plugins/x-import/PLUGIN.md +3 -3
  62. package/payload/platform/plugins/x-import/skills/x-import/SKILL.md +7 -7
  63. package/payload/platform/scripts/seed-neo4j.sh +3 -3
  64. package/payload/platform/templates/agents/admin/IDENTITY.md +7 -7
  65. package/payload/platform/templates/specialists/agents/database-operator.md +6 -6
  66. package/payload/platform/templates/specialists/agents/librarian.md +2 -2
  67. package/payload/server/public/assets/{admin-_pm0RRTE.js → admin-CQMFJD81.js} +1 -1
  68. package/payload/server/public/assets/{data-b6q64Ot4.js → data-k-sUX-rP.js} +1 -1
  69. package/payload/server/public/assets/{graph-D1SVlBLb.js → graph-By7i71WM.js} +1 -1
  70. package/payload/server/public/assets/{graph-labels-BiDGzVs2.js → graph-labels-DemDBXA3.js} +1 -1
  71. package/payload/server/public/assets/{page-DwFigpyR.js → page-CHBpFW3X.js} +1 -1
  72. package/payload/server/public/assets/{page-BV6E4J2q.js → page-jqA-w9gF.js} +1 -1
  73. package/payload/server/public/data.html +3 -3
  74. package/payload/server/public/graph.html +3 -3
  75. package/payload/server/public/index.html +4 -4
  76. package/payload/server/server.js +20 -18
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bash
2
- # Task 419post-turn-graph-pass Stop hook test suite.
2
+ # Task 425pre-turn-graph-pass UserPromptSubmit hook test suite.
3
3
  #
4
- # Modeled on session-end-retrospective.test.sh. A local Python HTTP server
4
+ # Supersedes the Task 419 Stop-hook test suite. A local Python HTTP server
5
5
  # stands in for the admin server:
6
6
  # POST /api/admin/log-ingest → records every line emit_log sent.
7
7
  # GET /api/admin/post-turn-context → returns the contents of $CTX_FILE,
@@ -10,30 +10,37 @@
10
10
  # every POST body AND whether the GET was attempted at all.
11
11
  #
12
12
  # Contract under test:
13
- # - Role/specialist gates: exit 0, single trigger-skipped log line, no
14
- # stderr directive.
15
- # - MAXY_UI_INTERNAL_PORT unset: exit 0, no stderr directive. The hook
16
- # emits its missing-env diagnostic on its own stderr channel because
17
- # /api/admin/log-ingest is unreachable; that loud-fail is acceptable.
18
- # - Empty stdin / missing-transcript: exit 0, single trigger-skipped log
19
- # line, no stderr directive.
20
- # - Dispatch path (admin + populated JSONL): exit 2, stderr begins with
21
- # `[system: post-turn-graph-pass-stop-hook]` and contains
22
- # `<conversation>` + `<prior-writes>` blocks, with conversationId
23
- # interpolated from CONVERSATION_NODE_ID.
24
- # - Prior-writes fetch fails (context endpoint 500): exit 2, dispatch
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
25
27
  # still fires, `<prior-writes>` block resolves to `(none)`.
26
- # - CONVERSATION_NODE_ID unset: exit 2, prompt names
28
+ # - CONVERSATION_NODE_ID unset: exit 0, additionalContext names
27
29
  # `conversationId: <unset>`, and no GET to /api/admin/post-turn-context
28
30
  # is attempted.
29
31
  # - Multi-block assistant turn: conversation walk concatenates `text`
30
32
  # blocks; `tool_use` blocks dropped.
31
33
  # - User turn whose only content is `tool_result` blocks: skipped in
32
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.
33
40
 
34
41
  set -u
35
42
 
36
- HOOK="$(cd "$(dirname "$0")/.." && pwd)/post-turn-graph-pass.sh"
43
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/pre-turn-graph-pass.sh"
37
44
  if [[ ! -x "$HOOK" ]]; then
38
45
  echo "FAIL: $HOOK not executable" >&2
39
46
  exit 1
@@ -160,15 +167,32 @@ context_get_count() {
160
167
  grep -E '^GET\s+/api/admin/post-turn-context' "$REQ_LOG" 2>/dev/null | wc -l | tr -d ' '
161
168
  }
162
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
+
163
182
  envelope_for() {
164
183
  python3 -c '
165
184
  import json, sys
166
- print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
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
+ }))
167
192
  ' "$1" "$2"
168
193
  }
169
194
 
170
195
  write_simple_transcript() {
171
- # write_simple_transcript <outfile> <user_text> <assistant_text>
172
196
  local out="$1"; shift
173
197
  python3 - "$out" "$@" <<'PY'
174
198
  import sys, json
@@ -179,12 +203,12 @@ while i + 1 < len(sys.argv):
179
203
  u, a = sys.argv[i], sys.argv[i+1]
180
204
  records.append({
181
205
  "type": "user",
182
- "timestamp": "2026-05-24T10:00:00.000Z",
206
+ "timestamp": "2026-05-26T10:00:00.000Z",
183
207
  "message": {"role": "user", "content": u},
184
208
  })
185
209
  records.append({
186
210
  "type": "assistant",
187
- "timestamp": "2026-05-24T10:00:01.000Z",
211
+ "timestamp": "2026-05-26T10:00:01.000Z",
188
212
  "message": {
189
213
  "id": "msg_a",
190
214
  "role": "assistant",
@@ -198,8 +222,10 @@ with open(out, "w", encoding="utf-8") as f:
198
222
  PY
199
223
  }
200
224
 
225
+ DIRECTIVE_TOKEN='[system: pre-turn-graph-pass-hook]'
226
+
201
227
  # ---------------------------------------------------------------------------
202
- # Case 1: role != admin → trigger-skipped reason=role-not-admin, no directive
228
+ # Case 1: role != admin → trigger-skipped reason=role-not-admin
203
229
  # ---------------------------------------------------------------------------
204
230
  start_listener
205
231
  TRANSCRIPT_BASIC=$(mktemp); TMPFILES+=("$TRANSCRIPT_BASIC")
@@ -208,8 +234,8 @@ ENV_BASIC=$(envelope_for "$OP_ID" "$TRANSCRIPT_BASIC")
208
234
  : > "$REQ_LOG"
209
235
  run_hook "public" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
210
236
  [[ "$HOOK_RC" -eq 0 ]] || fail "case-1 rc=$HOOK_RC"
211
- if printf '%s' "$HOOK_STDERR" | grep -q '\[system: post-turn-graph-pass-stop-hook\]'; then
212
- fail "case-1 stderr must not contain dispatch directive"
237
+ if [[ -n "$HOOK_STDOUT" ]]; then
238
+ fail "case-1 stdout must be empty on role-not-admin, got: $HOOK_STDOUT"
213
239
  fi
214
240
  if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=role-not-admin$"; then
215
241
  pass "case-1 role=public → trigger-skipped reason=role-not-admin"
@@ -223,8 +249,8 @@ fi
223
249
  : > "$REQ_LOG"
224
250
  run_hook "admin" "database-operator" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
225
251
  [[ "$HOOK_RC" -eq 0 ]] || fail "case-2 rc=$HOOK_RC"
226
- if printf '%s' "$HOOK_STDERR" | grep -q '\[system: post-turn-graph-pass-stop-hook\]'; then
227
- fail "case-2 stderr must not contain dispatch directive"
252
+ if [[ -n "$HOOK_STDOUT" ]]; then
253
+ fail "case-2 stdout must be empty on is-specialist, got: $HOOK_STDOUT"
228
254
  fi
229
255
  if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=is-specialist specialist=database-operator$"; then
230
256
  pass "case-2 specialist=database-operator → trigger-skipped reason=is-specialist"
@@ -233,15 +259,13 @@ else
233
259
  fi
234
260
 
235
261
  # ---------------------------------------------------------------------------
236
- # Case 3: MAXY_UI_INTERNAL_PORT unset → exit 0, no dispatch directive.
237
- # The hook prints a missing-env line on its own stderr (loud-fail);
238
- # that's expected — /log-ingest is unreachable without the port.
262
+ # Case 3: MAXY_UI_INTERNAL_PORT unset → exit 0, empty stdout, loud stderr.
239
263
  # ---------------------------------------------------------------------------
240
264
  : > "$REQ_LOG"
241
265
  run_hook "admin" "" "" "$CONV_ID" "$ENV_BASIC"
242
266
  [[ "$HOOK_RC" -eq 0 ]] || fail "case-3 rc=$HOOK_RC"
243
- if printf '%s' "$HOOK_STDERR" | grep -q '\[system: post-turn-graph-pass-stop-hook\]'; then
244
- fail "case-3 stderr must not contain dispatch directive"
267
+ if [[ -n "$HOOK_STDOUT" ]]; then
268
+ fail "case-3 stdout must be empty when MAXY_UI_INTERNAL_PORT unset"
245
269
  fi
246
270
  if [[ "$(ingest_lines | wc -l | tr -d ' ')" == "0" ]]; then
247
271
  pass "case-3 missing MAXY_UI_INTERNAL_PORT → exit 0, no dispatch"
@@ -250,117 +274,112 @@ else
250
274
  fi
251
275
 
252
276
  # ---------------------------------------------------------------------------
253
- # Case 4: empty stdin → trigger-skipped reason=empty-stdin
254
- # ---------------------------------------------------------------------------
255
- : > "$REQ_LOG"
256
- run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" ""
257
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-4 rc=$HOOK_RC"
258
- if printf '%s' "$HOOK_STDERR" | grep -q '\[system: post-turn-graph-pass-stop-hook\]'; then
259
- fail "case-4 stderr must not contain dispatch directive"
260
- fi
261
- if ingest_lines | grep -qE '^trigger-skipped sessionId=unknown reason=empty-stdin$'; then
262
- pass "case-4 empty stdin → trigger-skipped reason=empty-stdin"
263
- else
264
- fail "case-4 expected empty-stdin, got: $(ingest_lines)"
265
- fi
266
-
267
- # ---------------------------------------------------------------------------
268
- # Case 5: transcript_path missing on disk → trigger-skipped reason=missing-transcript
277
+ # Case 4: transcript_path missing on disk → trigger-skipped reason=missing-transcript
269
278
  # ---------------------------------------------------------------------------
270
- MISSING_PATH="/tmp/post-turn-graph-pass-does-not-exist-$$.jsonl"
279
+ MISSING_PATH="/tmp/pre-turn-graph-pass-does-not-exist-$$.jsonl"
271
280
  rm -f "$MISSING_PATH"
272
281
  ENV_MISSING=$(envelope_for "$OP_ID" "$MISSING_PATH")
273
282
  : > "$REQ_LOG"
274
283
  run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_MISSING"
275
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-5 rc=$HOOK_RC"
276
- if printf '%s' "$HOOK_STDERR" | grep -q '\[system: post-turn-graph-pass-stop-hook\]'; then
277
- fail "case-5 stderr must not contain dispatch directive"
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"
278
287
  fi
279
288
  if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=missing-transcript$"; then
280
- pass "case-5 missing transcript_path → trigger-skipped reason=missing-transcript"
289
+ pass "case-4 missing transcript_path → trigger-skipped reason=missing-transcript"
281
290
  else
282
- fail "case-5 expected missing-transcript, got: $(ingest_lines)"
291
+ fail "case-4 expected missing-transcript, got: $(ingest_lines)"
283
292
  fi
284
293
 
285
294
  # ---------------------------------------------------------------------------
286
- # Case 6: Dispatch path admin + populated JSONL → exit 2, dispatch directive
287
- # on stderr with <conversation> + <prior-writes>, conversationId interpolated.
295
+ # Case 5: Dispatch path exit 0, UserPromptSubmit JSON envelope, blocks
296
+ # interpolated, dispatch log emitted.
288
297
  # ---------------------------------------------------------------------------
289
298
  cat > "$CTX_FILE" <<'JSON'
290
299
  {"writes":[{"elementId":"4:abc:1","labels":["KnowledgeDocument"],"properties":{"title":"prior note"}}]}
291
300
  JSON
292
301
  : > "$REQ_LOG"
293
302
  run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
294
- [[ "$HOOK_RC" -eq 2 ]] || fail "case-6 expected rc=2, got $HOOK_RC"
295
- if ! printf '%s' "$HOOK_STDERR" | head -n 1 | grep -q '^\[system: post-turn-graph-pass-stop-hook\]'; then
296
- fail "case-6 stderr must begin with [system: post-turn-graph-pass-stop-hook]"
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"
297
317
  fi
298
- if ! printf '%s' "$HOOK_STDERR" | grep -q '<conversation>'; then
299
- fail "case-6 stderr missing <conversation> block"
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"
300
321
  fi
301
- if ! printf '%s' "$HOOK_STDERR" | grep -q '<prior-writes>'; then
302
- fail "case-6 stderr missing <prior-writes> block"
322
+ if ! printf '%s' "$CTX_BODY" | grep -q '<conversation>'; then
323
+ fail "case-5 additionalContext missing <conversation> block"
303
324
  fi
304
- if ! printf '%s' "$HOOK_STDERR" | grep -q "conversationId: ${CONV_ID}"; then
305
- fail "case-6 stderr must interpolate conversationId=${CONV_ID}"
325
+ if ! printf '%s' "$CTX_BODY" | grep -q '<prior-writes>'; then
326
+ fail "case-5 additionalContext missing <prior-writes> block"
306
327
  fi
307
- if ! printf '%s' "$HOOK_STDERR" | grep -q '\[user: hello\]'; then
308
- fail "case-6 stderr must carry the user turn '[user: hello]'"
328
+ if ! printf '%s' "$CTX_BODY" | grep -q "conversationId: ${CONV_ID}"; then
329
+ fail "case-5 additionalContext must interpolate conversationId=${CONV_ID}"
309
330
  fi
310
- if ! printf '%s' "$HOOK_STDERR" | grep -q '\[assistant: hi there\]'; then
311
- fail "case-6 stderr must carry the assistant turn '[assistant: hi there]'"
331
+ if ! printf '%s' "$CTX_BODY" | grep -q '\[user: hello\]'; then
332
+ fail "case-5 additionalContext must carry user turn '[user: hello]'"
312
333
  fi
313
- if ! printf '%s' "$HOOK_STDERR" | grep -q 'KnowledgeDocument'; then
314
- fail "case-6 stderr <prior-writes> must include fetched labels (KnowledgeDocument)"
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"
315
339
  fi
316
340
  if ingest_lines | grep -qE "^dispatch sessionId=${OP_ID} conversationId=${CONV_ID} "; then
317
- pass "case-6 dispatch path → exit 2 + directive with conversation, prior-writes, conversationId"
341
+ pass "case-5 dispatch path → exit 0 + JSON envelope with conversation, prior-writes, conversationId"
318
342
  else
319
- fail "case-6 expected dispatch log line, got: $(ingest_lines)"
343
+ fail "case-5 expected dispatch log line, got: $(ingest_lines)"
320
344
  fi
321
345
 
322
346
  # ---------------------------------------------------------------------------
323
- # Case 7: Prior-writes fetch fails (context endpoint 500) → exit 2, dispatch
324
- # still fires, <prior-writes> resolves to `(none)`.
347
+ # Case 6: Prior-writes fetch fails (500) → exit 0, prior-writes=(none).
325
348
  # ---------------------------------------------------------------------------
326
349
  : > "$CTX_FILE"
327
350
  : > "$REQ_LOG"
328
351
  run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
329
- [[ "$HOOK_RC" -eq 2 ]] || fail "case-7 expected rc=2, got $HOOK_RC"
330
- if ! printf '%s' "$HOOK_STDERR" | grep -q '<prior-writes>'; then
331
- fail "case-7 stderr missing <prior-writes> block"
332
- fi
333
- # Extract the prior-writes block and check it contains only `(none)`.
334
- PRIOR_BODY=$(printf '%s' "$HOOK_STDERR" | awk '/<prior-writes>/{flag=1;next}/<\/prior-writes>/{flag=0}flag' | tr -d '[:space:]')
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:]')
335
355
  if [[ "$PRIOR_BODY" == "(none)" ]]; then
336
- pass "case-7 context endpoint 500 → dispatch still fires, prior-writes = (none)"
356
+ pass "case-6 context endpoint 500 → dispatch still fires, prior-writes = (none)"
337
357
  else
338
- fail "case-7 expected prior-writes=(none), got [$PRIOR_BODY]"
358
+ fail "case-6 expected prior-writes=(none), got [$PRIOR_BODY]"
339
359
  fi
340
360
 
341
361
  # ---------------------------------------------------------------------------
342
- # Case 8: CONVERSATION_NODE_ID unset → exit 2, prompt names
343
- # `conversationId: <unset>`, no GET to /api/admin/post-turn-context.
362
+ # Case 7: CONVERSATION_NODE_ID unset → conversationId:<unset>, no context GET.
344
363
  # ---------------------------------------------------------------------------
345
364
  cat > "$CTX_FILE" <<'JSON'
346
365
  {"writes":[]}
347
366
  JSON
348
367
  : > "$REQ_LOG"
349
368
  run_hook "admin" "" "$LISTENER_PORT" "" "$ENV_BASIC"
350
- [[ "$HOOK_RC" -eq 2 ]] || fail "case-8 expected rc=2, got $HOOK_RC"
351
- if ! printf '%s' "$HOOK_STDERR" | grep -q 'conversationId: <unset>'; then
352
- fail "case-8 stderr must contain 'conversationId: <unset>'"
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>'"
353
373
  fi
354
374
  GET_COUNT=$(context_get_count)
355
375
  if [[ "$GET_COUNT" == "0" ]]; then
356
- pass "case-8 CONVERSATION_NODE_ID unset → no GET /api/admin/post-turn-context attempted"
376
+ pass "case-7 CONVERSATION_NODE_ID unset → no GET /api/admin/post-turn-context attempted"
357
377
  else
358
- fail "case-8 expected zero context GETs, got: $GET_COUNT"
378
+ fail "case-7 expected zero context GETs, got: $GET_COUNT"
359
379
  fi
360
380
 
361
381
  # ---------------------------------------------------------------------------
362
- # Case 9: Multi-block assistant turn → conversation walk concatenates text
363
- # blocks; tool_use blocks dropped.
382
+ # Case 8: Multi-block assistant turn → text concatenated, tool_use dropped.
364
383
  # ---------------------------------------------------------------------------
365
384
  T_MULTI=$(mktemp); TMPFILES+=("$T_MULTI")
366
385
  python3 - "$T_MULTI" <<'PY'
@@ -369,12 +388,12 @@ out = sys.argv[1]
369
388
  records = [
370
389
  {
371
390
  "type": "user",
372
- "timestamp": "2026-05-24T10:00:00.000Z",
391
+ "timestamp": "2026-05-26T10:00:00.000Z",
373
392
  "message": {"role": "user", "content": "go"},
374
393
  },
375
394
  {
376
395
  "type": "assistant",
377
- "timestamp": "2026-05-24T10:00:01.000Z",
396
+ "timestamp": "2026-05-26T10:00:01.000Z",
378
397
  "message": {
379
398
  "id": "msg_a",
380
399
  "role": "assistant",
@@ -396,20 +415,20 @@ cat > "$CTX_FILE" <<'JSON'
396
415
  JSON
397
416
  : > "$REQ_LOG"
398
417
  run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_MULTI"
399
- [[ "$HOOK_RC" -eq 2 ]] || fail "case-9 expected rc=2, got $HOOK_RC"
400
- if ! printf '%s' "$HOOK_STDERR" | grep -q '\[assistant: part-one part-two\]'; then
401
- fail "case-9 expected concatenated assistant text '[assistant: part-one part-two]'"
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]'"
402
422
  fi
403
- if printf '%s' "$HOOK_STDERR" | grep -q '"tool_use"'; then
404
- fail "case-9 tool_use block must not appear in dispatch stderr"
423
+ if printf '%s' "$CTX_BODY" | grep -q '"tool_use"'; then
424
+ fail "case-8 tool_use block must not appear in additionalContext"
405
425
  fi
406
- if printf '%s' "$HOOK_STDERR" | grep -q '\[assistant: part-one part-two\]'; then
407
- pass "case-9 multi-block assistant turn → text concatenated, tool_use dropped"
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"
408
428
  fi
409
429
 
410
430
  # ---------------------------------------------------------------------------
411
- # Case 10: User turn whose only content is tool_result blocks → skipped in
412
- # the conversation walk (no `[user: ...]` line for that record).
431
+ # Case 9: User turn with only tool_result blocks → skipped in conversation walk.
413
432
  # ---------------------------------------------------------------------------
414
433
  T_TRONLY=$(mktemp); TMPFILES+=("$T_TRONLY")
415
434
  python3 - "$T_TRONLY" <<'PY'
@@ -418,12 +437,12 @@ out = sys.argv[1]
418
437
  records = [
419
438
  {
420
439
  "type": "user",
421
- "timestamp": "2026-05-24T10:00:00.000Z",
440
+ "timestamp": "2026-05-26T10:00:00.000Z",
422
441
  "message": {"role": "user", "content": "real user turn"},
423
442
  },
424
443
  {
425
444
  "type": "assistant",
426
- "timestamp": "2026-05-24T10:00:01.000Z",
445
+ "timestamp": "2026-05-26T10:00:01.000Z",
427
446
  "message": {
428
447
  "id": "msg_a",
429
448
  "role": "assistant",
@@ -432,7 +451,7 @@ records = [
432
451
  },
433
452
  {
434
453
  "type": "user",
435
- "timestamp": "2026-05-24T10:00:02.000Z",
454
+ "timestamp": "2026-05-26T10:00:02.000Z",
436
455
  "message": {
437
456
  "role": "user",
438
457
  "content": [
@@ -448,25 +467,78 @@ PY
448
467
  ENV_TRONLY=$(envelope_for "$OP_ID" "$T_TRONLY")
449
468
  : > "$REQ_LOG"
450
469
  run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_TRONLY"
451
- [[ "$HOOK_RC" -eq 2 ]] || fail "case-10 expected rc=2, got $HOOK_RC"
452
- CONV_BLOCK=$(printf '%s' "$HOOK_STDERR" | awk '/<conversation>/{flag=1;next}/<\/conversation>/{flag=0}flag')
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')
453
473
  if ! printf '%s' "$CONV_BLOCK" | grep -q '\[user: real user turn\]'; then
454
- fail "case-10 conversation block must include real user turn"
474
+ fail "case-9 conversation block must include real user turn"
455
475
  fi
456
476
  if printf '%s' "$CONV_BLOCK" | grep -q 'tool plumbing payload'; then
457
- fail "case-10 tool_result-only user record must not appear in conversation walk"
477
+ fail "case-9 tool_result-only user record must not appear in conversation walk"
458
478
  fi
459
- # The heredoc indents the conversation block; match leading whitespace.
460
479
  TR_LINES=$(printf '%s\n' "$CONV_BLOCK" | grep -cE '\[user:' || true)
461
480
  if [[ "$TR_LINES" == "1" ]]; then
462
- pass "case-10 tool_result-only user record skipped in conversation walk"
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"
463
535
  else
464
- fail "case-10 expected exactly one [user: ...] line, got $TR_LINES"
536
+ fail "case-11 expected wrong-hook-event skip, got: $(ingest_lines)"
465
537
  fi
466
538
 
467
539
  # ---------------------------------------------------------------------------
468
540
  # Summary
469
541
  # ---------------------------------------------------------------------------
470
542
  echo
471
- echo "post-turn-graph-pass tests: $PASS passed, $FAIL failed"
543
+ echo "pre-turn-graph-pass tests: $PASS passed, $FAIL failed"
472
544
  [[ "$FAIL" -eq 0 ]]
@@ -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