@rubytech/create-maxy-code 0.1.69 → 0.1.71
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/config/brand.json +1 -0
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +163 -102
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +127 -13
- package/payload/platform/plugins/docs/references/admin-session.md +22 -18
- package/payload/platform/plugins/docs/references/platform.md +4 -0
- package/payload/platform/plugins/memory/PLUGIN.md +9 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js +116 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/resolve-active-vertical.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js +100 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-loader.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js +197 -5
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-validator.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.d.ts +33 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.js +44 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/resolve-active-vertical.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts +29 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js +57 -5
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-loader.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts +9 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js +36 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/schema-validator.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js +7 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-personfields-open.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +22 -11
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +2 -2
- package/payload/platform/scripts/lib/read-brand-json.sh +69 -0
- package/payload/platform/scripts/seed-neo4j.sh +67 -8
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +73 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +8 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +49 -4
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +5 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +11 -3
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +45 -20
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +7 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +5 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +5 -28
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +13 -5
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/build.md +186 -14
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/registers.md +3 -2
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/template.html +3 -1
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-extract/SKILL.md +2 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-socials/SKILL.md +14 -13
- package/payload/server/public/assets/{admin-BPRjc4z9.js → admin-Bej-Q1IZ.js} +1 -1
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Asw7tNG5.js → architectureDiagram-Q4EWVU46-umb1qQ5e.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-Blvay4_b.js → blockDiagram-DXYQGD6D-C8kHw50g.js} +1 -1
- package/payload/server/public/assets/{brand-CwHvVF8s.css → brand-DxX912hX.css} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-C2st6jZP.js → c4Diagram-AHTNJAMY-CtKxKvPu.js} +1 -1
- package/payload/server/public/assets/channel-BshbIIpE.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-Cp7B2yh1.js → chunk-336JU56O-_xUA1Yj-.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-B0x3RXvj.js → chunk-426QAEUC-d_A8vQqM.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-B0yM0pgq.js → chunk-4TB4RGXK-BgkVBpN_.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-CqGhfArO.js → chunk-5FUZZQ4R-CGOqTZDS.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-jSeD3Yrj.js → chunk-5PVQY5BW-CW78oYeu.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-C7HuvG2r.js → chunk-EDXVE4YY-D_w_ZcbQ.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-DzCrge6H.js → chunk-ENJZ2VHE-MIcdFlwQ.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-BPvCh0Yc.js → chunk-ICPOFSXX-BaRHp6jX.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-DamxwrTn.js → chunk-OYMX7WX6-Cgo3QttE.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-DDiVd0qV.js → chunk-U2HBQHQK-BVZMJI1z.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-CDof681v.js → chunk-X2U36JSP-DdgOyRDq.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-DYzaYjrN.js → chunk-YZCP3GAM-CO61zu06.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-j0bcHHzU.js → chunk-ZZ45TVLE-B3BNOUKS.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-mzmkY4eP.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Co3bBdih.js +1 -0
- package/payload/server/public/assets/clone-BRSMOear.js +1 -0
- package/payload/server/public/assets/{dagre-C8nbdG6y.js → dagre-BY0FcwR1.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-1RnGN_fy.js → dagre-KV5264BT-YvKOR5T0.js} +1 -1
- package/payload/server/public/assets/data-LkSd6fEQ.js +1 -0
- package/payload/server/public/assets/{device-url-actions-C_Keyr0L.js → device-url-actions-Cq5VIBKc.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-C6d3Rrui.js → diagram-5BDNPKRD-DL3SiNlA.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-aD4Dik01.js → diagram-G4DWMVQ6-C_IwINPH.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-CAub49YE.js → diagram-MMDJMWI5-vumL8N8U.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-CAKnAfEr.js → diagram-TYMM5635-ewBaf58r.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-BrqOworC.js → erDiagram-SMLLAGMA-kiidRkW3.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-DV4eU-T2.js → flowDiagram-DWJPFMVM-C-1rI_Iw.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DayXUN42.js → ganttDiagram-T4ZO3ILL-B_9YdwWh.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CK1xV1OV.js → gitGraphDiagram-UUTBAWPF-BmwivVe3.js} +1 -1
- package/payload/server/public/assets/graph-CPXz4A4I.js +1 -0
- package/payload/server/public/assets/graph-labels-CZR9j5Ge.js +1 -0
- package/payload/server/public/assets/{graphlib-CkF9o1XT.js → graphlib-Dvt3mNtm.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-CiepM2H6.js → infoDiagram-42DDH7IO-B2n7mCmp.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-DwNkdQ6z.js → ishikawaDiagram-UXIWVN3A-C2ejJadN.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-BlDLMfu_.js → journeyDiagram-VCZTEJTY-BaIXYVwB.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-ayZTc0Oj.js → kanban-definition-6JOO6SKY-B1NCV7hH.js} +1 -1
- package/payload/server/public/assets/{line-LhPUiuH-.js → line-BEjGPKWc.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-DyZYsQv_.js → mermaid-parser.core-DwLM_Bkq.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-CauxgRdz.js → mermaid.core-BwedTSna.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CQzcaJuh.js → mindmap-definition-QFDTVHPH-D6q_XhF7.js} +1 -1
- package/payload/server/public/assets/{page-DAw3-U_s.js → page-BBRZF8Z8.js} +1 -1
- package/payload/server/public/assets/{page-BGv81dGN.js → page-CLr-z4E2.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-BPhAeTm9.js → pieDiagram-DEJITSTG-CAKQ-eUh.js} +1 -1
- package/payload/server/public/assets/{public-CGI24wrL.js → public-CfVPuA1x.js} +3 -3
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-CxrzL3ZF.js → quadrantDiagram-34T5L4WZ-B8_Xl5p8.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-ZBSC4fGX.js → requirementDiagram-MS252O5E-BWSq9eFs.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-BTDBEisC.js → sankeyDiagram-XADWPNL6-V3uWSRXp.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-Bv7fA5_J.js → sequenceDiagram-FGHM5R23-DJ_F3-vT.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-D07lcZrk.js → stateDiagram-FHFEXIEX-odlwuCTP.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DE5qFFDY.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-DyeyUqKa.js → timeline-definition-GMOUNBTQ-DXE7m_zC.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-B9FqDmwS.js → vennDiagram-DHZGUBPP-DEiEepoV.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-4-5uCIAD.js → wardleyDiagram-NUSXRM2D-CRhVx95h.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-WdkP5T_y.js → xychartDiagram-5P7HB3ND-9Gv4VNaG.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +5 -5
- package/payload/server/public/index.html +7 -7
- package/payload/server/public/public.html +4 -4
- package/payload/server/public/assets/channel-DTh_uFbz.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-Dsupydrn.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DrwAXnBf.js +0 -1
- package/payload/server/public/assets/clone-COx5mf-H.js +0 -1
- package/payload/server/public/assets/data-DOI26n9p.js +0 -1
- package/payload/server/public/assets/graph-DP9P7TKZ.js +0 -1
- package/payload/server/public/assets/graph-labels-BoLyECfJ.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Cr1Qet-f.js +0 -1
- /package/payload/server/public/assets/{brand-B8JhGW9e.js → brand-BT-mNX7-.js} +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Task 129/131/165/175/177 regression test for the Stop hook.
|
|
2
|
+
# Task 129/131/165/175/177/195 regression test for the Stop hook.
|
|
3
3
|
#
|
|
4
|
-
# Contract:
|
|
4
|
+
# Contract (post-195):
|
|
5
5
|
# - Every log emission goes through `POST /api/admin/log-ingest`. Hook
|
|
6
6
|
# stderr is silent on every path.
|
|
7
7
|
# - Every gated-off path emits one `trigger-skipped` line carrying a
|
|
@@ -11,14 +11,17 @@
|
|
|
11
11
|
# `conversationBytes=<n>` and exactly ONE POST to
|
|
12
12
|
# `/api/admin/claude-sessions`. The body carries the operator
|
|
13
13
|
# session id as `adminSessionId` and an `initialMessage` whose value
|
|
14
|
-
# is
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
14
|
+
# is the FILLED database-operator paragraph — the four-sentence
|
|
15
|
+
# template with `{schema}` replaced by `schema-base.md` (plus the
|
|
16
|
+
# brand-declared vertical when set) and `{conversation}` replaced by
|
|
17
|
+
# a flat `role: text` rendering of the envelope's `turns` array with
|
|
18
|
+
# inline `[tool: name(args)]` markers on assistant turns that called
|
|
19
|
+
# tools. The initialMessage is a plain string, not a JSON-stringified
|
|
20
|
+
# envelope; no instruction prose, no leading JSON.
|
|
21
|
+
# - The hook emits exactly one `[turn-recorder] envelope ...` line and
|
|
22
|
+
# one `[turn-recorder] substitution ...` line before `spawn-request`.
|
|
23
|
+
# The substitution line carries `schemaBytes`, `conversationBytes`,
|
|
24
|
+
# and `bodyBytes` so the per-spawn substitution is auditable.
|
|
22
25
|
# - The hook never writes the legacy `spawn-with-input` / `fired` stderr
|
|
23
26
|
# lines.
|
|
24
27
|
#
|
|
@@ -219,57 +222,88 @@ else
|
|
|
219
222
|
fail "case-5b expected exactly 1 recorder-spawn POST, got $RECORDER_LINES"
|
|
220
223
|
fi
|
|
221
224
|
|
|
222
|
-
# 5c.
|
|
223
|
-
#
|
|
224
|
-
#
|
|
225
|
-
#
|
|
225
|
+
# 5c. (Task 195) initialMessage is the FILLED four-sentence paragraph.
|
|
226
|
+
# The template's `{schema}` is replaced by schema-base.md content;
|
|
227
|
+
# `{conversation}` is replaced by `role: text` lines for every turn.
|
|
228
|
+
# The string is plain text, not a JSON envelope. The template's
|
|
229
|
+
# opening sentence and closing sentence are present verbatim; the
|
|
230
|
+
# four operator/assistant texts each appear on their own line.
|
|
226
231
|
RECORDER_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
227
232
|
BODY_OK=$(printf '%s' "$RECORDER_BODY" | python3 -c '
|
|
228
|
-
import sys, json
|
|
233
|
+
import sys, json
|
|
229
234
|
op = sys.argv[1]
|
|
230
|
-
acct = sys.argv[2]
|
|
231
235
|
try:
|
|
232
236
|
outer = json.load(sys.stdin)
|
|
233
|
-
|
|
234
|
-
if not isinstance(
|
|
235
|
-
print("no:initialMessage-not-string:"+repr(type(
|
|
237
|
+
msg = outer.get("initialMessage")
|
|
238
|
+
if not isinstance(msg, str):
|
|
239
|
+
print("no:initialMessage-not-string:"+repr(type(msg).__name__))
|
|
236
240
|
sys.exit(0)
|
|
237
|
-
msg = json.loads(msg_str)
|
|
238
|
-
keys = set(msg.keys())
|
|
239
|
-
turns = msg.get("turns") or []
|
|
240
241
|
conds = []
|
|
241
|
-
conds.append(("
|
|
242
|
-
|
|
243
|
-
conds.append(("
|
|
244
|
-
|
|
245
|
-
conds.append(("
|
|
246
|
-
conds.append(("no-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
conds.append(("t3-text", turns[3].get("text") == "One question to anchor the next move: what is your name?"))
|
|
242
|
+
conds.append(("starts-with-template",
|
|
243
|
+
msg.startswith("You are an expert Neo4J graph operator.")))
|
|
244
|
+
conds.append(("ends-with-template",
|
|
245
|
+
msg.rstrip().endswith("do not emit it.")))
|
|
246
|
+
conds.append(("no-placeholder-schema", "{schema}" not in msg))
|
|
247
|
+
conds.append(("no-placeholder-conversation", "{conversation}" not in msg))
|
|
248
|
+
# schema-base.md content is interpolated.
|
|
249
|
+
conds.append(("contains-schema-base-header", "Schema Reference" in msg))
|
|
250
|
+
# The conversation lines appear in role: text format.
|
|
251
|
+
conds.append(("has-user-1", "\nuser: New Real Agent session" in msg))
|
|
252
|
+
conds.append(("has-asst-1",
|
|
253
|
+
"\nassistant: Session noted. What would you like to work on?" in msg))
|
|
254
|
+
conds.append(("has-user-2", "\nuser: you tell me" in msg))
|
|
255
|
+
conds.append(("has-asst-2",
|
|
256
|
+
"\nassistant: One question to anchor the next move: what is your name?"
|
|
257
|
+
in msg))
|
|
258
|
+
conds.append(("no-leading-json", not msg.lstrip().startswith("{")))
|
|
259
|
+
conds.append(("no-operatorMessage", "operatorMessage" not in msg))
|
|
260
|
+
conds.append(("no-assistantReply", "assistantReply" not in msg))
|
|
261
261
|
conds.append(("outer-specialist", outer.get("specialist") == "database-operator"))
|
|
262
262
|
conds.append(("outer-model", outer.get("model") == "haiku"))
|
|
263
263
|
conds.append(("outer-adminSessionId", outer.get("adminSessionId") == op))
|
|
264
|
+
conds.append(("outer-channel", outer.get("channel") == "browser"))
|
|
264
265
|
failed = [name for name, ok in conds if not ok]
|
|
265
|
-
print("yes" if not failed else "no:"+repr(failed)
|
|
266
|
+
print("yes" if not failed else "no:"+repr(failed))
|
|
266
267
|
except Exception as e:
|
|
267
268
|
print("parse-fail:"+str(e))
|
|
268
|
-
' "$OP_ID"
|
|
269
|
+
' "$OP_ID" 2>/dev/null)
|
|
269
270
|
if [[ "$BODY_OK" == "yes" ]]; then
|
|
270
|
-
pass "case-5c
|
|
271
|
+
pass "case-5c initialMessage is the filled four-sentence paragraph; placeholders substituted; transcript rendered as role: text lines"
|
|
272
|
+
else
|
|
273
|
+
fail "case-5c filled-prompt shape wrong ($BODY_OK)"
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
# 5h. (Task 195) exactly one `substitution` log-ingest line carrying
|
|
277
|
+
# positive byte counts for schema / conversation / body.
|
|
278
|
+
SUBST_LINE_COUNT=$(ingest_lines | grep -cE "^substitution sessionId=${OP_ID} schemaBytes=[1-9][0-9]* conversationBytes=[1-9][0-9]* bodyBytes=[1-9][0-9]*$" || true)
|
|
279
|
+
if [[ "$SUBST_LINE_COUNT" -eq 1 ]]; then
|
|
280
|
+
pass "case-5h substitution log line emitted once with positive byte counts"
|
|
281
|
+
else
|
|
282
|
+
fail "case-5h expected exactly 1 substitution line w/ positive bytes, got $SUBST_LINE_COUNT (lines: $(ingest_lines))"
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
# 5i. (Task 195) substitution line lands between envelope and spawn-request.
|
|
286
|
+
ALL_5I_LINES=$(ingest_lines)
|
|
287
|
+
ENV_POS_5I=$(printf '%s\n' "$ALL_5I_LINES" | grep -nE "^envelope sessionId=${OP_ID}" | head -1 | cut -d: -f1)
|
|
288
|
+
SUB_POS_5I=$(printf '%s\n' "$ALL_5I_LINES" | grep -nE "^substitution sessionId=${OP_ID}" | head -1 | cut -d: -f1)
|
|
289
|
+
SPAWN_POS_5I=$(printf '%s\n' "$ALL_5I_LINES" | grep -nE "^spawn-request sessionId=${OP_ID}" | head -1 | cut -d: -f1)
|
|
290
|
+
if [[ -n "$ENV_POS_5I" && -n "$SUB_POS_5I" && -n "$SPAWN_POS_5I" \
|
|
291
|
+
&& "$ENV_POS_5I" -lt "$SUB_POS_5I" && "$SUB_POS_5I" -lt "$SPAWN_POS_5I" ]]; then
|
|
292
|
+
pass "case-5i envelope < substitution < spawn-request ordering preserved"
|
|
271
293
|
else
|
|
272
|
-
fail "case-
|
|
294
|
+
fail "case-5i ordering wrong (env=$ENV_POS_5I sub=$SUB_POS_5I spawn=$SPAWN_POS_5I)"
|
|
295
|
+
fi
|
|
296
|
+
|
|
297
|
+
# 5j. (Task 195) `initialMessageBytes` on spawn-request equals `bodyBytes`
|
|
298
|
+
# on substitution — the two log lines are reading the same number.
|
|
299
|
+
BODY_BYTES_5J=$(printf '%s\n' "$ALL_5I_LINES" | grep -E "^substitution sessionId=${OP_ID}" | head -1 \
|
|
300
|
+
| sed -E 's/.*bodyBytes=([0-9]+).*/\1/')
|
|
301
|
+
INIT_BYTES_5J=$(printf '%s\n' "$ALL_5I_LINES" | grep -E "^spawn-request sessionId=${OP_ID}" | head -1 \
|
|
302
|
+
| sed -E 's/.*initialMessageBytes=([0-9]+).*/\1/')
|
|
303
|
+
if [[ -n "$BODY_BYTES_5J" && "$BODY_BYTES_5J" == "$INIT_BYTES_5J" ]]; then
|
|
304
|
+
pass "case-5j substitution.bodyBytes == spawn-request.initialMessageBytes (=$BODY_BYTES_5J)"
|
|
305
|
+
else
|
|
306
|
+
fail "case-5j bodyBytes ($BODY_BYTES_5J) does not match initialMessageBytes ($INIT_BYTES_5J)"
|
|
273
307
|
fi
|
|
274
308
|
|
|
275
309
|
# 5d. exactly ONE `spawn-request` log-ingest line
|
|
@@ -307,12 +341,12 @@ else
|
|
|
307
341
|
pass "case-5g no legacy stderr emissions"
|
|
308
342
|
fi
|
|
309
343
|
|
|
310
|
-
# --- Case 6 (Task
|
|
311
|
-
# Two assistant
|
|
312
|
-
#
|
|
313
|
-
#
|
|
314
|
-
#
|
|
315
|
-
#
|
|
344
|
+
# --- Case 6 (Task 195): multi-block assistant collapse ----------------
|
|
345
|
+
# Two assistant records sharing `message.id`: thinking-only then text.
|
|
346
|
+
# The walker collapses both into ONE assistant turn whose text is the
|
|
347
|
+
# text-record's text. The filled prompt's conversation portion has
|
|
348
|
+
# exactly two transcript lines: `user: think then answer` and
|
|
349
|
+
# `assistant: answer is 42`.
|
|
316
350
|
MULTI_TRANSCRIPT=$(mktemp); TMPFILES+=("$MULTI_TRANSCRIPT")
|
|
317
351
|
{
|
|
318
352
|
printf '{"type":"user","timestamp":"2026-05-19T21:30:00.000Z","message":{"role":"user","content":"think then answer"}}\n'
|
|
@@ -331,23 +365,31 @@ MULTI_OK=$(printf '%s' "$MULTI_BODY" | python3 -c '
|
|
|
331
365
|
import sys, json
|
|
332
366
|
try:
|
|
333
367
|
outer = json.load(sys.stdin)
|
|
334
|
-
msg =
|
|
335
|
-
|
|
368
|
+
msg = outer.get("initialMessage")
|
|
369
|
+
if not isinstance(msg, str):
|
|
370
|
+
print("no:initialMessage-not-string")
|
|
371
|
+
sys.exit(0)
|
|
336
372
|
conds = []
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
373
|
+
# Filled-prompt sanity.
|
|
374
|
+
conds.append(("starts-template", msg.startswith("You are an expert Neo4J graph operator.")))
|
|
375
|
+
conds.append(("no-placeholder-conv", "{conversation}" not in msg))
|
|
376
|
+
# Conversation lines.
|
|
377
|
+
conds.append(("has-user", "\nuser: think then answer" in msg))
|
|
378
|
+
conds.append(("has-asst", "\nassistant: answer is 42" in msg))
|
|
379
|
+
# Thinking content does NOT bleed in.
|
|
380
|
+
conds.append(("no-thinking-text", "considering..." not in msg))
|
|
381
|
+
# Exactly two transcript lines (one per turn) — no duplicate assistant.
|
|
382
|
+
user_lines = msg.count("\nuser: ")
|
|
383
|
+
asst_lines = msg.count("\nassistant: ")
|
|
384
|
+
conds.append(("one-user-line", user_lines == 1))
|
|
385
|
+
conds.append(("one-asst-line", asst_lines == 1))
|
|
344
386
|
failed = [name for name, ok in conds if not ok]
|
|
345
|
-
print("yes" if not failed else "no:"+repr(failed)
|
|
387
|
+
print("yes" if not failed else "no:"+repr(failed))
|
|
346
388
|
except Exception as e:
|
|
347
389
|
print("parse-fail:"+str(e))
|
|
348
390
|
' 2>/dev/null)
|
|
349
391
|
if [[ "$MULTI_OK" == "yes" ]]; then
|
|
350
|
-
pass "case-6 multi-block assistant collapses to one
|
|
392
|
+
pass "case-6 multi-block assistant collapses to one transcript line; thinking content does not bleed in"
|
|
351
393
|
else
|
|
352
394
|
fail "case-6 multi-block collapse wrong ($MULTI_OK): $MULTI_BODY"
|
|
353
395
|
fi
|
|
@@ -387,30 +429,35 @@ TOOL_OK=$(printf '%s' "$TOOL_BODY" | python3 -c '
|
|
|
387
429
|
import sys, json
|
|
388
430
|
try:
|
|
389
431
|
outer = json.load(sys.stdin)
|
|
390
|
-
msg =
|
|
391
|
-
|
|
432
|
+
msg = outer.get("initialMessage")
|
|
433
|
+
if not isinstance(msg, str):
|
|
434
|
+
print("no:initialMessage-not-string")
|
|
435
|
+
sys.exit(0)
|
|
392
436
|
conds = []
|
|
393
|
-
#
|
|
394
|
-
#
|
|
395
|
-
conds.append(("
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
437
|
+
# The user turn carrying only a tool_result becomes no transcript line;
|
|
438
|
+
# so the filled prompt should have exactly ONE `user:` line.
|
|
439
|
+
conds.append(("one-user-line", msg.count("\nuser: ") == 1))
|
|
440
|
+
conds.append(("has-user-prompt", "\nuser: please write a node" in msg))
|
|
441
|
+
# Two assistant lines: one carrying the tool_use marker (with empty
|
|
442
|
+
# text body), one carrying "done".
|
|
443
|
+
conds.append(("two-asst-lines", msg.count("\nassistant: ") == 2))
|
|
444
|
+
conds.append(("has-asst-done", "\nassistant: done" in msg))
|
|
445
|
+
# Tool-call marker on the owning assistant turn — name and full JSON
|
|
446
|
+
# input round-trip inline.
|
|
447
|
+
conds.append(("has-tool-marker", "[tool: mcp__memory__memory-write(" in msg))
|
|
448
|
+
conds.append(("tool-marker-name", "\"name\": \"Smalleys\"" in msg or "\"name\":\"Smalleys\"" in msg))
|
|
449
|
+
conds.append(("tool-marker-nested", "\"nested\"" in msg))
|
|
450
|
+
conds.append(("tool-marker-nodeType", "\"nodeType\": \"LocalBusiness\"" in msg or "\"nodeType\":\"LocalBusiness\"" in msg))
|
|
451
|
+
# The tool_result content does NOT bleed in (the rendering chose
|
|
452
|
+
# input-only markers; output goes nowhere in the filled prompt).
|
|
453
|
+
conds.append(("no-output-leakage", "ok: wrote LocalBusiness" not in msg))
|
|
407
454
|
failed = [name for name, ok in conds if not ok]
|
|
408
|
-
print("yes" if not failed else "no:"+repr(failed)
|
|
455
|
+
print("yes" if not failed else "no:"+repr(failed))
|
|
409
456
|
except Exception as e:
|
|
410
457
|
print("parse-fail:"+str(e))
|
|
411
458
|
' 2>/dev/null)
|
|
412
459
|
if [[ "$TOOL_OK" == "yes" ]]; then
|
|
413
|
-
pass "case-7 tool_use
|
|
460
|
+
pass "case-7 assistant tool_use renders as [tool: name(args)] marker on the owning turn; tool_result does not create a separate user line"
|
|
414
461
|
else
|
|
415
462
|
fail "case-7 tool pairing wrong ($TOOL_OK): $TOOL_BODY"
|
|
416
463
|
fi
|
|
@@ -449,23 +496,29 @@ LONG_OK=$(printf '%s' "$LONG_BODY" | python3 -c '
|
|
|
449
496
|
import sys, json
|
|
450
497
|
try:
|
|
451
498
|
outer = json.load(sys.stdin)
|
|
452
|
-
msg =
|
|
453
|
-
|
|
499
|
+
msg = outer.get("initialMessage")
|
|
500
|
+
if not isinstance(msg, str):
|
|
501
|
+
print("no:initialMessage-not-string")
|
|
502
|
+
sys.exit(0)
|
|
503
|
+
user_lines = msg.count("\nuser: ")
|
|
504
|
+
asst_lines = msg.count("\nassistant: ")
|
|
454
505
|
conds = []
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
506
|
+
# No windowing — every one of the 100 turns appears as a transcript line.
|
|
507
|
+
conds.append(("100-user", user_lines == 50))
|
|
508
|
+
conds.append(("100-asst", asst_lines == 50))
|
|
509
|
+
conds.append(("first-user-0", "\nuser: user msg 0" in msg))
|
|
510
|
+
conds.append(("last-user-49", "\nuser: user msg 49" in msg))
|
|
511
|
+
conds.append(("last-asst-49", "\nassistant: asst reply 49" in msg))
|
|
512
|
+
# The filled prompt is large — schema + 100 transcript lines easily
|
|
513
|
+
# exceeds 5_000 bytes even with a tiny schema.
|
|
514
|
+
conds.append(("body-large", len(msg.encode("utf-8")) > 5000))
|
|
462
515
|
failed = [name for name, ok in conds if not ok]
|
|
463
|
-
print("yes" if not failed else "no:"+repr(failed)+"|
|
|
516
|
+
print("yes" if not failed else "no:"+repr(failed)+"|users="+str(user_lines)+"|assts="+str(asst_lines))
|
|
464
517
|
except Exception as e:
|
|
465
518
|
print("parse-fail:"+str(e))
|
|
466
519
|
' 2>/dev/null)
|
|
467
520
|
if [[ "$LONG_OK" == "yes" ]]; then
|
|
468
|
-
pass "case-8 100-turn transcript →
|
|
521
|
+
pass "case-8 100-turn transcript → 50 user lines + 50 assistant lines in filled prompt; no truncation"
|
|
469
522
|
else
|
|
470
523
|
fail "case-8 long-conversation wrong ($LONG_OK)"
|
|
471
524
|
fi
|
|
@@ -522,24 +575,32 @@ run_hook "admin" "" "$ESC_ENVELOPE"
|
|
|
522
575
|
ESC_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
523
576
|
ESC_OK=$(printf '%s' "$ESC_BODY" | python3 -c '
|
|
524
577
|
import sys, json
|
|
525
|
-
|
|
526
|
-
|
|
578
|
+
# The user turn text contains literal quotes, a backslash-n (two chars),
|
|
579
|
+
# a real newline, an em-dash, and a fox emoji. The assistant text has
|
|
580
|
+
# two backslashes and a real newline.
|
|
581
|
+
EXPECTED_OP_FRAG_1 = "hello \"world\""
|
|
582
|
+
EXPECTED_OP_FRAG_2 = "last — café \U0001F98A"
|
|
583
|
+
EXPECTED_ASST_FRAG = "reply with backslash \\\\ and newline"
|
|
527
584
|
try:
|
|
528
585
|
outer = json.load(sys.stdin)
|
|
529
|
-
msg =
|
|
530
|
-
|
|
586
|
+
msg = outer.get("initialMessage")
|
|
587
|
+
if not isinstance(msg, str):
|
|
588
|
+
print("no:initialMessage-not-string")
|
|
589
|
+
sys.exit(0)
|
|
531
590
|
conds = []
|
|
532
|
-
conds.append(("
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
591
|
+
conds.append(("op-quotes", EXPECTED_OP_FRAG_1 in msg))
|
|
592
|
+
conds.append(("op-emoji", EXPECTED_OP_FRAG_2 in msg))
|
|
593
|
+
conds.append(("asst-backslash", EXPECTED_ASST_FRAG in msg))
|
|
594
|
+
conds.append(("user-prefix", "\nuser: " in msg))
|
|
595
|
+
conds.append(("asst-prefix", "\nassistant: " in msg))
|
|
596
|
+
conds.append(("template-intact", "You are an expert Neo4J graph operator." in msg))
|
|
536
597
|
failed = [name for name, ok in conds if not ok]
|
|
537
|
-
print("yes" if not failed else "no:"+repr(failed)
|
|
598
|
+
print("yes" if not failed else "no:"+repr(failed))
|
|
538
599
|
except Exception as e:
|
|
539
600
|
print("parse-fail:"+str(e))
|
|
540
601
|
' 2>/dev/null)
|
|
541
602
|
if [[ "$ESC_OK" == "yes" ]]; then
|
|
542
|
-
pass "case-10 escaping: quotes/backslashes/
|
|
603
|
+
pass "case-10 escaping: quotes / backslashes / em-dash / emoji survive interpolation into the filled prompt"
|
|
543
604
|
else
|
|
544
605
|
fail "case-10 escaping wrong ($ESC_OK): $ESC_BODY"
|
|
545
606
|
fi
|
|
@@ -283,26 +283,140 @@ print(f"{len(turns)} {u} {a} {tc}")
|
|
|
283
283
|
read -r TURNS_COUNT USER_TURNS ASST_TURNS TC_TURNS <<<"$ENVELOPE_COUNTS"
|
|
284
284
|
emit_log "envelope sessionId=${ADMIN_SESSION_ID} turnsCount=${TURNS_COUNT} userTurns=${USER_TURNS} assistantTurns=${ASST_TURNS} toolCallTurns=${TC_TURNS}"
|
|
285
285
|
|
|
286
|
-
#
|
|
287
|
-
#
|
|
288
|
-
#
|
|
289
|
-
# the
|
|
290
|
-
|
|
286
|
+
# Task 195 — substitute {schema} and {conversation} placeholders in the
|
|
287
|
+
# database-operator agent body before spawning. The agent file on disk
|
|
288
|
+
# carries the four-sentence template verbatim with both placeholders; the
|
|
289
|
+
# hook renders the actual schema text and a flat `role: text` conversation
|
|
290
|
+
# transcript here, then sends the filled paragraph as `initialMessage`.
|
|
291
|
+
# The model's first input is the filled prompt — no JSON envelope, no
|
|
292
|
+
# instruction prose, no placeholders.
|
|
293
|
+
HOOK_DIR_RESOLVED="$(cd "$(dirname "$0")" && pwd)"
|
|
294
|
+
PLATFORM_ROOT_RESOLVED="$(cd "${HOOK_DIR_RESOLVED}/../../.." && pwd)"
|
|
295
|
+
SCHEMA_BASE_PATH="${PLATFORM_ROOT_RESOLVED}/plugins/memory/references/schema-base.md"
|
|
296
|
+
BRAND_JSON_PATH="${PLATFORM_ROOT_RESOLVED}/config/brand.json"
|
|
297
|
+
AGENT_BODY_PATH="${PLATFORM_ROOT_RESOLVED}/templates/specialists/agents/database-operator.md"
|
|
298
|
+
|
|
299
|
+
if [ ! -f "$SCHEMA_BASE_PATH" ]; then
|
|
300
|
+
emit_log "spawn-failed sessionId=${ADMIN_SESSION_ID} reason=schema-base-missing path=${SCHEMA_BASE_PATH}"
|
|
301
|
+
exit 0
|
|
302
|
+
fi
|
|
303
|
+
if [ ! -f "$AGENT_BODY_PATH" ]; then
|
|
304
|
+
emit_log "spawn-failed sessionId=${ADMIN_SESSION_ID} reason=agent-body-missing path=${AGENT_BODY_PATH}"
|
|
305
|
+
exit 0
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
# Brand-declared vertical (Task 193). Treated as null when brand.json is
|
|
309
|
+
# absent, unparseable, lacks the field, or names a file that does not
|
|
310
|
+
# exist — every one of those collapses to "schema-base only", never
|
|
311
|
+
# loud-fails this hook. Task 193 itself is the loud-fail surface for
|
|
312
|
+
# brand-config defects.
|
|
313
|
+
VERTICAL_PATH=""
|
|
314
|
+
if [ -f "$BRAND_JSON_PATH" ]; then
|
|
315
|
+
VERTICAL_NAME=$(python3 -c '
|
|
316
|
+
import sys, json
|
|
317
|
+
try:
|
|
318
|
+
with open(sys.argv[1]) as f:
|
|
319
|
+
b = json.load(f)
|
|
320
|
+
v = b.get("vertical")
|
|
321
|
+
print(v if isinstance(v, str) and v else "")
|
|
322
|
+
except Exception:
|
|
323
|
+
print("")
|
|
324
|
+
' "$BRAND_JSON_PATH")
|
|
325
|
+
if [ -n "$VERTICAL_NAME" ]; then
|
|
326
|
+
CANDIDATE="${PLATFORM_ROOT_RESOLVED}/plugins/memory/references/${VERTICAL_NAME}.md"
|
|
327
|
+
if [ -f "$CANDIDATE" ]; then
|
|
328
|
+
VERTICAL_PATH="$CANDIDATE"
|
|
329
|
+
fi
|
|
330
|
+
fi
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
# One Python pass:
|
|
334
|
+
# - reads the agent file and strips the frontmatter to recover the
|
|
335
|
+
# paragraph (the literal four sentences with `{schema}` and
|
|
336
|
+
# `{conversation}` placeholders);
|
|
337
|
+
# - reads schema-base.md (and concatenates the vertical when set);
|
|
338
|
+
# - renders the envelope's `turns` array into a flat transcript — one
|
|
339
|
+
# line per turn, `role: text`, with inline `[tool: name(args)]`
|
|
340
|
+
# markers where the assistant called tools (so the model sees the
|
|
341
|
+
# tool-shape it has been calling without having to read JSON);
|
|
342
|
+
# - substitutes both placeholders in the paragraph;
|
|
343
|
+
# - emits one tab-separated line of byte counts, then the
|
|
344
|
+
# spawn-body JSON containing the filled paragraph as `initialMessage`.
|
|
345
|
+
# The envelope rides through a tmpfile (not stdin) so the python script
|
|
346
|
+
# can be sourced from the heredoc; the two stdin redirections would
|
|
347
|
+
# otherwise collide and Python would see an empty envelope.
|
|
348
|
+
ENVELOPE_FILE=$(mktemp)
|
|
349
|
+
printf '%s' "$ENVELOPE" > "$ENVELOPE_FILE"
|
|
350
|
+
COMBINED=$(python3 - \
|
|
351
|
+
"$AGENT_BODY_PATH" "$SCHEMA_BASE_PATH" "$VERTICAL_PATH" "$ADMIN_SESSION_ID" "$ENVELOPE_FILE" <<'PY'
|
|
291
352
|
import sys, json
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
353
|
+
|
|
354
|
+
agent_path, schema_path, vertical_path, sid, envelope_path = sys.argv[1:6]
|
|
355
|
+
|
|
356
|
+
with open(agent_path, "r", encoding="utf-8") as f:
|
|
357
|
+
raw = f.read()
|
|
358
|
+
parts = raw.split("---", 2)
|
|
359
|
+
body_template = parts[2].lstrip("\n").rstrip() if len(parts) >= 3 else raw.rstrip()
|
|
360
|
+
|
|
361
|
+
with open(schema_path, "r", encoding="utf-8") as f:
|
|
362
|
+
schema_text = f.read()
|
|
363
|
+
if vertical_path:
|
|
364
|
+
with open(vertical_path, "r", encoding="utf-8") as f:
|
|
365
|
+
schema_text = schema_text.rstrip() + "\n\n" + f.read()
|
|
366
|
+
|
|
367
|
+
with open(envelope_path, "r", encoding="utf-8") as f:
|
|
368
|
+
envelope = json.load(f)
|
|
369
|
+
turns = envelope.get("turns") or []
|
|
370
|
+
lines = []
|
|
371
|
+
for t in turns:
|
|
372
|
+
role = t.get("role", "?")
|
|
373
|
+
text_t = t.get("text", "") or ""
|
|
374
|
+
marks = []
|
|
375
|
+
for tc in (t.get("toolCalls") or []):
|
|
376
|
+
tool = tc.get("tool", "?")
|
|
377
|
+
inp = tc.get("input")
|
|
378
|
+
try:
|
|
379
|
+
inp_s = json.dumps(inp, ensure_ascii=False) if inp is not None else ""
|
|
380
|
+
except Exception:
|
|
381
|
+
inp_s = str(inp)
|
|
382
|
+
marks.append(f"[tool: {tool}({inp_s})]")
|
|
383
|
+
if marks:
|
|
384
|
+
lines.append(f"{role}: {text_t} {' '.join(marks)}".rstrip())
|
|
385
|
+
else:
|
|
386
|
+
lines.append(f"{role}: {text_t}")
|
|
387
|
+
# Wrap the transcript block in newlines so each turn lands on its own
|
|
388
|
+
# line in the filled prompt — the template's `{conversation}` sits
|
|
389
|
+
# mid-sentence, so the leading newline separates the first transcript
|
|
390
|
+
# line from the surrounding prose, and the trailing newline separates
|
|
391
|
+
# the last one from the closing `, using the tools at your disposal.`
|
|
392
|
+
conversation_text = "\n" + "\n".join(lines) + "\n"
|
|
393
|
+
|
|
394
|
+
filled = body_template.replace("{schema}", schema_text).replace("{conversation}", conversation_text)
|
|
395
|
+
|
|
396
|
+
schema_bytes = len(schema_text.encode("utf-8"))
|
|
397
|
+
conv_bytes = len(conversation_text.encode("utf-8"))
|
|
398
|
+
body_bytes = len(filled.encode("utf-8"))
|
|
399
|
+
|
|
400
|
+
spawn_body = {
|
|
295
401
|
"adminSessionId": sid,
|
|
296
402
|
"channel": "browser",
|
|
297
403
|
"specialist": "database-operator",
|
|
298
404
|
"model": "haiku",
|
|
299
|
-
"initialMessage":
|
|
405
|
+
"initialMessage": filled,
|
|
300
406
|
}
|
|
301
|
-
print(json.dumps(body))
|
|
302
|
-
' "$ADMIN_SESSION_ID" "$ENVELOPE" 2>/dev/null)
|
|
303
407
|
|
|
304
|
-
|
|
305
|
-
|
|
408
|
+
print(f"{schema_bytes}\t{conv_bytes}\t{body_bytes}")
|
|
409
|
+
print(json.dumps(spawn_body))
|
|
410
|
+
PY
|
|
411
|
+
)
|
|
412
|
+
rm -f "$ENVELOPE_FILE"
|
|
413
|
+
|
|
414
|
+
BYTES_LINE=$(printf '%s' "$COMBINED" | head -n1)
|
|
415
|
+
SPAWN_BODY=$(printf '%s' "$COMBINED" | tail -n +2)
|
|
416
|
+
IFS=$'\t' read -r SCHEMA_BYTES CONV_BYTES BODY_BYTES <<<"$BYTES_LINE"
|
|
417
|
+
|
|
418
|
+
emit_log "substitution sessionId=${ADMIN_SESSION_ID} schemaBytes=${SCHEMA_BYTES} conversationBytes=${CONV_BYTES} bodyBytes=${BODY_BYTES}"
|
|
419
|
+
emit_log "spawn-request sessionId=${ADMIN_SESSION_ID} specialist=database-operator initialMessageBytes=${BODY_BYTES}"
|
|
306
420
|
|
|
307
421
|
SPAWN_RES_FILE=$(mktemp)
|
|
308
422
|
SPAWN_HTTP=$(curl -sS -o "$SPAWN_RES_FILE" -w '%{http_code}' -X POST \
|
|
@@ -92,29 +92,32 @@ After every completed operator admin turn, the Stop hook `platform/plugins/admin
|
|
|
92
92
|
Body overrides relative to a plain Sidebar click:
|
|
93
93
|
- `specialist: 'database-operator'`
|
|
94
94
|
- `model: 'haiku'`
|
|
95
|
-
- `initialMessage: <
|
|
95
|
+
- `initialMessage: <filled-paragraph>` (the four-sentence database-operator template with `{schema}` and `{conversation}` substituted at hook time — see below)
|
|
96
96
|
|
|
97
97
|
**Spawn body.** The hook POSTs `{adminSessionId, channel: 'browser', specialist: 'database-operator', model: 'haiku', initialMessage}` to `POST /api/admin/claude-sessions`. `adminSessionId` is the loopback-bypass key — the wrapper resolves the operator's real `senderId` from `GET <managerBase>/<adminSessionId>/meta` and forwards a Sidebar-shape body to the manager.
|
|
98
98
|
|
|
99
|
-
**`initialMessage`
|
|
99
|
+
**`initialMessage` substitution contract (Task 195).** The agent body on disk is the user-specified four-sentence paragraph verbatim, with two placeholders `{schema}` and `{conversation}`:
|
|
100
100
|
|
|
101
101
|
```
|
|
102
|
-
{
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
"toolCalls": [ { "tool": "...", "input": {...}, "output": ... } ] },
|
|
107
|
-
...
|
|
108
|
-
],
|
|
109
|
-
"conversationId": "<operator session_id>",
|
|
110
|
-
"accountId": "<ACCOUNT_ID env, stamped on the manager systemd unit>",
|
|
111
|
-
"occurredAt": "<hook fire time, ISO-8601 UTC, .000Z>"
|
|
112
|
-
}
|
|
102
|
+
You are an expert Neo4J graph operator. Here is the schema {schema}.
|
|
103
|
+
Use your expert judgement to update the graph in reaction to the
|
|
104
|
+
following conversation {conversation}, using the tools at your disposal.
|
|
105
|
+
You are not user-facing and your text goes nowhere — do not emit it.
|
|
113
106
|
```
|
|
114
107
|
|
|
115
|
-
|
|
108
|
+
At fire time the Stop hook substitutes both placeholders and sends the filled paragraph as `initialMessage` — a plain string, not a JSON envelope, no instruction prose, no leading object.
|
|
116
109
|
|
|
117
|
-
|
|
110
|
+
- `{schema}` ← the concatenated text of `platform/plugins/memory/references/schema-base.md` plus, when `brand.json#vertical` names a vertical file that exists in the same directory, that file's content joined with a blank-line separator (Task 193). When the brand declares `vertical: null`, the field is missing, or the named file is absent, the schema is `schema-base.md` alone — never loud-fail here; Task 193 owns the brand-config gate.
|
|
111
|
+
- `{conversation}` ← a flat rendering of the envelope's `turns` array. Format: one line per turn, `role: text`, wrapped in leading and trailing newlines so each line lands on its own line in the filled prompt. Assistant turns that called tools carry inline `[tool: name(args)]` markers at the end of their line, with `args` being the call's input as a JSON literal. Tool outputs are not included — only the call shape the model has been emitting. The user record carrying only `tool_result` blocks does not create a separate transcript line; thinking-only records contribute nothing.
|
|
112
|
+
|
|
113
|
+
The envelope walker still runs (so `[turn-recorder] envelope ...` continues to land for observability and the `trigger-skipped reason=conversation-empty` gate still fires on a turns-empty transcript), but its JSON shape never reaches the model.
|
|
114
|
+
|
|
115
|
+
Walker semantics for `turns` (preserved from Task 177): oldest first, newest last; every user / assistant message in the operator's JSONL from session start to the moment the recorder fired; no windowing, no truncation, no env knob; multi-record assistant messages collapse on `message.id`; `tool_use` and `tool_result` blocks are paired by `tool_use_id` so the result attaches to the assistant turn that owned the call rather than fabricating a separate user turn; if `turns` is empty after the walker runs the hook emits `trigger-skipped reason=conversation-empty` and no recorder is spawned. The Task 177 envelope shape — `{turns, conversationId, accountId, occurredAt}` — is now an internal hook structure; it is not what the model sees.
|
|
116
|
+
|
|
117
|
+
The hook emits two summary lines per fire immediately before `spawn-request`:
|
|
118
|
+
|
|
119
|
+
- `[turn-recorder] envelope sessionId=<op> turnsCount=<n> userTurns=<n> assistantTurns=<n> toolCallTurns=<n>` — the walker's output. `turnsCount` is monotone across spawns in the same operator session; a flat or decreasing series is the regression signature for "windowing crept back in".
|
|
120
|
+
- `[turn-recorder] substitution sessionId=<op> schemaBytes=<n> conversationBytes=<n> bodyBytes=<n>` — the per-spawn substitution. `schemaBytes` covers schema-base plus any vertical concatenated. `bodyBytes` equals the trailing `initialMessageBytes` field on the next `spawn-request` line — the two log lines read the same number.
|
|
118
121
|
|
|
119
122
|
**Manager-side specialist branches.** The recorder is the first specialist subagent that exercises the full specialist flag matrix:
|
|
120
123
|
- The bundled `platform/templates/specialists/agents/database-operator.md` template is symlinked into `$CLAUDE_CONFIG_DIR/agents/database-operator.md` by the installer; without that link `claude --agent database-operator` silently falls back to the admin agent.
|
|
@@ -200,10 +203,11 @@ Three log-line shapes, all keyed on the same recorder `sessionId`, distinguish t
|
|
|
200
203
|
```
|
|
201
204
|
|
|
202
205
|
- `[pty-spawn-tool-inventory]` (Task 178) measures the **framework** layer — the argv built and the MCP servers' published tools. Argv-tools count must be 11; mcp-listed-tools must be ≥ 11.
|
|
203
|
-
- `[recorder-session-init]` (Task 183 — emitted by the JSONL tail in `claude-session-manager`) measures the **CLI/SDK** layer — what the SDK actually exposed to the model. The
|
|
206
|
+
- `[recorder-session-init]` (Task 183 — emitted by the JSONL tail in `claude-session-manager`) measures the **CLI/SDK** layer — what the SDK actually exposed to the model. The `source=` field names the data path; `toolsArgvMissingFromExposed` is the Outcome A fingerprint:
|
|
207
|
+
- `source=system.init` / `command_permissions` / `deferred_tools_delta` ⇒ the SDK wrote an init record to the JSONL and the tail matched one of the three known shapes. The bridge-attached `--remote-control` mode writes `attachment.deferred_tools_delta`; the headless SDK init record (`system.subtype=init`) is what `claude -p --output-format=stream-json` modes write.
|
|
208
|
+
- `source=headless-argv-fallback` ⇒ Task 185 finding (2026-05-20): headless `claude --verbose` PTY mode (which the database-operator recorder uses) does **not** write any tool-list record to the JSONL — none of the three probe shapes ever appears. The tail falls back to the spawner's `--allowed-tools` argv as the exposed set, independently verified by `[pty-spawn-tool-inventory]`'s `exposed=` field. Treat this source identically to a positive JSONL match: the argv set is authoritative for headless spawns.
|
|
204
209
|
- `toolsArgvMissingFromExposed=—` ⇒ all argv tools made it through; Outcome A is ruled out.
|
|
205
|
-
- Any non-`—` value enumerates the names the SDK dropped and is the diagnosis.
|
|
206
|
-
- `error=init-record-not-found probedTypes=system.init,command_permissions,deferred_tools_delta` ⇒ no init-shaped record was found in the JSONL by the time the first assistant turn landed. The miss itself is the observable; iterate the probe list against the live JSONL to learn the new record shape.
|
|
210
|
+
- Any non-`—` value enumerates the names the SDK dropped and is the diagnosis. Only meaningful when `source` is JSONL-derived; under `headless-argv-fallback` the exposed set equals argv by construction, so this field is always `—`.
|
|
207
211
|
- `[recorder-turn]` measures the **model** layer — Haiku's verbatim words (`textHead`) and the tool names it actually called (`toolUseNames`). `toolUseNames=—` paired with `toolsArgvMissingFromExposed=—` is the Outcome B signature: tools present, model abstained.
|
|
208
212
|
|
|
209
213
|
The three lines are emitted into `server.log` via the platform UI's `/api/admin/log-ingest` loopback route, so a single grep against `server.log` answers the question for any past, present, or future recorder spawn:
|