@rubytech/create-maxy-code 0.1.161 → 0.1.164

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 (87) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/neo4j/edge-annotations.json +4 -0
  3. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  4. package/payload/platform/plugins/admin/PLUGIN.md +1 -6
  5. package/payload/platform/plugins/admin/hooks/lib/admin-graph-pass-common.sh +12 -15
  6. package/payload/platform/plugins/admin/mcp/dist/index.js +0 -41
  7. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  8. package/payload/platform/plugins/docs/references/platform.md +1 -1
  9. package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
  10. package/payload/platform/plugins/email/PLUGIN.md +8 -12
  11. package/payload/platform/plugins/email/mcp/dist/index.js +8 -34
  12. package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.d.ts +39 -0
  14. package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.d.ts.map +1 -0
  15. package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.js +0 -0
  16. package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.js.map +1 -0
  17. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts +23 -1
  18. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts.map +1 -1
  19. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js +131 -3
  20. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js.map +1 -1
  21. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.d.ts +19 -0
  22. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.d.ts.map +1 -0
  23. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.js +51 -0
  24. package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.js.map +1 -0
  25. package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +0 -18
  26. package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
  27. package/payload/platform/plugins/email/mcp/dist/lib/graph.js +0 -12
  28. package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
  29. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +88 -0
  30. package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
  31. package/payload/platform/plugins/email/mcp/dist/lib/imap.js +189 -1
  32. package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
  33. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.d.ts.map +1 -1
  34. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js +6 -1
  35. package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js.map +1 -1
  36. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts +7 -15
  37. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts.map +1 -1
  38. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js +206 -86
  39. package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js.map +1 -1
  40. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -1
  41. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +104 -4
  42. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -1
  43. package/payload/platform/plugins/email/mcp/dist/tools/email-read.d.ts.map +1 -1
  44. package/payload/platform/plugins/email/mcp/dist/tools/email-read.js +18 -12
  45. package/payload/platform/plugins/email/mcp/dist/tools/email-read.js.map +1 -1
  46. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
  47. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +5 -4
  48. package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
  49. package/payload/platform/plugins/email/mcp/dist/tools/email-search.d.ts.map +1 -1
  50. package/payload/platform/plugins/email/mcp/dist/tools/email-search.js +13 -12
  51. package/payload/platform/plugins/email/mcp/dist/tools/email-search.js.map +1 -1
  52. package/payload/platform/plugins/email/references/email-reference.md +8 -8
  53. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  54. package/payload/platform/plugins/memory/PLUGIN.md +2 -2
  55. package/payload/platform/plugins/memory/mcp/dist/index.js +4 -3
  56. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  57. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.js +1 -13
  58. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.js.map +1 -1
  59. package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts.map +1 -1
  60. package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js +3 -5
  61. package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js.map +1 -1
  62. package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.d.ts +1 -1
  63. package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.d.ts.map +1 -1
  64. package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.js +10 -4
  65. package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.js.map +1 -1
  66. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts.map +1 -1
  67. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js +5 -6
  68. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js.map +1 -1
  69. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +21 -4
  70. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  71. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  72. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +6 -8
  73. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  74. package/payload/platform/plugins/memory/references/schema-base.md +2 -1
  75. package/payload/platform/plugins/memory/references/transcript-formats/circleback.md +49 -0
  76. package/payload/platform/plugins/memory/references/transcript-formats/granola.md +50 -0
  77. package/payload/platform/plugins/memory/references/transcript-formats/otter.md +50 -0
  78. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +1 -0
  79. package/payload/platform/plugins/memory/skills/conversation-archive-mcp/SKILL.md +170 -0
  80. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  81. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +3 -4
  82. package/payload/platform/scripts/seed-neo4j.sh +1 -3
  83. package/payload/platform/templates/specialists/agents/librarian.md +1 -0
  84. package/payload/platform/plugins/admin/hooks/__tests__/per-turn-graph-pass-gate.test.sh +0 -361
  85. package/payload/platform/plugins/admin/hooks/__tests__/pre-turn-graph-pass.test.sh +0 -441
  86. package/payload/platform/plugins/admin/hooks/per-turn-graph-pass-gate.sh +0 -106
  87. package/payload/platform/plugins/admin/hooks/pre-turn-graph-pass.sh +0 -171
@@ -1,441 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Tasks 425, 430 — pre-turn-graph-pass UserPromptSubmit hook test suite.
3
- #
4
- # Task 430 reshaped the hook: admin walks the operator turn, admin
5
- # dispatches per-write briefs to `database-operator`. The hook no longer
6
- # packages a `<conversation>` block — admin already has the turn in
7
- # context. The `<prior-writes>` block remains as a skip-list for admin.
8
- #
9
- # A local Python HTTP server stands in for the admin server:
10
- # POST /api/admin/log-ingest → records every line emit_log sent.
11
- # GET /api/admin/post-turn-context → returns the contents of $CTX_FILE,
12
- # or 500 if $CTX_FILE is missing/empty.
13
- # Both kinds of request are appended to $REQ_LOG so assertions can see
14
- # every POST body AND whether the GET was attempted at all.
15
- #
16
- # Contract under test:
17
- # - Role/specialist gates: exit 0, single trigger-skipped log line, empty
18
- # stdout (no additionalContext).
19
- # - MAXY_UI_INTERNAL_PORT unset: exit 0, empty stdout. The hook emits its
20
- # missing-env diagnostic on its own stderr channel because
21
- # /api/admin/log-ingest is unreachable; loud-fail is acceptable.
22
- # - Missing-transcript: exit 0, single trigger-skipped log line, empty
23
- # stdout.
24
- # - Dispatch path (admin + populated JSONL): exit 0, stdout is a JSON
25
- # envelope `{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit",
26
- # "additionalContext":"..."}}` whose additionalContext begins with
27
- # `[system: pre-turn-graph-pass-hook]`, names `database-operator` as
28
- # the dispatch target, carries a `<prior-writes>` block with the
29
- # fetched node, interpolates sessionId from SESSION_NODE_ID,
30
- # and carries NO `<conversation>` block (admin has the turn already).
31
- # Dispatch log line carries `directive-shape=admin-walks`.
32
- # - Prior-writes fetch fails (context endpoint 500): exit 0, dispatch
33
- # still fires, `<prior-writes>` block resolves to `(none)`.
34
- # - SESSION_NODE_ID unset: exit 0, additionalContext names
35
- # `sessionId: <unset>`, and no GET to /api/admin/post-turn-context
36
- # is attempted.
37
- # - UserPromptSubmit stdin-shape regression: a synthetic UserPromptSubmit
38
- # payload produces the documented stdout envelope; a stale Stop-shaped
39
- # payload is rejected with `wrong-hook-event`.
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, session_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+=("SESSION_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 must NOT carry a <conversation> block (admin has the turn in context)"
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 'database-operator'; then
329
- fail "case-5 additionalContext must name the database-operator dispatch target"
330
- fi
331
- if ! printf '%s' "$CTX_BODY" | grep -q "sessionId: ${CONV_ID}"; then
332
- fail "case-5 additionalContext must interpolate sessionId=${CONV_ID}"
333
- fi
334
- if ! printf '%s' "$CTX_BODY" | grep -q 'KnowledgeDocument'; then
335
- fail "case-5 additionalContext <prior-writes> must include fetched labels"
336
- fi
337
- if ingest_lines | grep -qE "^dispatch claudeSessionId=${OP_ID} sessionId=${CONV_ID} directive-shape=admin-walks "; then
338
- pass "case-5 dispatch path → exit 0 + JSON envelope with prior-writes, sessionId, admin-walks shape"
339
- else
340
- fail "case-5 expected dispatch log line with directive-shape=admin-walks, got: $(ingest_lines)"
341
- fi
342
-
343
- # ---------------------------------------------------------------------------
344
- # Case 6: Prior-writes fetch fails (500) → exit 0, prior-writes=(none).
345
- # ---------------------------------------------------------------------------
346
- : > "$CTX_FILE"
347
- : > "$REQ_LOG"
348
- run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
349
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-6 expected rc=0, got $HOOK_RC"
350
- CTX_BODY=$(additional_context)
351
- PRIOR_BODY=$(printf '%s' "$CTX_BODY" | awk '/<prior-writes>/{flag=1;next}/<\/prior-writes>/{flag=0}flag' | tr -d '[:space:]')
352
- if [[ "$PRIOR_BODY" == "(none)" ]]; then
353
- pass "case-6 context endpoint 500 → dispatch still fires, prior-writes = (none)"
354
- else
355
- fail "case-6 expected prior-writes=(none), got [$PRIOR_BODY]"
356
- fi
357
-
358
- # ---------------------------------------------------------------------------
359
- # Case 7: SESSION_NODE_ID unset → sessionId:<unset>, no context GET.
360
- # ---------------------------------------------------------------------------
361
- cat > "$CTX_FILE" <<'JSON'
362
- {"writes":[]}
363
- JSON
364
- : > "$REQ_LOG"
365
- run_hook "admin" "" "$LISTENER_PORT" "" "$ENV_BASIC"
366
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-7 expected rc=0, got $HOOK_RC"
367
- CTX_BODY=$(additional_context)
368
- if ! printf '%s' "$CTX_BODY" | grep -q 'sessionId: <unset>'; then
369
- fail "case-7 additionalContext must contain 'sessionId: <unset>'"
370
- fi
371
- GET_COUNT=$(context_get_count)
372
- if [[ "$GET_COUNT" == "0" ]]; then
373
- pass "case-7 SESSION_NODE_ID unset → no GET /api/admin/post-turn-context attempted"
374
- else
375
- fail "case-7 expected zero context GETs, got: $GET_COUNT"
376
- fi
377
-
378
- # ---------------------------------------------------------------------------
379
- # Cases 8 and 9 in the Task 425 suite asserted the conversation walk
380
- # (multi-block assistant concatenation; tool_result-only user skip).
381
- # Task 430 removed the conversation walk entirely — admin reads the turn
382
- # it already has in context — so those cases no longer apply.
383
- #
384
- # Case 10: UserPromptSubmit shape — directive on stdout only, never stderr;
385
- # stdout envelope has hookEventName="UserPromptSubmit".
386
- # ---------------------------------------------------------------------------
387
- cat > "$CTX_FILE" <<'JSON'
388
- {"writes":[]}
389
- JSON
390
- : > "$REQ_LOG"
391
- run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
392
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-10 expected rc=0, got $HOOK_RC"
393
- if printf '%s' "$HOOK_STDERR" | grep -qF "$DIRECTIVE_TOKEN"; then
394
- fail "case-10 directive must not appear on stderr (UserPromptSubmit contract is stdout JSON)"
395
- fi
396
- HEN=$(printf '%s' "$HOOK_STDOUT" | python3 -c '
397
- import sys, json
398
- try:
399
- d = json.load(sys.stdin)
400
- sys.stdout.write(d["hookSpecificOutput"]["hookEventName"])
401
- except Exception:
402
- pass
403
- ')
404
- if [[ "$HEN" == "UserPromptSubmit" ]]; then
405
- pass "case-10 stdout envelope is well-formed JSON with hookEventName=UserPromptSubmit"
406
- else
407
- fail "case-10 expected hookEventName=UserPromptSubmit, got [$HEN]"
408
- fi
409
-
410
- # ---------------------------------------------------------------------------
411
- # Case 11: Stop-shaped stdin regression — a CC Stop-hook payload carries
412
- # hook_event_name="Stop". The hook must not silently emit a
413
- # UserPromptSubmit envelope on a Stop fire. Expect exit 0 with
414
- # empty stdout and a `wrong-hook-event` skip line.
415
- # ---------------------------------------------------------------------------
416
- ENV_STOP_SHAPED=$(python3 -c '
417
- import json, sys
418
- print(json.dumps({
419
- "session_id": sys.argv[1],
420
- "transcript_path": sys.argv[2],
421
- "hook_event_name": "Stop",
422
- }))
423
- ' "$OP_ID" "$TRANSCRIPT_BASIC")
424
- : > "$REQ_LOG"
425
- run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_STOP_SHAPED"
426
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-11 expected rc=0, got $HOOK_RC"
427
- if [[ -n "$HOOK_STDOUT" ]]; then
428
- fail "case-11 stdout must be empty on wrong-hook-event Stop, got: $HOOK_STDOUT"
429
- fi
430
- if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=wrong-hook-event event=Stop$"; then
431
- pass "case-11 Stop-shaped stdin rejected with wrong-hook-event skip"
432
- else
433
- fail "case-11 expected wrong-hook-event skip, got: $(ingest_lines)"
434
- fi
435
-
436
- # ---------------------------------------------------------------------------
437
- # Summary
438
- # ---------------------------------------------------------------------------
439
- echo
440
- echo "pre-turn-graph-pass tests: $PASS passed, $FAIL failed"
441
- [[ "$FAIL" -eq 0 ]]
@@ -1,106 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Stop hook — per-turn gate. Task 440.
3
- #
4
- # Mirrors `session-end-retrospective.sh`'s structural primitive (exit 2 +
5
- # stderr instruction + sentinel-grep release) but fires on every admin turn
6
- # end rather than only on operator end-intent. Closes the gap left when
7
- # Task 425 moved the per-turn graph pass off Stop onto UserPromptSubmit:
8
- # the UserPromptSubmit `additionalContext` directive carries the prior-writes
9
- # skip-list and the instruction, but admin can silently drop the directive
10
- # because UserPromptSubmit's only blocking shape erases the prompt without
11
- # feeding stderr to Claude. The structural fix is here — Stop's exit-2
12
- # stderr-to-Claude contract gives the gate a release path admin can act on.
13
- #
14
- # Release conditions, evaluated against the operator's JSONL since the
15
- # latest real-user message (string content OR a content list with at least
16
- # one non-`tool_result` block):
17
- # A. admin dispatched the writes — a `Task` `tool_use` whose
18
- # `input.subagent_type` is `database-operator` appeared.
19
- # B. admin declined the writes — a `tool_use` whose `name` is exactly
20
- # `mcp__admin__pre-turn-graph-pass-skip` appeared. The sentinel takes a
21
- # `reason` string (free-form, e.g. `no-new-entities`,
22
- # `turn-is-acknowledgement`); the tool records the call for audit but
23
- # takes no graph action.
24
- #
25
- # Either release path emits `gate-released sessionId=<id> via=<dispatch|skip-sentinel>`
26
- # and exits 0. Both Stop-hook tags emit independently and compose: the
27
- # session-end retrospective gates only on `/end` intent and uses its own
28
- # sentinel; this hook gates every turn and uses the two release patterns
29
- # above.
30
- #
31
- # Recursion guard — the shared `agp_guard_role_specialist` skips when
32
- # `MAXY_SPECIALIST` is set, so the database-operator subagent's own turn
33
- # end never re-triggers this gate.
34
- #
35
- # Loop-safety — once release path A or B fires, the matching tool_use sits
36
- # in the JSONL and every subsequent Stop on the same operator-prompt window
37
- # sees it and exits 0 with `gate-released`. The Task 412 loop case is
38
- # avoided because the release decision is JSONL-based, never a re-fire of
39
- # the directive.
40
- #
41
- # Input: Claude Code's Stop hook stdin shape
42
- # { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
43
-
44
- set -uo pipefail
45
-
46
- AGP_LOG_TAG="per-turn-graph-pass-gate"
47
- AGP_LIB="$(dirname "$0")/lib/admin-graph-pass-common.sh"
48
- if [ ! -f "$AGP_LIB" ]; then
49
- echo "[${AGP_LOG_TAG}] trigger-skipped sessionId=unknown reason=missing-helper path=${AGP_LIB}" >&2
50
- exit 0
51
- fi
52
- # shellcheck source=lib/admin-graph-pass-common.sh
53
- source "$AGP_LIB"
54
-
55
- agp_setup_logging
56
- agp_read_stdin
57
- agp_parse_stdin
58
- agp_guard_role_specialist
59
- agp_guard_transcript require-stdin
60
-
61
- # Release alternation: the MCP-prefixed skip-sentinel name, or a Task
62
- # tool_use with `input.subagent_type=database-operator` (matched as the
63
- # synthetic name `Task:database-operator` by the helper).
64
- RELEASE_PATTERN='mcp__admin__pre-turn-graph-pass-skip|Task:database-operator'
65
-
66
- INSPECTION=$(agp_scan_post_user_toolnames "$RELEASE_PATTERN")
67
- USER_PRESENT="${INSPECTION%% *}"
68
- MATCHED_NAME="${INSPECTION#* }"
69
-
70
- if [ "$USER_PRESENT" != "yes" ]; then
71
- # No real-user message in the transcript yet — e.g. the SessionStart
72
- # bootstrap turn, an empty transcript, or a session that only contains
73
- # tool_result-shaped user records. Nothing to gate against.
74
- agp_emit_log "trigger-skipped sessionId=${SESSION_ID} reason=no-real-user"
75
- exit 0
76
- fi
77
-
78
- if [ -n "$MATCHED_NAME" ]; then
79
- case "$MATCHED_NAME" in
80
- Task:database-operator)
81
- VIA="dispatch"
82
- ;;
83
- mcp__admin__pre-turn-graph-pass-skip)
84
- VIA="skip-sentinel"
85
- ;;
86
- *)
87
- VIA="unknown"
88
- ;;
89
- esac
90
- agp_emit_log "gate-released sessionId=${SESSION_ID} via=${VIA}"
91
- exit 0
92
- fi
93
-
94
- agp_emit_log "gate-blocked sessionId=${SESSION_ID} reason=sentinel-absent"
95
-
96
- cat >&2 <<'INSTRUCTION'
97
- Before this turn closes, do the per-turn graph pass. Scan the operator's latest message for any new graph-worthy content not already covered by the prior-writes block in this turn's pre-turn-graph-pass directive — mentions of named entities, ideas or learnings the operator stated, contacts named for the first time, tasks the operator committed to, decisions, anything the schema admits.
98
-
99
- For each candidate write, dispatch one Task to the `database-operator` specialist with a brief that names the label, the relevant properties, and (when attaching to an existing node) the elementId from the prior-writes list. Skip writes the prior-writes list already covers.
100
-
101
- If the turn supports no new writes, call the `pre-turn-graph-pass-skip` MCP tool with a short `reason` (e.g. `no-new-entities`, `turn-is-acknowledgement`) so the gate releases. Calling that tool is the deterministic signal that you considered the turn and chose not to write.
102
-
103
- Either path releases this gate and lets the turn close. Do not surface this dispatch in your reply to the operator.
104
- INSTRUCTION
105
-
106
- exit 2