@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
|
@@ -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,111 +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: any MCP-prefixed skip-sentinel name (the admin MCP
|
|
62
|
-
# server can surface as either `mcp__admin__…` when wired via .mcp.json or
|
|
63
|
-
# `mcp__plugin_admin_admin__…` when loaded via the Claude Code plugin
|
|
64
|
-
# system — both forms appear in JSONL `tool_use.name` and both release the
|
|
65
|
-
# gate), or a Task tool_use with `input.subagent_type=database-operator`
|
|
66
|
-
# (matched as the synthetic name `Task:database-operator` by the helper).
|
|
67
|
-
# Same defect class Task 417 fixed for the session-end retrospective gate,
|
|
68
|
-
# generalised here to cover the plugin-system namespace.
|
|
69
|
-
RELEASE_PATTERN='mcp__[a-z_]+__pre-turn-graph-pass-skip|Task:database-operator'
|
|
70
|
-
|
|
71
|
-
INSPECTION=$(agp_scan_post_user_toolnames "$RELEASE_PATTERN")
|
|
72
|
-
USER_PRESENT="${INSPECTION%% *}"
|
|
73
|
-
MATCHED_NAME="${INSPECTION#* }"
|
|
74
|
-
|
|
75
|
-
if [ "$USER_PRESENT" != "yes" ]; then
|
|
76
|
-
# No real-user message in the transcript yet — e.g. the SessionStart
|
|
77
|
-
# bootstrap turn, an empty transcript, or a session that only contains
|
|
78
|
-
# tool_result-shaped user records. Nothing to gate against.
|
|
79
|
-
agp_emit_log "trigger-skipped sessionId=${SESSION_ID} reason=no-real-user"
|
|
80
|
-
exit 0
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
if [ -n "$MATCHED_NAME" ]; then
|
|
84
|
-
case "$MATCHED_NAME" in
|
|
85
|
-
Task:database-operator)
|
|
86
|
-
VIA="dispatch"
|
|
87
|
-
;;
|
|
88
|
-
mcp__*__pre-turn-graph-pass-skip)
|
|
89
|
-
VIA="skip-sentinel"
|
|
90
|
-
;;
|
|
91
|
-
*)
|
|
92
|
-
VIA="unknown"
|
|
93
|
-
;;
|
|
94
|
-
esac
|
|
95
|
-
agp_emit_log "gate-released sessionId=${SESSION_ID} via=${VIA} tool=${MATCHED_NAME}"
|
|
96
|
-
exit 0
|
|
97
|
-
fi
|
|
98
|
-
|
|
99
|
-
agp_emit_log "gate-blocked sessionId=${SESSION_ID} reason=sentinel-absent"
|
|
100
|
-
|
|
101
|
-
cat >&2 <<'INSTRUCTION'
|
|
102
|
-
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.
|
|
103
|
-
|
|
104
|
-
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.
|
|
105
|
-
|
|
106
|
-
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.
|
|
107
|
-
|
|
108
|
-
Either path releases this gate and lets the turn close. Do not surface this dispatch in your reply to the operator.
|
|
109
|
-
INSTRUCTION
|
|
110
|
-
|
|
111
|
-
exit 2
|