@rubytech/create-maxy-code 0.1.163 → 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.
- package/package.json +1 -1
- package/payload/platform/neo4j/edge-annotations.json +4 -0
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +1 -6
- package/payload/platform/plugins/admin/hooks/lib/admin-graph-pass-common.sh +12 -15
- package/payload/platform/plugins/admin/mcp/dist/index.js +0 -41
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/email/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +8 -12
- package/payload/platform/plugins/email/mcp/dist/index.js +8 -34
- package/payload/platform/plugins/email/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.d.ts +39 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.js +0 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-archive.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts +23 -1
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js +131 -3
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-dispatch.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.d.ts +19 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.js +51 -0
- package/payload/platform/plugins/email/mcp/dist/lib/conversation-archive-lookup.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts +0 -18
- package/payload/platform/plugins/email/mcp/dist/lib/graph.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js +0 -12
- package/payload/platform/plugins/email/mcp/dist/lib/graph.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts +88 -0
- package/payload/platform/plugins/email/mcp/dist/lib/imap.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js +189 -1
- package/payload/platform/plugins/email/mcp/dist/lib/imap.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js +6 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-fetch.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts +7 -15
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js +206 -86
- package/payload/platform/plugins/email/mcp/dist/tools/email-graph-query.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +104 -4
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-read.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-read.js +18 -12
- package/payload/platform/plugins/email/mcp/dist/tools/email-read.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js +5 -4
- package/payload/platform/plugins/email/mcp/dist/tools/email-reply.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-search.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-search.js +13 -12
- package/payload/platform/plugins/email/mcp/dist/tools/email-search.js.map +1 -1
- package/payload/platform/plugins/email/references/email-reference.md +8 -8
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +2 -2
- package/payload/platform/plugins/memory/mcp/dist/index.js +4 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.js +1 -13
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js +3 -5
- package/payload/platform/plugins/memory/mcp/dist/lib/kd-classify-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.js +10 -4
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js +5 -6
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +21 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +6 -8
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +2 -1
- package/payload/platform/plugins/memory/references/transcript-formats/circleback.md +49 -0
- package/payload/platform/plugins/memory/references/transcript-formats/granola.md +50 -0
- package/payload/platform/plugins/memory/references/transcript-formats/otter.md +50 -0
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +1 -0
- package/payload/platform/plugins/memory/skills/conversation-archive-mcp/SKILL.md +170 -0
- package/payload/platform/scripts/seed-neo4j.sh +1 -3
- package/payload/platform/templates/specialists/agents/librarian.md +1 -0
- package/payload/platform/plugins/admin/hooks/__tests__/per-turn-graph-pass-gate.test.sh +0 -387
- package/payload/platform/plugins/admin/hooks/__tests__/pre-turn-graph-pass.test.sh +0 -441
- package/payload/platform/plugins/admin/hooks/per-turn-graph-pass-gate.sh +0 -111
- package/payload/platform/plugins/admin/hooks/pre-turn-graph-pass.sh +0 -171
|
@@ -229,15 +229,13 @@ cat > "$ACCOUNT_SETTINGS" << SETTINGS_EOF
|
|
|
229
229
|
"UserPromptSubmit": [
|
|
230
230
|
{
|
|
231
231
|
"hooks": [
|
|
232
|
-
{ "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
|
|
233
|
-
{ "type": "command", "command": "bash $HOOKS_PATH/pre-turn-graph-pass.sh" }
|
|
232
|
+
{ "type": "command", "command": "bash $HOOKS_PATH/archive-ingest-surface-gate.sh" }
|
|
234
233
|
]
|
|
235
234
|
}
|
|
236
235
|
],
|
|
237
236
|
"Stop": [
|
|
238
237
|
{
|
|
239
238
|
"hooks": [
|
|
240
|
-
{ "type": "command", "command": "bash $HOOKS_PATH/per-turn-graph-pass-gate.sh" },
|
|
241
239
|
{ "type": "command", "command": "bash $HOOKS_PATH/session-end-retrospective.sh" }
|
|
242
240
|
]
|
|
243
241
|
}
|
|
@@ -34,6 +34,7 @@ The dispatch brief from the admin agent names the input class. Map it to one ski
|
|
|
34
34
|
|---|---|---|
|
|
35
35
|
| Document (PDF / text / web page) | `document-ingest` (mode='document') | `:KnowledgeDocument` (keyed on `attachmentId`) |
|
|
36
36
|
| Conversation transcript (any source) | `conversation-archive` | `:ConversationArchive` (Task 397 — keyed on `conversationIdentity`) |
|
|
37
|
+
| Granola / Otter / Circleback meeting ingest (provider MCP connected to the host) | `conversation-archive-mcp` | `:ConversationArchive` (same identity formula as `conversation-archive`; source ∈ {granola, otter, circleback}; per-provider meeting ID and `calendarEventId` stamped as cross-references) |
|
|
37
38
|
| Per-row enrich pass over an existing conversation-document parent | `conversation-archive-enrich` | (mutates existing `:Section` chunks) |
|
|
38
39
|
| LinkedIn Basic Data Export directory | `linkedin-import` | flat-dataset MERGE via `memory-archive-write` |
|
|
39
40
|
| Post-unzip tree forwarded by admin | route by detected shape (LinkedIn export folder → `linkedin-import`; loose docs → `document-ingest`; transcript files → `conversation-archive`) | per branch |
|
|
@@ -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 ]]
|