@rubytech/create-maxy-code 0.1.163 → 0.1.165

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 (106) 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 +91 -40
  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 +204 -169
  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 +22 -13
  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 +17 -13
  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 +3 -2
  75. package/payload/platform/plugins/memory/references/schema-estate-agent.md +1 -1
  76. package/payload/platform/plugins/memory/references/transcript-formats/circleback.md +49 -0
  77. package/payload/platform/plugins/memory/references/transcript-formats/granola.md +50 -0
  78. package/payload/platform/plugins/memory/references/transcript-formats/otter.md +50 -0
  79. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +1 -0
  80. package/payload/platform/plugins/memory/skills/conversation-archive-mcp/SKILL.md +170 -0
  81. package/payload/platform/scripts/seed-neo4j.sh +1 -3
  82. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +9 -1
  83. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
  84. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +74 -38
  85. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
  86. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  87. package/payload/platform/services/claude-session-manager/dist/http-server.js +16 -0
  88. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  89. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +16 -1
  90. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  91. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +21 -7
  92. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  93. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +7 -0
  94. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
  95. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +10 -0
  96. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
  97. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts +52 -10
  98. package/payload/platform/services/claude-session-manager/dist/systemd-scope.d.ts.map +1 -1
  99. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js +73 -13
  100. package/payload/platform/services/claude-session-manager/dist/systemd-scope.js.map +1 -1
  101. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  102. package/payload/platform/templates/specialists/agents/librarian.md +1 -0
  103. package/payload/platform/plugins/admin/hooks/__tests__/per-turn-graph-pass-gate.test.sh +0 -387
  104. package/payload/platform/plugins/admin/hooks/__tests__/pre-turn-graph-pass.test.sh +0 -441
  105. package/payload/platform/plugins/admin/hooks/per-turn-graph-pass-gate.sh +0 -111
  106. package/payload/platform/plugins/admin/hooks/pre-turn-graph-pass.sh +0 -171
@@ -1,387 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Task 440 — per-turn-graph-pass-gate Stop hook test suite.
3
- #
4
- # Mirrors session-end-retrospective.test.sh's listener-mock pattern. Local
5
- # Python HTTP server stands in for /api/admin/log-ingest and records every
6
- # POST so the assertions read every log line the hook emitted.
7
- #
8
- # Contract under test:
9
- # - Role/specialist gates: exit 0, single trigger-skipped log line, no
10
- # stderr.
11
- # role-not-admin | is-specialist | empty-stdin | missing-transcript
12
- # - No real-user record in transcript (e.g. SessionStart bootstrap, or
13
- # only tool_result-shaped user records): exit 0, single
14
- # `trigger-skipped reason=no-real-user` line, no stderr.
15
- # - Block path — admin's turn since the latest real-user message contains
16
- # no `Task(database-operator)` dispatch and no `pre-turn-graph-pass-skip`
17
- # tool_use: exit 2, stderr carries the instruction block,
18
- # `gate-blocked sessionId=<id> reason=sentinel-absent` emitted.
19
- # - Release via dispatch — a `Task` tool_use with
20
- # `input.subagent_type=database-operator` appears AFTER the latest
21
- # real-user message: exit 0, `gate-released sessionId=<id> via=dispatch`.
22
- # - Release via skip-sentinel — a `tool_use` named
23
- # `mcp__admin__pre-turn-graph-pass-skip` appears AFTER the latest
24
- # real-user message: exit 0,
25
- # `gate-released sessionId=<id> via=skip-sentinel`.
26
- # - Per-prompt scoping — a dispatch BEFORE the latest real-user message
27
- # does NOT release the gate.
28
- # - Re-entry — a second Stop fired after the release tool_use still
29
- # releases (the JSONL grep is the re-entry guard, no loop).
30
-
31
- set -u
32
-
33
- HOOK="$(cd "$(dirname "$0")/.." && pwd)/per-turn-graph-pass-gate.sh"
34
- if [[ ! -x "$HOOK" ]]; then
35
- echo "FAIL: $HOOK not executable" >&2
36
- exit 1
37
- fi
38
-
39
- OP_ID='aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb'
40
-
41
- TMPFILES=()
42
- LISTENER_PIDS=()
43
- cleanup_test_state() {
44
- for pid in "${LISTENER_PIDS[@]:-}"; do
45
- if [[ -n "$pid" ]]; then
46
- kill "$pid" 2>/dev/null || true
47
- wait "$pid" 2>/dev/null || true
48
- fi
49
- done
50
- for f in "${TMPFILES[@]:-}"; do
51
- [[ -n "$f" ]] && rm -f "$f" 2>/dev/null || true
52
- done
53
- }
54
- trap cleanup_test_state EXIT
55
-
56
- PASS=0
57
- FAIL=0
58
- pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
59
- fail() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
60
-
61
- start_listener() {
62
- REQ_LOG=$(mktemp); TMPFILES+=("$REQ_LOG")
63
- LISTENER_PORT=$((39800 + RANDOM % 100))
64
- python3 - "$LISTENER_PORT" "$REQ_LOG" <<'PY' &
65
- import sys, http.server, json
66
- port = int(sys.argv[1])
67
- log_path = sys.argv[2]
68
- class H(http.server.BaseHTTPRequestHandler):
69
- def log_message(self, *a, **k): pass
70
- def do_POST(self):
71
- n = int(self.headers.get('Content-Length','0') or 0)
72
- body = self.rfile.read(n).decode('utf-8','replace')
73
- with open(log_path, 'a', encoding='utf-8') as f:
74
- f.write(self.path + '\t' + body + '\n')
75
- self.send_response(200)
76
- self.send_header('Content-Type','application/json')
77
- self.end_headers()
78
- self.wfile.write(json.dumps({"ok": True}).encode('utf-8'))
79
- http.server.HTTPServer(('127.0.0.1', port), H).serve_forever()
80
- PY
81
- LISTENER_PIDS+=("$!")
82
- for _ in $(seq 1 20); do
83
- if curl -sS --max-time 1 -X POST "http://127.0.0.1:${LISTENER_PORT}/ping" -d '{}' >/dev/null 2>&1; then
84
- break
85
- fi
86
- sleep 0.1
87
- done
88
- : > "$REQ_LOG"
89
- }
90
-
91
- run_hook() {
92
- local role="$1"; local specialist="$2"; local stdin_json="$3"
93
- local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
94
- local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
95
- printf '%s' "$stdin_json" | \
96
- MAXY_SESSION_ROLE="$role" \
97
- MAXY_SPECIALIST="$specialist" \
98
- MAXY_UI_INTERNAL_PORT="$LISTENER_PORT" \
99
- bash "$HOOK" >"$stdout_file" 2>"$stderr_file"
100
- HOOK_RC=$?
101
- HOOK_STDERR=$(cat "$stderr_file")
102
- HOOK_STDOUT=$(cat "$stdout_file")
103
- sleep 0.1
104
- }
105
-
106
- ingest_lines() {
107
- grep -E '^/api/admin/log-ingest ' "$REQ_LOG" 2>/dev/null | python3 -c '
108
- import sys, json
109
- for raw in sys.stdin:
110
- try:
111
- _, body = raw.rstrip("\n").split("\t", 1)
112
- d = json.loads(body)
113
- if isinstance(d, dict) and isinstance(d.get("line"), str):
114
- print(d["line"])
115
- except Exception:
116
- pass
117
- ' || true
118
- }
119
-
120
- # write_transcript: builds JSONL from positional specs.
121
- # Spec format: <role>|<text>|<extra>
122
- # extra "" — plain text record
123
- # extra "tool_result" — user record carrying only a tool_result block
124
- # extra "tool_use:<NAME>" — assistant record with a tool_use block (no input)
125
- # extra "task:<subagent_type>" — assistant record with a Task tool_use whose
126
- # input.subagent_type equals <subagent_type>
127
- write_transcript() {
128
- local out="$1"; shift
129
- python3 - "$out" "$@" <<'PY'
130
- import sys, json
131
- out = sys.argv[1]
132
- spec = sys.argv[2:]
133
- with open(out, "w", encoding="utf-8") as f:
134
- for entry in spec:
135
- role, text, extra = entry.split("|", 2)
136
- rec = {"type": role, "timestamp": "2026-05-26T10:00:00.000Z"}
137
- if role == "user":
138
- if extra == "tool_result":
139
- rec["message"] = {
140
- "role": "user",
141
- "content": [
142
- {"type": "tool_result", "tool_use_id": "tu_x", "content": text or "ok"},
143
- ],
144
- }
145
- else:
146
- rec["message"] = {"role": "user", "content": text}
147
- else:
148
- content_blocks = []
149
- if text:
150
- content_blocks.append({"type": "text", "text": text})
151
- if extra.startswith("tool_use:"):
152
- tool_name = extra.split(":", 1)[1]
153
- content_blocks.append({
154
- "type": "tool_use",
155
- "id": f"toolu_{tool_name}",
156
- "name": tool_name,
157
- "input": {},
158
- })
159
- elif extra.startswith("task:"):
160
- subagent = extra.split(":", 1)[1]
161
- content_blocks.append({
162
- "type": "tool_use",
163
- "id": f"toolu_task_{subagent}",
164
- "name": "Task",
165
- "input": {"subagent_type": subagent, "description": "test", "prompt": "test"},
166
- })
167
- rec["message"] = {
168
- "id": f"msg_{role}_{hash(text) & 0xffff:04x}",
169
- "role": "assistant",
170
- "content": content_blocks,
171
- }
172
- f.write(json.dumps(rec) + "\n")
173
- PY
174
- }
175
-
176
- envelope_for() {
177
- python3 -c '
178
- import json, sys
179
- print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
180
- ' "$1" "$2"
181
- }
182
-
183
- INSTRUCTION_TOKEN='Before this turn closes, do the per-turn graph pass'
184
-
185
- # ---------------------------------------------------------------------------
186
- # Case 1: role != admin → trigger-skipped reason=role-not-admin
187
- # ---------------------------------------------------------------------------
188
- start_listener
189
- TR_BASIC=$(mktemp); TMPFILES+=("$TR_BASIC")
190
- write_transcript "$TR_BASIC" "user|hello|" "assistant|hi|"
191
- ENV_BASIC=$(envelope_for "$OP_ID" "$TR_BASIC")
192
- : > "$REQ_LOG"
193
- run_hook "public" "" "$ENV_BASIC"
194
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-1 rc=$HOOK_RC"
195
- [[ -z "$HOOK_STDERR" ]] || fail "case-1 stderr must be empty"
196
- if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=role-not-admin$"; then
197
- pass "case-1 role=public → trigger-skipped reason=role-not-admin"
198
- else
199
- fail "case-1 expected role-not-admin, got: $(ingest_lines)"
200
- fi
201
-
202
- # ---------------------------------------------------------------------------
203
- # Case 2: MAXY_SPECIALIST set → trigger-skipped reason=is-specialist
204
- # ---------------------------------------------------------------------------
205
- : > "$REQ_LOG"
206
- run_hook "admin" "database-operator" "$ENV_BASIC"
207
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-2 rc=$HOOK_RC"
208
- [[ -z "$HOOK_STDERR" ]] || fail "case-2 stderr must be empty"
209
- if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=is-specialist specialist=database-operator$"; then
210
- pass "case-2 specialist=database-operator → trigger-skipped is-specialist"
211
- else
212
- fail "case-2 expected is-specialist, got: $(ingest_lines)"
213
- fi
214
-
215
- # ---------------------------------------------------------------------------
216
- # Case 3: empty stdin → trigger-skipped reason=empty-stdin
217
- # ---------------------------------------------------------------------------
218
- : > "$REQ_LOG"
219
- run_hook "admin" "" ""
220
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-3 rc=$HOOK_RC"
221
- if ingest_lines | grep -qE "^trigger-skipped sessionId=.* reason=empty-stdin$"; then
222
- pass "case-3 empty stdin → trigger-skipped reason=empty-stdin"
223
- else
224
- fail "case-3 expected empty-stdin, got: $(ingest_lines)"
225
- fi
226
-
227
- # ---------------------------------------------------------------------------
228
- # Case 4: missing transcript_path → trigger-skipped reason=missing-transcript
229
- # ---------------------------------------------------------------------------
230
- BAD_ENV=$(python3 -c 'import json,sys; print(json.dumps({"session_id": sys.argv[1]}))' "$OP_ID")
231
- : > "$REQ_LOG"
232
- run_hook "admin" "" "$BAD_ENV"
233
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-4 rc=$HOOK_RC"
234
- if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=missing-transcript$"; then
235
- pass "case-4 missing transcript → trigger-skipped reason=missing-transcript"
236
- else
237
- fail "case-4 expected missing-transcript, got: $(ingest_lines)"
238
- fi
239
-
240
- # ---------------------------------------------------------------------------
241
- # Case 5: no real-user record in transcript → trigger-skipped no-real-user
242
- # ---------------------------------------------------------------------------
243
- TR_NO_USER=$(mktemp); TMPFILES+=("$TR_NO_USER")
244
- # Only a tool_result-shaped user record + an assistant record. The hook should
245
- # treat this as "no real user yet" and trigger-skip.
246
- write_transcript "$TR_NO_USER" "user|ok|tool_result" "assistant|booting|"
247
- ENV_NO_USER=$(envelope_for "$OP_ID" "$TR_NO_USER")
248
- : > "$REQ_LOG"
249
- run_hook "admin" "" "$ENV_NO_USER"
250
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-5 rc=$HOOK_RC"
251
- [[ -z "$HOOK_STDERR" ]] || fail "case-5 stderr must be empty"
252
- if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=no-real-user$"; then
253
- pass "case-5 only tool_result-user records → trigger-skipped no-real-user"
254
- else
255
- fail "case-5 expected no-real-user, got: $(ingest_lines)"
256
- fi
257
-
258
- # ---------------------------------------------------------------------------
259
- # Case 6: gate-blocked — admin replied to operator without dispatching or
260
- # calling the skip sentinel.
261
- # ---------------------------------------------------------------------------
262
- TR_BLOCKED=$(mktemp); TMPFILES+=("$TR_BLOCKED")
263
- write_transcript "$TR_BLOCKED" "user|hello|" "assistant|hi there|"
264
- ENV_BLOCKED=$(envelope_for "$OP_ID" "$TR_BLOCKED")
265
- : > "$REQ_LOG"
266
- run_hook "admin" "" "$ENV_BLOCKED"
267
- [[ "$HOOK_RC" -eq 2 ]] || fail "case-6 expected rc=2, got $HOOK_RC"
268
- if ! printf '%s' "$HOOK_STDERR" | grep -qF "$INSTRUCTION_TOKEN"; then
269
- fail "case-6 stderr must contain instruction token; got: $HOOK_STDERR"
270
- fi
271
- if ingest_lines | grep -qE "^gate-blocked sessionId=${OP_ID} reason=sentinel-absent$"; then
272
- pass "case-6 admin replied without dispatch/skip → gate-blocked + stderr instruction + rc=2"
273
- else
274
- fail "case-6 expected gate-blocked, got: $(ingest_lines)"
275
- fi
276
-
277
- # ---------------------------------------------------------------------------
278
- # Case 7: release via dispatch — admin issued Task(database-operator) AFTER
279
- # the latest real-user message.
280
- # ---------------------------------------------------------------------------
281
- TR_DISPATCH=$(mktemp); TMPFILES+=("$TR_DISPATCH")
282
- write_transcript "$TR_DISPATCH" \
283
- "user|tell me about Alice|" \
284
- "assistant|dispatching|task:database-operator" \
285
- "assistant|done|"
286
- ENV_DISPATCH=$(envelope_for "$OP_ID" "$TR_DISPATCH")
287
- : > "$REQ_LOG"
288
- run_hook "admin" "" "$ENV_DISPATCH"
289
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-7 expected rc=0, got $HOOK_RC"
290
- [[ -z "$HOOK_STDERR" ]] || fail "case-7 stderr must be empty on release"
291
- if ingest_lines | grep -qE "^gate-released sessionId=${OP_ID} via=dispatch( |$)"; then
292
- pass "case-7 Task(database-operator) after latest user → gate-released via=dispatch"
293
- else
294
- fail "case-7 expected gate-released via=dispatch, got: $(ingest_lines)"
295
- fi
296
-
297
- # ---------------------------------------------------------------------------
298
- # Case 8: release via skip-sentinel — admin called the skip MCP tool AFTER
299
- # the latest real-user message.
300
- # ---------------------------------------------------------------------------
301
- TR_SKIP=$(mktemp); TMPFILES+=("$TR_SKIP")
302
- write_transcript "$TR_SKIP" \
303
- "user|thanks|" \
304
- "assistant|noted|tool_use:mcp__admin__pre-turn-graph-pass-skip" \
305
- "assistant|you're welcome|"
306
- ENV_SKIP=$(envelope_for "$OP_ID" "$TR_SKIP")
307
- : > "$REQ_LOG"
308
- run_hook "admin" "" "$ENV_SKIP"
309
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-8 expected rc=0, got $HOOK_RC"
310
- [[ -z "$HOOK_STDERR" ]] || fail "case-8 stderr must be empty on release"
311
- if ingest_lines | grep -qE "^gate-released sessionId=${OP_ID} via=skip-sentinel tool=mcp__admin__pre-turn-graph-pass-skip$"; then
312
- pass "case-8 skip-sentinel after latest user → gate-released via=skip-sentinel"
313
- else
314
- fail "case-8 expected gate-released via=skip-sentinel, got: $(ingest_lines)"
315
- fi
316
-
317
- # ---------------------------------------------------------------------------
318
- # Case 9: per-prompt scoping — a dispatch BEFORE the latest real-user
319
- # message does NOT release the current turn's gate.
320
- # ---------------------------------------------------------------------------
321
- TR_PRIOR=$(mktemp); TMPFILES+=("$TR_PRIOR")
322
- write_transcript "$TR_PRIOR" \
323
- "user|first turn|" \
324
- "assistant|dispatching|task:database-operator" \
325
- "assistant|done|" \
326
- "user|second turn|" \
327
- "assistant|hello again|"
328
- ENV_PRIOR=$(envelope_for "$OP_ID" "$TR_PRIOR")
329
- : > "$REQ_LOG"
330
- run_hook "admin" "" "$ENV_PRIOR"
331
- [[ "$HOOK_RC" -eq 2 ]] || fail "case-9 expected rc=2, got $HOOK_RC"
332
- if ingest_lines | grep -qE "^gate-blocked sessionId=${OP_ID} reason=sentinel-absent$"; then
333
- pass "case-9 dispatch before latest user does NOT release current turn → gate-blocked"
334
- else
335
- fail "case-9 expected gate-blocked, got: $(ingest_lines)"
336
- fi
337
-
338
- # ---------------------------------------------------------------------------
339
- # Case 10: re-entry — a release tool_use earlier in the same prompt window
340
- # stays released on a second Stop fire (no loop).
341
- # ---------------------------------------------------------------------------
342
- TR_REENTRY=$(mktemp); TMPFILES+=("$TR_REENTRY")
343
- write_transcript "$TR_REENTRY" \
344
- "user|hi|" \
345
- "assistant|dispatching|task:database-operator" \
346
- "assistant|done|" \
347
- "assistant|wrap-up reply|"
348
- ENV_REENTRY=$(envelope_for "$OP_ID" "$TR_REENTRY")
349
- : > "$REQ_LOG"
350
- run_hook "admin" "" "$ENV_REENTRY"
351
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-10 expected rc=0, got $HOOK_RC"
352
- if ingest_lines | grep -qE "^gate-released sessionId=${OP_ID} via=dispatch( |$)"; then
353
- pass "case-10 second Stop after dispatch still releases (no loop)"
354
- else
355
- fail "case-10 expected gate-released via=dispatch, got: $(ingest_lines)"
356
- fi
357
-
358
- # ---------------------------------------------------------------------------
359
- # Case 11: namespace alias — admin called the skip MCP tool via the Claude
360
- # Code plugin-system namespace (`mcp__plugin_admin_admin__…`) instead of the
361
- # bare `mcp__admin__…`. Both forms must release the gate. Regression guard
362
- # for the Task 440 namespace-match bug observed in session
363
- # 2cfe467c-743d-4386-ac04-3eec8c89d8c9, where the agent looped on Stop
364
- # because the gate's literal `mcp__admin__pre-turn-graph-pass-skip` pattern
365
- # did not fullmatch the plugin-prefixed `tool_use.name` actually written
366
- # to JSONL.
367
- # ---------------------------------------------------------------------------
368
- TR_NS=$(mktemp); TMPFILES+=("$TR_NS")
369
- write_transcript "$TR_NS" \
370
- "user|thanks|" \
371
- "assistant|noted|tool_use:mcp__plugin_admin_admin__pre-turn-graph-pass-skip" \
372
- "assistant|you're welcome|"
373
- ENV_NS=$(envelope_for "$OP_ID" "$TR_NS")
374
- : > "$REQ_LOG"
375
- run_hook "admin" "" "$ENV_NS"
376
- [[ "$HOOK_RC" -eq 0 ]] || fail "case-11 expected rc=0, got $HOOK_RC"
377
- [[ -z "$HOOK_STDERR" ]] || fail "case-11 stderr must be empty on release"
378
- if ingest_lines | grep -qE "^gate-released sessionId=${OP_ID} via=skip-sentinel tool=mcp__plugin_admin_admin__pre-turn-graph-pass-skip$"; then
379
- pass "case-11 plugin-namespaced skip-sentinel → gate-released via=skip-sentinel"
380
- else
381
- fail "case-11 expected gate-released via=skip-sentinel under plugin namespace, got: $(ingest_lines)"
382
- fi
383
-
384
- # ---------------------------------------------------------------------------
385
- echo
386
- echo "per-turn-graph-pass-gate tests: $PASS passed, $FAIL failed"
387
- [[ "$FAIL" -eq 0 ]]