@rubytech/create-maxy-code 0.1.58 → 0.1.60
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/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +30 -19
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +35 -90
- package/payload/platform/plugins/docs/references/admin-session.md +17 -16
- package/payload/platform/plugins/docs/references/platform.md +3 -3
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/loop/PLUGIN.md +13 -1
- package/payload/platform/plugins/loop/mcp/dist/index.js +8 -7
- package/payload/platform/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/lib/file-crypto.d.ts +3 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/file-crypto.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/file-crypto.js +48 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/file-crypto.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/key-store.d.ts +22 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/key-store.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/key-store.js +165 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/key-store.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/loop-api.d.ts +1 -1
- package/payload/platform/plugins/loop/mcp/dist/lib/loop-api.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/lib/loop-api.js +4 -39
- package/payload/platform/plugins/loop/mcp/dist/lib/loop-api.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/key-deregister.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/key-deregister.js +5 -15
- package/payload/platform/plugins/loop/mcp/dist/tools/key-deregister.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/key-register.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/key-register.js +11 -28
- package/payload/platform/plugins/loop/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/src/index.ts +10 -7
- package/payload/platform/plugins/loop/mcp/src/lib/file-crypto.ts +54 -0
- package/payload/platform/plugins/loop/mcp/src/lib/key-store.ts +221 -0
- package/payload/platform/plugins/loop/mcp/src/lib/loop-api.ts +5 -42
- package/payload/platform/plugins/loop/mcp/src/tools/key-deregister.ts +5 -20
- package/payload/platform/plugins/loop/mcp/src/tools/key-register.ts +14 -39
- package/payload/platform/plugins/preval/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/preval/PLUGIN.md +39 -0
- package/payload/platform/plugins/preval/skills/property-preval/SKILL.md +78 -0
- package/payload/platform/plugins/preval/skills/property-preval/references/render.py +265 -0
- package/payload/platform/plugins/preval/skills/property-preval/references/template-inputs.schema.json +143 -0
- package/payload/platform/plugins/preval/skills/property-preval/references/template.html +253 -0
- package/payload/platform/plugins/property-data/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/property-data/PLUGIN.md +60 -2
- package/payload/platform/plugins/property-data/mcp/dist/index.js +95 -12
- package/payload/platform/plugins/property-data/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/lib/file-crypto.d.ts +3 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/file-crypto.d.ts.map +1 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/file-crypto.js +49 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/file-crypto.js.map +1 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/key-store.d.ts +15 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/key-store.d.ts.map +1 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/key-store.js +135 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/key-store.js.map +1 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/propertydata-api.d.ts +0 -1
- package/payload/platform/plugins/property-data/mcp/dist/lib/propertydata-api.d.ts.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/lib/propertydata-api.js +11 -22
- package/payload/platform/plugins/property-data/mcp/dist/lib/propertydata-api.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.d.ts +10 -0
- package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.d.ts.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.js +15 -2
- package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-deregister.d.ts.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-deregister.js +5 -15
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-deregister.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-register.d.ts.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-register.js +30 -23
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/src/__tests__/endpoints.test.ts +252 -0
- package/payload/platform/plugins/property-data/mcp/src/__tests__/schema-parity.test.ts +16 -2
- package/payload/platform/plugins/property-data/mcp/src/index.ts +171 -11
- package/payload/platform/plugins/property-data/mcp/src/lib/file-crypto.ts +55 -0
- package/payload/platform/plugins/property-data/mcp/src/lib/key-store.ts +181 -0
- package/payload/platform/plugins/property-data/mcp/src/lib/propertydata-api.ts +9 -24
- package/payload/platform/plugins/property-data/mcp/src/tools/endpoints.ts +22 -2
- package/payload/platform/plugins/property-data/mcp/src/tools/key-deregister.ts +5 -17
- package/payload/platform/plugins/property-data/mcp/src/tools/key-register.ts +29 -33
- package/payload/platform/plugins/property-data/skills/property-data/SKILL.md +19 -0
- 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 +25 -260
- 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 +3 -7
- 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 +6 -32
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +6 -24
- 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 +18 -91
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/reaper.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/reaper.js +0 -11
- package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts +0 -21
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-store.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/platform/templates/agents/admin/SOUL.md +1 -1
- package/payload/premium-plugins/real-agent/BUNDLE.md +5 -3
- package/payload/premium-plugins/real-agent/agents/negotiator.md +1 -1
- package/payload/premium-plugins/real-agent/agents/valuer.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/premium-plugins/real-agent/plugins/loop/PLUGIN.md +13 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js +8 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/file-crypto.d.ts +3 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/file-crypto.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/file-crypto.js +48 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/file-crypto.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/key-store.d.ts +22 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/key-store.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/key-store.js +165 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/key-store.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.d.ts +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.js +4 -39
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-deregister.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-deregister.js +5 -15
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-deregister.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-register.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-register.js +11 -28
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/index.ts +10 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/lib/file-crypto.ts +54 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/lib/key-store.ts +221 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/lib/loop-api.ts +5 -42
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/key-deregister.ts +5 -20
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/key-register.ts +14 -39
- package/payload/premium-plugins/real-agent/plugins/preval/.claude-plugin/plugin.json +8 -0
- package/payload/premium-plugins/real-agent/plugins/preval/PLUGIN.md +39 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/SKILL.md +78 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/render.py +265 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template-inputs.schema.json +143 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template.html +253 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/.claude-plugin/plugin.json +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/PLUGIN.md +60 -2
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/index.js +95 -12
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/file-crypto.d.ts +3 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/file-crypto.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/file-crypto.js +49 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/file-crypto.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/key-store.d.ts +15 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/key-store.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/key-store.js +135 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/key-store.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/propertydata-api.d.ts +0 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/propertydata-api.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/propertydata-api.js +11 -22
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/propertydata-api.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.d.ts +10 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.js +15 -2
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-deregister.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-deregister.js +5 -15
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-deregister.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-register.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-register.js +30 -23
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/__tests__/endpoints.test.ts +252 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/__tests__/schema-parity.test.ts +16 -2
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/index.ts +171 -11
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/lib/file-crypto.ts +55 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/lib/key-store.ts +181 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/lib/propertydata-api.ts +9 -24
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/tools/endpoints.ts +22 -2
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/tools/key-deregister.ts +5 -17
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/tools/key-register.ts +29 -33
- package/payload/premium-plugins/real-agent/plugins/property-data/skills/property-data/SKILL.md +19 -0
- package/payload/server/public/assets/{admin-2ZJmzJ_g.js → admin-Bp-BjBCX.js} +51 -51
- package/payload/server/public/assets/{data-DlmoKxmN.js → data-BGUAGVkV.js} +1 -1
- package/payload/server/public/assets/{graph-BVvcFSEQ.js → graph-MvYxZOBF.js} +1 -1
- package/payload/server/public/assets/{graph-labels-cCvgu38Y.js → graph-labels-D865qb3K.js} +1 -1
- package/payload/server/public/assets/{page-BnAxPkmg.js → page--hOVRrgN.js} +1 -1
- package/payload/server/public/assets/{page-BnaN3u86.js → page-C2b1nlOc.js} +1 -1
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/server.js +18 -83
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.d.ts +0 -51
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.d.ts.map +0 -1
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.js +0 -196
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.js.map +0 -1
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/package.json +0 -7
package/package.json
CHANGED
|
@@ -85,12 +85,12 @@ PY
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
run_hook() {
|
|
88
|
-
local role="$1"; local
|
|
88
|
+
local role="$1"; local specialist="$2"; local stdin_json="$3"
|
|
89
89
|
local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
|
|
90
90
|
local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
|
|
91
91
|
printf '%s' "$stdin_json" | \
|
|
92
92
|
MAXY_SESSION_ROLE="$role" \
|
|
93
|
-
|
|
93
|
+
MAXY_SPECIALIST="$specialist" \
|
|
94
94
|
MAXY_UI_INTERNAL_PORT="$LISTENER_PORT" \
|
|
95
95
|
bash "$HOOK" >"$stdout_file" 2>"$stderr_file"
|
|
96
96
|
HOOK_RC=$?
|
|
@@ -131,7 +131,7 @@ print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
|
|
|
131
131
|
|
|
132
132
|
# --- Case 1: role != admin → trigger-skipped reason=role-not-admin -----
|
|
133
133
|
: > "$REQ_LOG"
|
|
134
|
-
run_hook "public" "
|
|
134
|
+
run_hook "public" "" "$ENVELOPE"
|
|
135
135
|
[[ "$HOOK_RC" -eq 0 ]] || fail "case-1 rc=$HOOK_RC"
|
|
136
136
|
[[ -z "$HOOK_STDERR" ]] || fail "case-1 stderr must be empty, got: $HOOK_STDERR"
|
|
137
137
|
if ingest_lines | grep -qE '^trigger-skipped sessionId=.* reason=role-not-admin$'; then
|
|
@@ -145,20 +145,20 @@ else
|
|
|
145
145
|
pass "case-1 no recorder-spawn POST"
|
|
146
146
|
fi
|
|
147
147
|
|
|
148
|
-
# --- Case 2:
|
|
148
|
+
# --- Case 2: MAXY_SPECIALIST=database-operator → trigger-skipped is-recorder ----
|
|
149
149
|
: > "$REQ_LOG"
|
|
150
|
-
run_hook "admin" "
|
|
150
|
+
run_hook "admin" "database-operator" "$ENVELOPE"
|
|
151
151
|
[[ "$HOOK_RC" -eq 0 ]] || fail "case-2 rc=$HOOK_RC"
|
|
152
152
|
[[ -z "$HOOK_STDERR" ]] || fail "case-2 stderr must be empty, got: $HOOK_STDERR"
|
|
153
153
|
if ingest_lines | grep -qE '^trigger-skipped sessionId=.* reason=is-recorder$'; then
|
|
154
|
-
pass "case-2
|
|
154
|
+
pass "case-2 MAXY_SPECIALIST=database-operator → trigger-skipped reason=is-recorder emitted"
|
|
155
155
|
else
|
|
156
156
|
fail "case-2 expected trigger-skipped is-recorder, got: $(ingest_lines)"
|
|
157
157
|
fi
|
|
158
158
|
|
|
159
159
|
# --- Case 3: empty stdin → trigger-skipped reason=empty-stdin ------------
|
|
160
160
|
: > "$REQ_LOG"
|
|
161
|
-
run_hook "admin" "
|
|
161
|
+
run_hook "admin" "" ""
|
|
162
162
|
[[ "$HOOK_RC" -eq 0 ]] || fail "case-3 rc=$HOOK_RC"
|
|
163
163
|
if ingest_lines | grep -qE '^trigger-skipped sessionId=.* reason=empty-stdin$'; then
|
|
164
164
|
pass "case-3 empty stdin → trigger-skipped reason=empty-stdin emitted"
|
|
@@ -169,7 +169,7 @@ fi
|
|
|
169
169
|
# --- Case 4: missing transcript_path → trigger-skipped missing-transcript -
|
|
170
170
|
: > "$REQ_LOG"
|
|
171
171
|
BAD_ENVELOPE=$(python3 -c 'import json,sys; print(json.dumps({"session_id": sys.argv[1]}))' "$OP_ID")
|
|
172
|
-
run_hook "admin" "
|
|
172
|
+
run_hook "admin" "" "$BAD_ENVELOPE"
|
|
173
173
|
[[ "$HOOK_RC" -eq 0 ]] || fail "case-4 rc=$HOOK_RC"
|
|
174
174
|
if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=missing-transcript$"; then
|
|
175
175
|
pass "case-4 missing transcript → trigger-skipped reason=missing-transcript emitted"
|
|
@@ -179,7 +179,7 @@ fi
|
|
|
179
179
|
|
|
180
180
|
# --- Case 5: happy path -------------------------------------------------
|
|
181
181
|
: > "$REQ_LOG"
|
|
182
|
-
run_hook "admin" "
|
|
182
|
+
run_hook "admin" "" "$ENVELOPE"
|
|
183
183
|
[[ "$HOOK_RC" -eq 0 ]] || fail "case-5 rc=$HOOK_RC stderr=$HOOK_STDERR"
|
|
184
184
|
[[ -z "$HOOK_STDERR" ]] || fail "case-5 stderr must be empty, got: $HOOK_STDERR"
|
|
185
185
|
|
|
@@ -200,8 +200,10 @@ else
|
|
|
200
200
|
fail "case-5b expected exactly 1 recorder-spawn POST, got $RECORDER_LINES"
|
|
201
201
|
fi
|
|
202
202
|
|
|
203
|
-
# 5c.
|
|
204
|
-
#
|
|
203
|
+
# 5c. Sidebar-shape body with three overrides (specialist, model,
|
|
204
|
+
# initialMessage); adminSessionId carries the operator's session id as
|
|
205
|
+
# the loopback-bypass lookup key. No senderId on the wire (the wrapper
|
|
206
|
+
# resolves it from the manager's /meta).
|
|
205
207
|
RECORDER_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
206
208
|
BODY_OK=$(printf '%s' "$RECORDER_BODY" | python3 -c '
|
|
207
209
|
import sys, json
|
|
@@ -212,30 +214,39 @@ try:
|
|
|
212
214
|
b = json.load(sys.stdin)
|
|
213
215
|
msg = b.get("initialMessage") or ""
|
|
214
216
|
conds = (
|
|
215
|
-
# Task 134: no synthetic senderId marker — loopback bypass is
|
|
216
|
-
# method+path-scoped; the body only carries the recorder discriminators.
|
|
217
217
|
"senderId" not in b,
|
|
218
218
|
b.get("specialist") == "database-operator",
|
|
219
|
-
b.get("
|
|
219
|
+
b.get("model") == "haiku",
|
|
220
|
+
b.get("adminSessionId") == op,
|
|
221
|
+
b.get("channel") == "browser",
|
|
220
222
|
msg == INSTRUCTION + "\n\n" + EXPECTED_LAST,
|
|
221
223
|
"Conversation id:" not in msg,
|
|
222
224
|
"Conversation transcript:" not in msg,
|
|
225
|
+
"originatingSessionId" not in b,
|
|
223
226
|
)
|
|
224
227
|
print("yes" if all(conds) else "no:"+repr(conds)+"|msg="+repr(msg))
|
|
225
228
|
except Exception as e:
|
|
226
229
|
print("parse-fail:"+str(e))
|
|
227
230
|
' "$OP_ID" 2>/dev/null)
|
|
228
231
|
if [[ "$BODY_OK" == "yes" ]]; then
|
|
229
|
-
pass "case-5c
|
|
232
|
+
pass "case-5c body is Sidebar-shape with three overrides (specialist, model, initialMessage)"
|
|
230
233
|
else
|
|
231
234
|
fail "case-5c body shape wrong ($BODY_OK): $RECORDER_BODY"
|
|
232
235
|
fi
|
|
233
236
|
|
|
234
|
-
# 5d.
|
|
237
|
+
# 5d. exactly ONE `spawn-request` log-ingest line
|
|
238
|
+
SPAWN_REQ_COUNT=$(ingest_lines | grep -cE "^spawn-request sessionId=${OP_ID} specialist=database-operator initialMessageBytes=[0-9]+$" || true)
|
|
239
|
+
if [[ "$SPAWN_REQ_COUNT" -eq 1 ]]; then
|
|
240
|
+
pass "case-5d exactly one spawn-request line emitted"
|
|
241
|
+
else
|
|
242
|
+
fail "case-5d expected 1 spawn-request, got $SPAWN_REQ_COUNT (lines: $(ingest_lines))"
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
# 5e. no legacy stderr emissions
|
|
235
246
|
if echo "$HOOK_STDERR" | grep -qE 'spawn-with-input|spawn-with-stdin|turn-completed-graph-write fired'; then
|
|
236
|
-
fail "case-
|
|
247
|
+
fail "case-5e legacy stderr line emitted: $HOOK_STDERR"
|
|
237
248
|
else
|
|
238
|
-
pass "case-
|
|
249
|
+
pass "case-5e no legacy stderr emissions"
|
|
239
250
|
fi
|
|
240
251
|
|
|
241
252
|
# --- Case 6 (Task 131): transcript has no assistant/user text → ---------
|
|
@@ -248,7 +259,7 @@ import json, sys
|
|
|
248
259
|
print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
|
|
249
260
|
' "$OP_ID" "$EMPTY_TRANSCRIPT")
|
|
250
261
|
: > "$REQ_LOG"
|
|
251
|
-
run_hook "admin" "
|
|
262
|
+
run_hook "admin" "" "$EMPTY_ENVELOPE"
|
|
252
263
|
[[ "$HOOK_RC" -eq 0 ]] || fail "case-6 rc=$HOOK_RC"
|
|
253
264
|
if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=last-turn-empty$"; then
|
|
254
265
|
pass "case-6 empty last-turn text → trigger-skipped reason=last-turn-empty emitted"
|
|
@@ -4,41 +4,26 @@
|
|
|
4
4
|
# text. The recorder is the only writer to the Neo4j graph; the admin agent
|
|
5
5
|
# stays focused on the operator's request.
|
|
6
6
|
#
|
|
7
|
-
# Task
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# literal instruction
|
|
19
|
-
# "update the graph with any new or missing information or intent"
|
|
20
|
-
# followed by a blank line. No transcript tail, no `Conversation id:`
|
|
21
|
-
# prose — both removed in Task 131. The recorder reads only this one
|
|
22
|
-
# most-recent turn.
|
|
23
|
-
# 4. When the recorder's turn ends (`stop_reason:"end_turn"` lands in
|
|
24
|
-
# the recorder JSONL), the manager posts `/<sid>/stop`, claude exits,
|
|
25
|
-
# the fs-watcher observes the PID-file removal, and the row leaves
|
|
26
|
-
# the index. No reaper involvement on the success path.
|
|
7
|
+
# Task 147 — the recorder spawn is byte-for-byte equivalent to a Sidebar
|
|
8
|
+
# "New session" body, with three overrides:
|
|
9
|
+
# - specialist: "database-operator"
|
|
10
|
+
# - model: "haiku"
|
|
11
|
+
# - initialMessage: "update the graph with any new or missing information
|
|
12
|
+
# or intent\n\n<last-turn-text>"
|
|
13
|
+
# It POSTs to the SAME route the Sidebar uses
|
|
14
|
+
# (`POST /api/admin/claude-sessions`). The wrapper accepts the loopback
|
|
15
|
+
# request without a cookie, resolves the operator's `senderId` from the
|
|
16
|
+
# manager's `/<adminSessionId>/meta`, and forwards a Sidebar-shape spawn
|
|
17
|
+
# body. No recorder-only carving on the manager side.
|
|
27
18
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# Gating (emits a `trigger-skipped` line, no longer a silent exit 0):
|
|
37
|
-
# - MAXY_SESSION_ROLE must equal "admin" → reason=role-not-admin
|
|
38
|
-
# - MAXY_IS_RECORDER must NOT equal "1" → reason=is-recorder
|
|
39
|
-
# - Stop-hook stdin must be non-empty → reason=empty-stdin
|
|
40
|
-
# - transcript_path must exist on disk → reason=missing-transcript
|
|
41
|
-
# - a most-recent assistant or user turn → reason=last-turn-empty
|
|
19
|
+
# Gating (emits a `trigger-skipped` line via `/api/admin/log-ingest`;
|
|
20
|
+
# stderr stays silent on the success path):
|
|
21
|
+
# - MAXY_SESSION_ROLE must equal "admin" → reason=role-not-admin
|
|
22
|
+
# - MAXY_SPECIALIST must NOT equal → reason=is-recorder
|
|
23
|
+
# "database-operator" (recursion guard)
|
|
24
|
+
# - Stop-hook stdin must be non-empty → reason=empty-stdin
|
|
25
|
+
# - transcript_path must exist on disk → reason=missing-transcript
|
|
26
|
+
# - a most-recent assistant or user turn → reason=last-turn-empty
|
|
42
27
|
# must be extractable from the JSONL
|
|
43
28
|
#
|
|
44
29
|
# Input: Claude Code's Stop hook stdin shape
|
|
@@ -46,12 +31,6 @@
|
|
|
46
31
|
|
|
47
32
|
set -uo pipefail
|
|
48
33
|
|
|
49
|
-
# UI listener port is stamped on the claude-session-manager systemd unit
|
|
50
|
-
# as `Environment=MAXY_UI_INTERNAL_PORT=<port>` (Task 134). Absence is a
|
|
51
|
-
# structural defect — historically the hook fell back to 19199 with no
|
|
52
|
-
# listener on most devices and silently swallowed connection-refused. Now
|
|
53
|
-
# it short-circuits with a named diagnostic to stderr (the only sink the
|
|
54
|
-
# log-ingest endpoint itself depends on this var to reach).
|
|
55
34
|
UI_PORT="${MAXY_UI_INTERNAL_PORT:-}"
|
|
56
35
|
if [ -z "$UI_PORT" ]; then
|
|
57
36
|
echo "[turn-recorder] spawn-failed sessionId=unknown reason=missing-env env=MAXY_UI_INTERNAL_PORT" >&2
|
|
@@ -60,10 +39,6 @@ fi
|
|
|
60
39
|
UI_BASE="http://127.0.0.1:${UI_PORT}"
|
|
61
40
|
LOG_INGEST_URL="${UI_BASE}/api/admin/log-ingest"
|
|
62
41
|
|
|
63
|
-
# Emit one log line via the platform's log-ingest endpoint. Best-effort:
|
|
64
|
-
# a failed POST does not break the hook — but the absence of the line is
|
|
65
|
-
# itself a system-level defect per Task 129 §157. We do NOT fall back to
|
|
66
|
-
# stderr, because stderr never reaches server.log.
|
|
67
42
|
emit_log() {
|
|
68
43
|
local line="$1"
|
|
69
44
|
curl -sS -o /dev/null -X POST \
|
|
@@ -76,8 +51,6 @@ print(json.dumps({"tag": "turn-recorder", "level": "info", "line": sys.argv[1]})
|
|
|
76
51
|
"$LOG_INGEST_URL" 2>/dev/null || true
|
|
77
52
|
}
|
|
78
53
|
|
|
79
|
-
# Parse stdin first so trigger-skipped lines can carry the operator
|
|
80
|
-
# session id (when present). Stop-hook stdin is required.
|
|
81
54
|
INPUT=""
|
|
82
55
|
if [ -t 0 ]; then
|
|
83
56
|
INPUT=""
|
|
@@ -98,20 +71,14 @@ except Exception:
|
|
|
98
71
|
ADMIN_SESSION_ID="${PARSED%% *}"
|
|
99
72
|
TRANSCRIPT_PATH="${PARSED#* }"
|
|
100
73
|
|
|
101
|
-
# Gate ordering — each gate gets a distinct trigger-skipped reason so the
|
|
102
|
-
# operator-ID-keyed grep can tell why a given turn did not record. Role
|
|
103
|
-
# gate fires first because non-admin invocations may have no operator id
|
|
104
|
-
# at all (the field is empty for public/recorder).
|
|
105
74
|
if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
|
|
106
75
|
emit_log "trigger-skipped sessionId=${ADMIN_SESSION_ID:-unknown} reason=role-not-admin"
|
|
107
76
|
exit 0
|
|
108
77
|
fi
|
|
109
|
-
#
|
|
110
|
-
# pty-spawner
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
# only correct discriminator against hook-recursion.
|
|
114
|
-
if [ "${MAXY_IS_RECORDER:-0}" = "1" ]; then
|
|
78
|
+
# Recursion guard. The recorder spawn carries
|
|
79
|
+
# `specialist=database-operator`; pty-spawner stamps MAXY_SPECIALIST on the
|
|
80
|
+
# PTY env. Skip recording the recorder's own turn.
|
|
81
|
+
if [ "${MAXY_SPECIALIST:-}" = "database-operator" ]; then
|
|
115
82
|
emit_log "trigger-skipped sessionId=${ADMIN_SESSION_ID:-unknown} reason=is-recorder"
|
|
116
83
|
exit 0
|
|
117
84
|
fi
|
|
@@ -124,14 +91,7 @@ if [ -z "$ADMIN_SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIP
|
|
|
124
91
|
exit 0
|
|
125
92
|
fi
|
|
126
93
|
|
|
127
|
-
# Extract the operator's most-recent turn text from the JSONL.
|
|
128
|
-
# rule (no fallbacks beyond the assistant→user pair) is:
|
|
129
|
-
# - the most-recent `assistant` record whose message.content yields any
|
|
130
|
-
# extractable text, OR
|
|
131
|
-
# - if no assistant record produces text, the most-recent `user` record
|
|
132
|
-
# that produces text.
|
|
133
|
-
# Tool-call blocks, system records, and ephemeral admin-state records are
|
|
134
|
-
# never the candidate. Empty output → reason=last-turn-empty.
|
|
94
|
+
# Extract the operator's most-recent turn text from the JSONL.
|
|
135
95
|
LAST_TURN_TEXT=$(python3 - "$TRANSCRIPT_PATH" <<'PY'
|
|
136
96
|
import sys, json
|
|
137
97
|
|
|
@@ -179,10 +139,6 @@ PY
|
|
|
179
139
|
LAST_TURN_BYTES=$(printf '%s' "$LAST_TURN_TEXT" | wc -c | tr -d ' ')
|
|
180
140
|
TRANSCRIPT_BYTES=$(wc -c <"$TRANSCRIPT_PATH" 2>/dev/null | tr -d ' ' || echo 0)
|
|
181
141
|
|
|
182
|
-
# Step 0 — trigger line. First link in the chain the operator-ID-keyed
|
|
183
|
-
# grep returns. Task 131: replaced `filteredRecords=<n>` with
|
|
184
|
-
# `lastTurnBytes=<n>` because the hook no longer ships a filtered
|
|
185
|
-
# transcript tail.
|
|
186
142
|
emit_log "trigger sessionId=${ADMIN_SESSION_ID} turnIndex=0 transcriptBytes=${TRANSCRIPT_BYTES} lastTurnBytes=${LAST_TURN_BYTES}"
|
|
187
143
|
|
|
188
144
|
if [ -z "$LAST_TURN_TEXT" ]; then
|
|
@@ -190,17 +146,10 @@ if [ -z "$LAST_TURN_TEXT" ]; then
|
|
|
190
146
|
exit 0
|
|
191
147
|
fi
|
|
192
148
|
|
|
193
|
-
# Compose the
|
|
194
|
-
#
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
# wrapper looks up the operator's real senderId from
|
|
198
|
-
# `/<originatingSessionId>/meta` and forwards a Sidebar-shape spawn
|
|
199
|
-
# body to the manager. No synthetic `senderId: 'turn-recorder'`
|
|
200
|
-
# marker — the loopback bypass is method+path-scoped.
|
|
201
|
-
# - `specialist: 'database-operator'` discriminates the recorder spawn
|
|
202
|
-
# from a vanilla Sidebar click.
|
|
203
|
-
# - No `Conversation id:` prose; no transcript-tail JSON.
|
|
149
|
+
# Compose the Sidebar-shape spawn body. Three overrides relative to a
|
|
150
|
+
# plain Sidebar "New session" click: specialist, model, initialMessage.
|
|
151
|
+
# `adminSessionId` is the loopback-bypass key the wrapper uses to resolve
|
|
152
|
+
# the operator's senderId from the manager's /<sid>/meta.
|
|
204
153
|
SPAWN_BODY=$(python3 -c '
|
|
205
154
|
import sys, json
|
|
206
155
|
sid = sys.argv[1]
|
|
@@ -208,13 +157,18 @@ last_turn = sys.argv[2]
|
|
|
208
157
|
instruction = "update the graph with any new or missing information or intent"
|
|
209
158
|
text = instruction + "\n\n" + last_turn
|
|
210
159
|
body = {
|
|
160
|
+
"adminSessionId": sid,
|
|
161
|
+
"channel": "browser",
|
|
211
162
|
"specialist": "database-operator",
|
|
163
|
+
"model": "haiku",
|
|
212
164
|
"initialMessage": text,
|
|
213
|
-
"originatingSessionId": sid,
|
|
214
165
|
}
|
|
215
166
|
print(json.dumps(body))
|
|
216
167
|
' "$ADMIN_SESSION_ID" "$LAST_TURN_TEXT" 2>/dev/null)
|
|
217
168
|
|
|
169
|
+
INITIAL_BYTES=$(printf '%s\n\n%s' "update the graph with any new or missing information or intent" "$LAST_TURN_TEXT" | wc -c | tr -d ' ')
|
|
170
|
+
emit_log "spawn-request sessionId=${ADMIN_SESSION_ID} specialist=database-operator initialMessageBytes=${INITIAL_BYTES}"
|
|
171
|
+
|
|
218
172
|
SPAWN_RES_FILE=$(mktemp)
|
|
219
173
|
SPAWN_HTTP=$(curl -sS -o "$SPAWN_RES_FILE" -w '%{http_code}' -X POST \
|
|
220
174
|
-H 'Content-Type: application/json' \
|
|
@@ -222,21 +176,12 @@ SPAWN_HTTP=$(curl -sS -o "$SPAWN_RES_FILE" -w '%{http_code}' -X POST \
|
|
|
222
176
|
--data "$SPAWN_BODY" \
|
|
223
177
|
"${UI_BASE}/api/admin/claude-sessions" 2>/dev/null || echo 000)
|
|
224
178
|
|
|
225
|
-
# The wrapper emits the named `spawn-request` / `spawn-success` /
|
|
226
|
-
# `input-delivered` lines itself. The hook only emits the hook-side
|
|
227
|
-
# failure variant — `loopback-http` — and even then only when the wrapper
|
|
228
|
-
# returned a non-2xx that is NOT one of the preflight reasons (those are
|
|
229
|
-
# logged by the manager before the wrapper sees them).
|
|
230
179
|
RES_BODY=$(cat "$SPAWN_RES_FILE" 2>/dev/null || true)
|
|
231
180
|
rm -f "$SPAWN_RES_FILE"
|
|
232
181
|
|
|
233
182
|
if [ "$SPAWN_HTTP" -lt 200 ] || [ "$SPAWN_HTTP" -ge 300 ]; then
|
|
234
183
|
ERR_SHORT=$(printf '%s' "$RES_BODY" | tr -d '\r\n' | cut -c1-200)
|
|
235
|
-
|
|
236
|
-
: # preflight or lookup already named the failure
|
|
237
|
-
else
|
|
238
|
-
emit_log "spawn-failed sessionId=${ADMIN_SESSION_ID} reason=loopback-http http=${SPAWN_HTTP} err=$(python3 -c 'import sys,json; print(json.dumps(sys.argv[1]))' "${ERR_SHORT}")"
|
|
239
|
-
fi
|
|
184
|
+
emit_log "spawn-failed sessionId=${ADMIN_SESSION_ID} reason=loopback-http http=${SPAWN_HTTP} err=$(python3 -c 'import sys,json; print(json.dumps(sys.argv[1]))' "${ERR_SHORT}")"
|
|
240
185
|
fi
|
|
241
186
|
|
|
242
187
|
exit 0
|
|
@@ -77,32 +77,33 @@ grep '\[client-acquire\]' ~/.${brand}/logs/claude-agent-stream-*.log
|
|
|
77
77
|
|
|
78
78
|
The Hono route `POST /api/admin/claude-sessions` at [`platform/ui/server/routes/admin/claude-sessions.ts`](../../../ui/server/routes/admin/claude-sessions.ts) is the single canonical entry point for any caller — UI click handler or programmatic — that needs to open an admin PTY session with a first user prompt. Direct calls to the session-manager's `/spawn` and `/:id/input` are not allowed; the wrapper owns the spawn-then-input ordering, the per-spawn enrichment (`aboutOwner`, `dormantPlugins`, `activePlugins`, `specialistDomains`, `tunnelUrl`), and the `senderId` resolution from the admin session cookie. The session manager binds to `127.0.0.1` only so the wrapper is the sole authorised caller; any ad-hoc spawn that bypasses it duplicates this contract and is a bug.
|
|
79
79
|
|
|
80
|
-
**Body schema.** `{channel?, permissionMode?, initialMessage?}` for
|
|
80
|
+
**Body schema.** `{channel?, permissionMode?, initialMessage?, specialist?, model?}` for the cookie path (Sidebar — `senderId` resolved from the admin session cookie, `hidden: false`). The Stop-hook loopback caller adds `adminSessionId` and uses three overrides: `specialist: 'database-operator'`, `model: 'haiku'`, plus `initialMessage`. The wrapper rejects nothing structurally on the cookie path — string fields are coerced or defaulted (`channel` → `'browser'`, `permissionMode` → undefined, `initialMessage` → null if empty/whitespace). The loopback path refuses with `400 admin-session-id-required` if `adminSessionId` is missing or non-UUID, and `404 admin-session-not-found` if the manager has no StoredSession for it.
|
|
81
81
|
|
|
82
|
-
**Auth surfaces.** Two: `cookie` (Sidebar — `requireAdminSession` validates `session_key`
|
|
82
|
+
**Auth surfaces.** Two: `cookie` (Sidebar — `requireAdminSession` validates `session_key` and resolves `senderId` from the in-memory session store) and `loopback` (Stop hook — `requireAdminSessionOrLoopback` skips cookie auth when method is `POST`, path is exactly `/api/admin/claude-sessions`, and the TCP peer is `127.0.0.1`/`::1`; the handler then resolves the operator's `senderId` from `GET {managerBase}/<adminSessionId>/meta`). Both the manager and the UI server bind to `127.0.0.1` only for internal traffic, so the trust boundary is reachable only from a process running on the device. Task 147 — one route, one body shape (the loopback variant differs only by overrides), no `/recorder-spawn` sibling, no XFF disambiguation.
|
|
83
83
|
|
|
84
84
|
**Forwarded endpoints.** Two upstream calls per spawn-with-message: `POST {managerBase}/spawn` (enriched body) returns `{sessionId}`; when `initialMessage` is set, the wrapper then issues `POST {managerBase}/<sessionId>/input { text: <initialMessage> }` synchronously so the PTY has its first turn stimulus before the operator types. The HTTP response streams the spawn upstream body straight through.
|
|
85
85
|
|
|
86
|
-
**Callers.** `Sidebar.tsx`'s "+ New session" click handler (sends
|
|
86
|
+
**Callers.** `Sidebar.tsx`'s "+ New session" click handler (sends no `initialMessage` — Task 146 made Claude Code's own label canonical, so no UI-side stimulus is injected); the turn-recorder hook (loopback path — see "Turn-recorder" below); future programmatic spawns. Every caller routes through this wrapper. The on-the-wire signal that the contract held is the `[claude-session-manager:wrapper] spawn-request-in surface=<cookie|loopback>` log line followed by `forward-spawn-done`.
|
|
87
87
|
|
|
88
|
-
## Turn-recorder:
|
|
88
|
+
## Turn-recorder: a Sidebar-equivalent spawn with three overrides
|
|
89
89
|
|
|
90
|
-
After every completed operator admin turn, the Stop hook `platform/plugins/admin/hooks/turn-completed-graph-write.sh` dispatches one headless `database-operator` session
|
|
90
|
+
After every completed operator admin turn, the Stop hook `platform/plugins/admin/hooks/turn-completed-graph-write.sh` dispatches one headless `database-operator` session that writes any new or missing information from the turn into the Neo4j graph. Task 147 — the recorder spawn is byte-for-byte equivalent to a Sidebar "New session" body, with three overrides:
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
update the graph with any new or missing information or intent
|
|
92
|
+
- `specialist: 'database-operator'`
|
|
93
|
+
- `model: 'haiku'`
|
|
94
|
+
- `initialMessage: "update the graph with any new or missing information or intent\n\n<last-turn-text>"`
|
|
97
95
|
|
|
98
|
-
|
|
99
|
-
```
|
|
100
|
-
The instruction wording is verbatim, with no trailing qualifier. `<last-turn-text>` is the most recent `assistant` record's text content from the operator JSONL, or — if no assistant record yields text — the most recent `user` record's text. Tool-call blocks, system records, and ephemeral admin-state records are never the candidate. If neither yields text, the hook emits `trigger-skipped reason=last-turn-empty` and no recorder is spawned. No transcript tail. No `Conversation id:` prose. The structural carrier of the operator session id is the spawn-body field `originatingSessionId`, not anything inside the message.
|
|
101
|
-
4. **On end_turn the recorder's PID file is removed.** When the recorder's JSONL gains an assistant message with `stop_reason: "end_turn"`, the manager posts `/<recorder-sid>/stop`, claude exits, the fs-watcher observes the PID-file removal and drops the row from the index. The reaper never fires on the success path; it exists only as the safety net for recorders that error without exiting.
|
|
96
|
+
There is no recorder-only carving on the manager side: no preflight, no tool-surface gate, no `originatingSessionId` discriminator, no `MAXY_IS_RECORDER` env stamp, no recorder-only log surfaces. The PTY's own first-use error is the contract for tool absence.
|
|
102
97
|
|
|
103
|
-
**
|
|
98
|
+
**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.
|
|
104
99
|
|
|
105
|
-
|
|
100
|
+
**`<last-turn-text>`** is the most recent `assistant` record's text content from the operator JSONL, or — if no assistant record yields text — the most recent `user` record's text. Tool-call blocks, system records, and ephemeral admin-state records are never the candidate. If neither yields text, the hook emits `trigger-skipped reason=last-turn-empty` and no recorder is spawned.
|
|
101
|
+
|
|
102
|
+
**End-of-turn auto-archive.** When any session whose specialist is `database-operator` gains an assistant message with `stop_reason: "end_turn"` in its JSONL, the manager posts `/<sid>/stop` against itself, claude exits, the fs-watcher observes the PID-file removal and drops the row from the index. The auto-archive log line is `auto-archive sessionId=<sid> specialist=database-operator reason=end-turn …`.
|
|
103
|
+
|
|
104
|
+
**Hook recursion gate.** `pty-spawner` stamps `MAXY_SPECIALIST=<specialist>` on every PTY env. The Stop hook short-circuits when `MAXY_SPECIALIST=database-operator` so the recorder's own end-of-turn does not re-fire the hook.
|
|
105
|
+
|
|
106
|
+
**Observability.** The hook emits exactly two lines per operator turn via `/api/admin/log-ingest`: `[turn-recorder] trigger sessionId=<op> turnIndex=0 transcriptBytes=<n> lastTurnBytes=<n>` and `[turn-recorder] spawn-request sessionId=<op> specialist=database-operator initialMessageBytes=<n>`. The remainder of the recorder lifecycle is covered by the Sidebar's existing lines (`pty-spawned`, JSONL events, `auto-archive`). Failure-mode names: `trigger-skipped reason=…` enumerates `role-not-admin | is-recorder | empty-stdin | missing-transcript | last-turn-empty`. Hook-side spawn errors emit `[turn-recorder] spawn-failed reason=loopback-http http=<code>`.
|
|
106
107
|
|
|
107
108
|
## Session identifiers (Task 135)
|
|
108
109
|
|
|
@@ -80,11 +80,11 @@ The row feed sits behind `requireAdminSession` like every other admin route, so
|
|
|
80
80
|
|
|
81
81
|
The trade-off is a longer-lived connection per tab: the manager's per-process subscriber count rises with open tabs, and the SSE channel must survive proxy idle timeouts. The manager emits a 25-second keep-alive comment line on every connection (ignored by EventSource consumers, refreshes the proxy clock) and the browser-side store force-closes-and-reconnects on transport errors with exponential backoff capped at 30s.
|
|
82
82
|
|
|
83
|
-
**Spawn lifecycle: PID-file driven.** Clicking "+ New session" spawns the PTY and waits for Claude Code's PID file to appear at `${CLAUDE_CONFIG_DIR}/sessions/<pid>.json`. The PID file lands at process init (for `entrypoint: cli` spawns) and carries the intrinsic `sessionId`, `bridgeSessionId`, `agent`, and `status` directly. The manager's filesystem watcher reports the create event; the spawn response includes the canonical `sessionId` from that file. URL capture still runs in parallel to populate the operator-facing iframe URL, but it no longer gates readiness. The JSONL transcript is written on the first operator turn (true on 2.1.143 and 2.1.128); the watcher fires a separate event for that, and `/list`, `/meta`, `/log` resolve any of four ids — `sessionId`, `bridgeSessionId`, `bridgeSuffix`, or numeric `pid` — to the same row.
|
|
83
|
+
**Spawn lifecycle: PID-file driven.** Clicking "+ New session" spawns the PTY and waits for Claude Code's PID file to appear at `${CLAUDE_CONFIG_DIR}/sessions/<pid>.json`. The PID file lands at process init (for `entrypoint: cli` spawns) and carries the intrinsic `sessionId`, `bridgeSessionId`, `agent`, and `status` directly. The manager's filesystem watcher reports the create event; the spawn response includes the canonical `sessionId` from that file. URL capture still runs in parallel to populate the operator-facing iframe URL, but it no longer gates readiness. The JSONL transcript is written on the first operator turn (true on 2.1.143 and 2.1.128); the watcher fires a separate event for that, and `/list`, `/meta`, `/log` resolve any of four ids — `sessionId`, `bridgeSessionId`, `bridgeSuffix`, or numeric `pid` — to the same row. The Sidebar POST body carries no `initialMessage` (Task 146): Claude Code's own `tail.aiTitle` is the canonical sidebar row label, and the wrapper does not inject any cold-start stimulus on operator-driven spawns. The wrapper at `platform/ui/server/routes/admin/claude-sessions.ts` is still the single canonical entry point for any programmatic admin spawn-with-prompt — see `admin-session.md` "Spawn-with-initialMessage wrapper" and `internals.md` "Programmatic spawn entry point" — and the turn-recorder loopback path still forwards an `initialMessage`. Resume flows are unaffected (the prior transcript is the stimulus).
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
The sidebar row's displayed name is `tail.aiTitle` verbatim, parsed by `jsonl-enumerator.ts` from the JSONL Claude Code writes. Until Claude Code has written its title, the row label is null and the cell renders empty — no UI-stamped sidecar layer, no 8-char id fallback. When Claude Code later updates its title mid-session, the next `/list` or `/events` tick surfaces the new label. Task 146.
|
|
86
86
|
|
|
87
|
-
**Stop vs. delete.** `POST /<id>/stop` sends SIGTERM, leaves the JSONL on disk for audit, and is idempotent against an already-dead row. `DELETE /<id>` removes the JSONL +
|
|
87
|
+
**Stop vs. delete.** `POST /<id>/stop` sends SIGTERM, leaves the JSONL on disk for audit, and is idempotent against an already-dead row. `DELETE /<id>` removes the JSONL + per-session subdir and returns 409 if the PTY is still alive (stop first). Any unknown id returns 404; nothing returns a silent 204 against an id the manager does not know.
|
|
88
88
|
|
|
89
89
|
The metadata pane subscribes to the same /list projection. When an operator clicks End on an alive row, the DELETE returns 200 and the post-mutation refetch decides what happens next: a session that wrote a JSONL surfaces as a dehydrated `status: 'ended'` row (the pane swaps `End session` for `Purge JSONL` plus `Resume`), and a session that never wrote a JSONL (`Turns: 0`) leaves the list entirely (the pane shows a `Session ended without a transcript. Close this pane.` banner with a Close button and no destructive action). The manager's `/list` and `/meta` are the only authorities on post-End state; the client does not pre-empt either response with an optimistic mutation.
|
|
90
90
|
|
|
@@ -73,7 +73,7 @@ Brand decides which premium plugins ship. Maxy installs ship only `platform/plug
|
|
|
73
73
|
| Plugin | Type | What it does | Public agent |
|
|
74
74
|
|--------|------|-------------|-------------|
|
|
75
75
|
| `teaching` | Skills | Interactive tutoring, lesson planning, and study pack generation from your knowledge base | Yes — all 3 skills serve students and parents |
|
|
76
|
-
| `real-agent` | Bundle (
|
|
76
|
+
| `real-agent` | Bundle (12 sub-plugins) | UK estate agency skills — sales, listings, vendor management, buyer management, lead generation, coaching, business operations, teaching, Loop CRM (five value pillars: auto-respond, viewing lifecycle, pipeline mining, listings prospecting, maintenance & preferences), PropertyData market analytics (valuation, sold prices, £/sqft baselines, £/sqft growth, demand-rent, area risk, planning precedent, UPRN matching, property-type distribution), property brochures, social-share image cards, A4 market reports, and single-address preval packs (full UK address → 4-page A4 PDF covering valuation, area, and demand). 3 specialist roles (negotiator, valuer, compliance) | 4 sub-plugins (estate-sales, buyers, estate-coaching, estate-teaching) |
|
|
77
77
|
| `writer-craft` | Skills + Agent | Manuscript review and writing craft — story architecture, reader engagement, prose craft, editorial practice, and multi-level review | No — writing craft serves the author |
|
|
78
78
|
|
|
79
79
|
**How it works:** Every boot {{productName}} delivers the brand's premium plugins from staging into `platform/plugins/` and stamps `enabledPlugins` against what is actually on disk. No conversation needed — the brand's full set is active from the first turn after install. Updates and reinstalls re-deliver from staging.
|
|
@@ -116,7 +116,19 @@ Keys default to all 8 groups. Restrict at registration time to limit what the ag
|
|
|
116
116
|
|
|
117
117
|
## Security
|
|
118
118
|
|
|
119
|
-
Never include API key values in responses. Identify teams by name only. When the user asks for a key value, explain that keys are encrypted and cannot be retrieved — they must be re-registered from Loop's dashboard.
|
|
119
|
+
Never include API key values in responses. Identify teams by name only. When the user asks for a key value, explain that keys are encrypted and cannot be retrieved — they must be re-registered from Loop's dashboard. Keys are encrypted with AES-256-GCM; the encryption-key file lives at `~/<configDir>/.loop-encryption-key` (platform mode) or `~/.claude/.realagent-loop-encryption-key` (standalone mode), both mode `0600`.
|
|
120
|
+
|
|
121
|
+
## Standalone mode (`~/.claude/` spawn)
|
|
122
|
+
|
|
123
|
+
The same `mcp/dist/index.js` boots in two environments, selected by env flag:
|
|
124
|
+
|
|
125
|
+
| Env var | Platform mode | Standalone mode |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `LOOP_STANDALONE` | unset | `"1"` |
|
|
128
|
+
| `ACCOUNT_ID` | platform tenant id | absent (defaults to `"local"`) |
|
|
129
|
+
| `NEO4J_URI` etc. | required | unused |
|
|
130
|
+
|
|
131
|
+
In standalone mode team keys live at `~/.claude/.realagent-loop-key.enc` as one AES-256-GCM-encrypted JSON blob keyed by accountId then teamName. Multi-team support is preserved. See `.docs/realagent-mcps-standalone-mode.md` for the full contract.
|
|
120
132
|
|
|
121
133
|
## When to use these tools
|
|
122
134
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Logging-tee was a platform-wrapper concern; the host owns the stderr pipe
|
|
2
|
+
// in both platform and standalone (Claude Code native) spawn paths.
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { z } from "zod";
|
|
@@ -28,10 +28,10 @@ import { autoResponder } from "./tools/auto-responder.js";
|
|
|
28
28
|
import { customerPreferences } from "./tools/customer-preferences.js";
|
|
29
29
|
import { supplier } from "./tools/supplier.js";
|
|
30
30
|
import { closeDriver } from "./lib/neo4j.js";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
import { selectLoopKeyStore } from "./lib/key-store.js";
|
|
32
|
+
// ACCOUNT_ID is the platform's tenant id; "local" is the single-tenant
|
|
33
|
+
// standalone stand-in when spawned natively by Claude Code.
|
|
34
|
+
const accountId = process.env.ACCOUNT_ID ?? "local";
|
|
35
35
|
const server = new McpServer({
|
|
36
36
|
name: "maxy-loop",
|
|
37
37
|
version: "0.2.0",
|
|
@@ -40,7 +40,8 @@ const ALL_PERMISSIONS = z.enum([
|
|
|
40
40
|
"properties", "people", "viewings", "feedback", "team",
|
|
41
41
|
"marketing", "customer", "supplier",
|
|
42
42
|
]);
|
|
43
|
-
|
|
43
|
+
const store = selectLoopKeyStore();
|
|
44
|
+
console.error(`[loop] server started, account=${accountId} backend=${store.backendName} path=${store.backendPath}`);
|
|
44
45
|
// Helper: wrap a tool function in standard error handling
|
|
45
46
|
function toolHandler(fn) {
|
|
46
47
|
return async (params) => {
|