@rubytech/create-maxy-code 0.1.45 → 0.1.49
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/dist/__tests__/premium-mcp-discover.test.js +85 -0
- package/dist/index.js +25 -16
- package/dist/lib/premium-mcp-discover.js +34 -0
- package/package.json +1 -1
- package/payload/platform/lib/admins-write/dist/index.d.ts +0 -1
- package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admins-write/dist/index.js +2 -5
- package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
- package/payload/platform/lib/admins-write/src/index.ts +2 -8
- package/payload/platform/lib/graph-mcp/dist/index.js +1 -4
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts +7 -6
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js +7 -6
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js.map +1 -1
- package/payload/platform/lib/graph-mcp/src/index.ts +1 -4
- package/payload/platform/lib/graph-mcp/src/schema-cache.ts +7 -6
- package/payload/platform/lib/graph-search/dist/index.d.ts +5 -5
- package/payload/platform/lib/graph-search/dist/index.js +4 -4
- package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +6 -7
- package/payload/platform/lib/graph-search/src/index.ts +5 -5
- package/payload/platform/lib/graph-trash/dist/index.d.ts +8 -12
- package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-trash/dist/index.js +8 -12
- package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-trash/src/index.ts +8 -12
- package/payload/platform/lib/graph-write/dist/index.d.ts +8 -9
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +9 -10
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/index.ts +9 -10
- package/payload/platform/lib/mcp-eager/dist/index.d.ts +4 -4
- package/payload/platform/lib/mcp-eager/dist/index.js +4 -4
- package/payload/platform/lib/mcp-eager/src/index.ts +4 -4
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +56 -38
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +42 -40
- package/payload/platform/plugins/cloudflare/scripts/__tests__/tunnel-ingress.test.ts +2 -2
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +7 -9
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +10 -12
- package/payload/platform/plugins/cloudflare/scripts/tunnel-ingress.ts +3 -3
- package/payload/platform/plugins/docs/references/deployment.md +2 -14
- package/payload/platform/plugins/docs/references/platform.md +2 -2
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +5 -5
- package/payload/platform/plugins/loop/PLUGIN.md +67 -12
- package/payload/platform/plugins/loop/mcp/dist/index.js +42 -16
- package/payload/platform/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.d.ts +12 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.js +22 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts +0 -7
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.js +4 -38
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.d.ts +33 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.js +19 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.d.ts +0 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.js +1 -7
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.d.ts +9 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.js +19 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/src/__tests__/new-tool-shape.test.ts +99 -0
- package/payload/platform/plugins/loop/mcp/src/__tests__/swagger-write-coverage.test.ts +12 -11
- package/payload/platform/plugins/loop/mcp/src/index.ts +68 -17
- package/payload/platform/plugins/loop/mcp/src/tools/auto-responder.ts +80 -0
- package/payload/platform/plugins/loop/mcp/src/tools/marketing-enquiry.ts +14 -100
- package/payload/platform/plugins/loop/mcp/src/tools/property-intent.ts +84 -0
- package/payload/platform/plugins/loop/mcp/src/tools/property-listed.ts +1 -8
- package/payload/platform/plugins/loop/mcp/src/tools/property-sold.ts +67 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js +4 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +7 -12
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-not-applicable.test.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js +4 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/session-name.d.ts.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/session-name.js +8 -9
- package/payload/platform/plugins/tasks/mcp/dist/tools/session-name.js.map +1 -1
- package/payload/platform/plugins/waitlist/mcp/dist/tools/waitlist-heal.d.ts +2 -2
- package/payload/platform/plugins/waitlist/mcp/dist/tools/waitlist-heal.js +2 -2
- package/payload/platform/scripts/admin-persist-audit.ts +4 -10
- package/payload/platform/scripts/conversation-id-allowlist.txt +0 -1
- package/payload/platform/scripts/lib/resolve-account-dir.sh +0 -2
- package/payload/platform/scripts/resume-tunnel.sh +2 -9
- package/payload/platform/scripts/seed-neo4j.sh +26 -104
- 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 +53 -7
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -5
- 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 +13 -17
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +10 -2
- package/payload/platform/templates/agents/admin/SOUL.md +23 -1
- package/payload/premium-plugins/real-agent/agents/buyer-enquiry/SOUL.md +10 -0
- package/payload/premium-plugins/real-agent/agents/compliance.md +1 -1
- 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/loop/PLUGIN.md +67 -12
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js +42 -16
- 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/tools/auto-responder.d.ts +12 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.js +22 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts +0 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.js +4 -38
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.d.ts +33 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.js +19 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.d.ts +0 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.js +1 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.d.ts +9 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.js +19 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/__tests__/new-tool-shape.test.ts +99 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/__tests__/swagger-write-coverage.test.ts +12 -11
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/index.ts +68 -17
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/auto-responder.ts +80 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/marketing-enquiry.ts +14 -100
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-intent.ts +84 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-listed.ts +1 -8
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-sold.ts +67 -0
- package/payload/server/public/assets/admin-DBuCWSPS.js +216 -0
- package/payload/server/public/assets/{data-BxpOR0eM.js → data-BeblX4m4.js} +1 -1
- package/payload/server/public/assets/{graph-CrmxRSeb.js → graph-ZuBiYril.js} +1 -1
- package/payload/server/public/assets/graph-labels-CnbJR6cp.js +1 -0
- package/payload/server/public/assets/page-DDyny08O.js +50 -0
- package/payload/server/public/assets/page-nAY64ID7.js +1 -0
- 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 +9 -46
- package/payload/platform/plugins/docs/references/migration-guide.md +0 -90
- package/payload/platform/plugins/loop/mcp/src/tools/property-request.ts +0 -42
- package/payload/platform/scripts/component-knowledgedoc-backfill.ts +0 -214
- package/payload/platform/scripts/migrate-import.sh +0 -437
- package/payload/platform/scripts/taskmaster-export.sh +0 -388
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-request.ts +0 -42
- package/payload/server/public/assets/admin-DVZcLuYu.js +0 -216
- package/payload/server/public/assets/graph-labels-C8A6BmtP.js +0 -1
- package/payload/server/public/assets/page-D5wAeFU0.js +0 -50
- package/payload/server/public/assets/page-DR5NU6S0.js +0 -1
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Regression test for the Stop-hook gate that fires the database-operator
|
|
3
|
-
# per completed admin-agent turn (Task
|
|
3
|
+
# per completed admin-agent turn (Task 114 redesign: normal admin session +
|
|
4
|
+
# /input POST, replacing Task 106's `--print` + stdin-EOT shape which is
|
|
5
|
+
# incompatible with OAuth).
|
|
4
6
|
#
|
|
5
7
|
# Behaviour verified:
|
|
6
8
|
# 1. MAXY_SESSION_ROLE!=admin → exit 0, no POST attempt, no log line.
|
|
7
|
-
# 2. MAXY_HIDDEN_SPAWN=1 → exit 0, no POST attempt, no log line
|
|
8
|
-
# (the Stop-hook self-stop path was removed in Task 106; the recorder
|
|
9
|
-
# now exits when its stdin closes, not via a /<id>/stop POST).
|
|
9
|
+
# 2. MAXY_HIDDEN_SPAWN=1 → exit 0, no POST attempt, no log line.
|
|
10
10
|
# 3. Empty stdin → exit 0 silently.
|
|
11
11
|
# 4. Missing transcript_path → exit 0 silently.
|
|
12
|
-
# 5. Happy path → ONE POST to /spawn
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# 5. Happy path → exactly ONE POST to /spawn (no stdinPayload
|
|
13
|
+
# field) and exactly ONE POST to /<id>/input whose `text` carries the
|
|
14
|
+
# Task 114 instruction string and the filtered transcript, plus the
|
|
15
|
+
# `[turn-recorder] spawn-with-input sessionId=<8> bytes=<n> filtered-from-records=<n> input-http=200`
|
|
15
16
|
# log line.
|
|
16
17
|
|
|
17
18
|
set -u
|
|
@@ -71,11 +72,11 @@ elif [[ -n "$HOOK_STDERR" ]]; then fail "case-1 expected empty stderr, got: $HOO
|
|
|
71
72
|
else pass "case-1 role=public → no fire (rc=0, stderr empty)"
|
|
72
73
|
fi
|
|
73
74
|
|
|
74
|
-
# --- Case 2: hidden=1 → no fire
|
|
75
|
+
# --- Case 2: hidden=1 → no fire ----------------------------------------
|
|
75
76
|
run_hook "admin" "1" "$ENVELOPE"
|
|
76
77
|
if [[ "$HOOK_RC" -ne 0 ]]; then fail "case-2 expected rc=0 got=$HOOK_RC"
|
|
77
|
-
elif [[ -n "$HOOK_STDERR" ]]; then fail "case-2 expected empty stderr
|
|
78
|
-
else pass "case-2 hidden=1 → silent
|
|
78
|
+
elif [[ -n "$HOOK_STDERR" ]]; then fail "case-2 expected empty stderr, got: $HOOK_STDERR"
|
|
79
|
+
else pass "case-2 hidden=1 → silent"
|
|
79
80
|
fi
|
|
80
81
|
|
|
81
82
|
# --- Case 3: empty stdin → silent --------------------------------------
|
|
@@ -93,11 +94,7 @@ elif [[ -n "$HOOK_STDERR" ]]; then fail "case-4 expected empty stderr, got: $HOO
|
|
|
93
94
|
else pass "case-4 missing transcript_path → silent (rc=0)"
|
|
94
95
|
fi
|
|
95
96
|
|
|
96
|
-
# --- Case 5: happy path → /spawn
|
|
97
|
-
# A tiny python HTTP listener records every request body/path. It accepts
|
|
98
|
-
# /spawn and returns a JSON body containing a recorder sessionId. If the
|
|
99
|
-
# hook makes a second POST to /<id>/input the listener records that path
|
|
100
|
-
# too — the test fails if any /input path appears in the request log.
|
|
97
|
+
# --- Case 5: happy path → one /spawn (no stdinPayload) + one /<id>/input
|
|
101
98
|
REQ_LOG=$(mktemp); TMPFILES+=("$REQ_LOG")
|
|
102
99
|
LISTENER_PORT=39406
|
|
103
100
|
python3 - "$LISTENER_PORT" "$REQ_LOG" <<'PY' &
|
|
@@ -114,7 +111,10 @@ class H(http.server.BaseHTTPRequestHandler):
|
|
|
114
111
|
self.send_response(200)
|
|
115
112
|
self.send_header('Content-Type','application/json')
|
|
116
113
|
self.end_headers()
|
|
117
|
-
self.
|
|
114
|
+
if self.path == '/spawn':
|
|
115
|
+
self.wfile.write(json.dumps({"sessionId":"rec00001-feedfeed"}).encode('utf-8'))
|
|
116
|
+
else:
|
|
117
|
+
self.wfile.write(json.dumps({"ok": True}).encode('utf-8'))
|
|
118
118
|
http.server.HTTPServer(('127.0.0.1', port), H).serve_forever()
|
|
119
119
|
PY
|
|
120
120
|
LISTENER_PIDS+=("$!")
|
|
@@ -141,22 +141,21 @@ if [[ "$HOOK_RC" -ne 0 ]]; then
|
|
|
141
141
|
fail "case-5 expected rc=0 got=$HOOK_RC stderr=$HOOK_STDERR"
|
|
142
142
|
fi
|
|
143
143
|
|
|
144
|
-
# Assert: spawn-with-
|
|
145
|
-
if ! echo "$HOOK_STDERR" | grep -qE '^\[recorder\] spawn-with-
|
|
146
|
-
fail "case-
|
|
144
|
+
# Assert: spawn-with-input log line emitted.
|
|
145
|
+
if ! echo "$HOOK_STDERR" | grep -qE '^\[turn-recorder\] spawn-with-input sessionId=rec00001 bytes=[0-9]+ filtered-from-records=[0-9]+ input-http=[0-9]+'; then
|
|
146
|
+
fail "case-5a expected spawn-with-input log line, got: $HOOK_STDERR"
|
|
147
147
|
else
|
|
148
|
-
pass "case-5a spawn-with-
|
|
148
|
+
pass "case-5a spawn-with-input log line emitted"
|
|
149
149
|
fi
|
|
150
150
|
|
|
151
|
-
# Assert: no legacy
|
|
152
|
-
if echo "$HOOK_STDERR" | grep -q '
|
|
153
|
-
fail "case-5b legacy
|
|
151
|
+
# Assert: no legacy spawn-with-stdin log line.
|
|
152
|
+
if echo "$HOOK_STDERR" | grep -q 'spawn-with-stdin'; then
|
|
153
|
+
fail "case-5b legacy spawn-with-stdin line must not be emitted in Task 114"
|
|
154
154
|
else
|
|
155
|
-
pass "case-5b legacy
|
|
155
|
+
pass "case-5b legacy spawn-with-stdin log line absent"
|
|
156
156
|
fi
|
|
157
157
|
|
|
158
|
-
# Assert: listener saw exactly one /spawn POST
|
|
159
|
-
# stdinPayload, and ZERO /<id>/input POSTs.
|
|
158
|
+
# Assert: listener saw exactly one /spawn POST and exactly one /<id>/input POST.
|
|
160
159
|
SPAWN_LINES=$(grep -c '^/spawn ' "$REQ_LOG" || true)
|
|
161
160
|
INPUT_LINES=$(grep -cE '^/[A-Za-z0-9_-]+/input ' "$REQ_LOG" || true)
|
|
162
161
|
STOP_LINES=$(grep -cE '^/[A-Za-z0-9_-]+/stop ' "$REQ_LOG" || true)
|
|
@@ -165,18 +164,18 @@ if [[ "$SPAWN_LINES" -ne 1 ]]; then
|
|
|
165
164
|
else
|
|
166
165
|
pass "case-5c exactly one /spawn POST observed"
|
|
167
166
|
fi
|
|
168
|
-
if [[ "$INPUT_LINES" -ne
|
|
169
|
-
fail "case-5d expected
|
|
167
|
+
if [[ "$INPUT_LINES" -ne 1 ]]; then
|
|
168
|
+
fail "case-5d expected exactly 1 /<id>/input POST, got $INPUT_LINES"
|
|
170
169
|
else
|
|
171
|
-
pass "case-5d
|
|
170
|
+
pass "case-5d exactly one /<id>/input POST observed"
|
|
172
171
|
fi
|
|
173
172
|
if [[ "$STOP_LINES" -ne 0 ]]; then
|
|
174
|
-
fail "case-5e expected ZERO /<id>/stop POSTs, got $STOP_LINES"
|
|
173
|
+
fail "case-5e expected ZERO /<id>/stop POSTs from the hook (auto-archive is in-manager), got $STOP_LINES"
|
|
175
174
|
else
|
|
176
175
|
pass "case-5e zero /<id>/stop POSTs"
|
|
177
176
|
fi
|
|
178
177
|
|
|
179
|
-
# Assert: the /spawn body
|
|
178
|
+
# Assert: the /spawn body has no stdinPayload field.
|
|
180
179
|
SPAWN_BODY=$(grep '^/spawn ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
181
180
|
if [[ -z "$SPAWN_BODY" ]]; then
|
|
182
181
|
fail "case-5f /spawn body was empty"
|
|
@@ -185,23 +184,42 @@ else
|
|
|
185
184
|
import sys, json
|
|
186
185
|
try:
|
|
187
186
|
b = json.load(sys.stdin)
|
|
188
|
-
|
|
189
|
-
print("yes" if isinstance(sp, str) and len(sp) > 0 else "no")
|
|
187
|
+
print("yes" if "stdinPayload" in b else "no")
|
|
190
188
|
except Exception:
|
|
191
189
|
print("parse-fail")
|
|
192
190
|
' 2>/dev/null)
|
|
193
|
-
if [[ "$HAS_STDIN" != "
|
|
194
|
-
fail "case-5f /spawn body must carry
|
|
191
|
+
if [[ "$HAS_STDIN" != "no" ]]; then
|
|
192
|
+
fail "case-5f /spawn body must NOT carry stdinPayload, got: $SPAWN_BODY"
|
|
195
193
|
else
|
|
196
|
-
pass "case-5f /spawn body
|
|
194
|
+
pass "case-5f /spawn body has no stdinPayload field"
|
|
197
195
|
fi
|
|
198
196
|
fi
|
|
199
197
|
|
|
198
|
+
# Assert: the /input body's `text` carries the Task 114 instruction and the
|
|
199
|
+
# transcript section header.
|
|
200
|
+
INPUT_BODY=$(grep -E '^/[A-Za-z0-9_-]+/input ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
201
|
+
INPUT_OK=$(printf '%s' "$INPUT_BODY" | python3 -c '
|
|
202
|
+
import sys, json
|
|
203
|
+
try:
|
|
204
|
+
b = json.load(sys.stdin)
|
|
205
|
+
t = b.get("text", "")
|
|
206
|
+
has_instr = "update the graph with any missing information or intent that can be derived from this conversation" in t
|
|
207
|
+
has_tx = "Conversation transcript:" in t
|
|
208
|
+
print("yes" if (has_instr and has_tx) else "no")
|
|
209
|
+
except Exception:
|
|
210
|
+
print("parse-fail")
|
|
211
|
+
' 2>/dev/null)
|
|
212
|
+
if [[ "$INPUT_OK" != "yes" ]]; then
|
|
213
|
+
fail "case-5g /input body missing instruction or transcript header: $INPUT_BODY"
|
|
214
|
+
else
|
|
215
|
+
pass "case-5g /input body carries Task 114 instruction + transcript"
|
|
216
|
+
fi
|
|
217
|
+
|
|
200
218
|
# Assert: final fired log line still emitted.
|
|
201
219
|
if ! echo "$HOOK_STDERR" | grep -qE '^turn-completed-graph-write fired conversationId=abcd1234 recorderSessionId=rec00001 filtered=[0-9]+ ms=[0-9]+'; then
|
|
202
|
-
fail "case-
|
|
220
|
+
fail "case-5h expected fired summary log line, got: $HOOK_STDERR"
|
|
203
221
|
else
|
|
204
|
-
pass "case-
|
|
222
|
+
pass "case-5h fired summary log line emitted"
|
|
205
223
|
fi
|
|
206
224
|
|
|
207
225
|
# --- Summary ------------------------------------------------------------
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Stop hook — fires on every completed admin-agent turn and dispatches one
|
|
3
|
-
# headless
|
|
4
|
-
#
|
|
5
|
-
# the operator's request.
|
|
3
|
+
# headless database-operator session against the conversation transcript.
|
|
4
|
+
# The hidden recorder is the only writer to the Neo4j graph; the admin agent
|
|
5
|
+
# stays focused on the operator's request.
|
|
6
6
|
#
|
|
7
|
-
# Pipeline (Task 106
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
7
|
+
# Pipeline (Task 114 redesign — replaces Task 106's `--print` + stdin-EOT
|
|
8
|
+
# shape, which is incompatible with the OAuth subscription the manager runs
|
|
9
|
+
# on):
|
|
10
|
+
# 1. /spawn with `hidden=true, specialist=database-operator` — the manager
|
|
11
|
+
# produces a structurally normal admin specialist session (same argv,
|
|
12
|
+
# same OAuth path) and returns its sessionId.
|
|
13
|
+
# 2. POST /<sessionId>/input with `<instruction>\n\nConversation transcript:\n<filtered JSON>`.
|
|
14
|
+
# 3. The manager's recorder-auto-archive subscriber stops the PTY as soon
|
|
15
|
+
# as the recorder's JSONL contains an assistant message with
|
|
16
|
+
# `stop_reason === "end_turn"`. The hook does not need to follow up.
|
|
15
17
|
#
|
|
16
18
|
# Gating:
|
|
17
19
|
# - MAXY_SESSION_ROLE must equal "admin"
|
|
18
|
-
# - MAXY_HIDDEN_SPAWN must equal "0" (
|
|
19
|
-
#
|
|
20
|
-
# hooks itself, but the guard stays as a belt-and-braces backstop).
|
|
20
|
+
# - MAXY_HIDDEN_SPAWN must equal "0" (the recorder PTY itself runs with
|
|
21
|
+
# MAXY_HIDDEN_SPAWN=1; the guard prevents accidental recursion).
|
|
21
22
|
#
|
|
22
23
|
# Input: Claude Code's Stop hook stdin shape
|
|
23
24
|
# { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
|
|
@@ -33,10 +34,6 @@ if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
|
|
|
33
34
|
exit 0
|
|
34
35
|
fi
|
|
35
36
|
if [ "${MAXY_HIDDEN_SPAWN:-0}" = "1" ]; then
|
|
36
|
-
# Recorder PTY's own Stop hook would land here if claude --print ever
|
|
37
|
-
# emitted one. The lifecycle is now stdin-close → process-exit, so no
|
|
38
|
-
# self-stop POST is needed; exit silently to prevent accidental recursion
|
|
39
|
-
# into the admin branch below.
|
|
40
37
|
exit 0
|
|
41
38
|
fi
|
|
42
39
|
|
|
@@ -106,36 +103,17 @@ FILTERED_COUNT=$(printf '%s' "$FILTERED_TAIL" | python3 -c 'import sys,json; d=j
|
|
|
106
103
|
MANAGER_PORT="${CLAUDE_SESSION_MANAGER_PORT:-19400}"
|
|
107
104
|
MANAGER_BASE="http://127.0.0.1:${MANAGER_PORT}"
|
|
108
105
|
|
|
109
|
-
# Compose the recorder prompt + bundle it into the /spawn body's
|
|
110
|
-
# `stdinPayload`. The manager appends `--print` to the claude argv and
|
|
111
|
-
# writes (payload + EOT) to the PTY's stdin; claude consumes one turn and
|
|
112
|
-
# exits, the fs-watcher archives the row.
|
|
113
106
|
SPAWN_BODY=$(python3 -c '
|
|
114
|
-
import
|
|
115
|
-
sid = sys.argv[1]
|
|
116
|
-
tail_json = sys.argv[2]
|
|
117
|
-
prompt = (
|
|
118
|
-
"You are the database-operator specialist. Record the following completed turn "
|
|
119
|
-
"into the Neo4j graph. The transcript is a JSON array of filtered records "
|
|
120
|
-
"from the admin agent (user prompts, assistant responses, tool calls). "
|
|
121
|
-
f"Conversation id: {sid}.\n\nTranscript:\n{tail_json}\n"
|
|
122
|
-
)
|
|
107
|
+
import json
|
|
123
108
|
body = {
|
|
124
109
|
"senderId": "turn-recorder",
|
|
125
110
|
"role": "admin",
|
|
126
111
|
"channel": "browser",
|
|
127
112
|
"hidden": True,
|
|
128
113
|
"specialist": "database-operator",
|
|
129
|
-
"stdinPayload": prompt,
|
|
130
114
|
}
|
|
131
115
|
print(json.dumps(body))
|
|
132
|
-
'
|
|
133
|
-
|
|
134
|
-
if [ -z "$SPAWN_BODY" ]; then
|
|
135
|
-
exit 0
|
|
136
|
-
fi
|
|
137
|
-
|
|
138
|
-
PAYLOAD_BYTES=$(printf '%s' "$SPAWN_BODY" | python3 -c 'import sys,json; b=json.load(sys.stdin); print(len(b.get("stdinPayload","").encode("utf-8")))' 2>/dev/null || echo 0)
|
|
116
|
+
' 2>/dev/null)
|
|
139
117
|
|
|
140
118
|
SPAWN_RES=$(curl -sS -X POST \
|
|
141
119
|
-H 'Content-Type: application/json' \
|
|
@@ -157,7 +135,31 @@ if [ -z "$RECORDER_SESSION_ID" ]; then
|
|
|
157
135
|
exit 0
|
|
158
136
|
fi
|
|
159
137
|
|
|
160
|
-
|
|
138
|
+
# Compose the input prompt and POST it to /<sessionId>/input. The instruction
|
|
139
|
+
# string is fixed (Task 114). The transcript is appended as a JSON array so
|
|
140
|
+
# the recorder reads it as a single user turn.
|
|
141
|
+
INPUT_BODY=$(python3 -c '
|
|
142
|
+
import sys, json
|
|
143
|
+
sid = sys.argv[1]
|
|
144
|
+
tail_json = sys.argv[2]
|
|
145
|
+
instruction = "update the graph with any missing information or intent that can be derived from this conversation"
|
|
146
|
+
text = (
|
|
147
|
+
f"{instruction}\n\n"
|
|
148
|
+
f"Conversation id: {sid}\n"
|
|
149
|
+
f"Conversation transcript:\n{tail_json}\n"
|
|
150
|
+
)
|
|
151
|
+
print(json.dumps({"text": text}))
|
|
152
|
+
' "$ADMIN_SESSION_ID" "$FILTERED_TAIL" 2>/dev/null)
|
|
153
|
+
|
|
154
|
+
INPUT_BYTES=$(printf '%s' "$INPUT_BODY" | python3 -c 'import sys,json; b=json.load(sys.stdin); print(len(b.get("text","").encode("utf-8")))' 2>/dev/null || echo 0)
|
|
155
|
+
|
|
156
|
+
INPUT_HTTP=$(curl -sS -o /tmp/turn-recorder-input.out -w '%{http_code}' -X POST \
|
|
157
|
+
-H 'Content-Type: application/json' \
|
|
158
|
+
--max-time 10 \
|
|
159
|
+
--data "$INPUT_BODY" \
|
|
160
|
+
"${MANAGER_BASE}/${RECORDER_SESSION_ID}/input" 2>/dev/null || echo 000)
|
|
161
|
+
|
|
162
|
+
echo "[turn-recorder] spawn-with-input sessionId=${RECORDER_SESSION_ID:0:8} bytes=${INPUT_BYTES} filtered-from-records=${FILTERED_COUNT} input-http=${INPUT_HTTP}" >&2
|
|
161
163
|
|
|
162
164
|
END_MS=$(python3 -c 'import time; print(int(time.time()*1000))')
|
|
163
165
|
ELAPSED=$((END_MS - START_MS))
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
// renderConfigYml
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
|
|
26
|
-
test("renderConfigYml: HTTPS-only matches
|
|
26
|
+
test("renderConfigYml: HTTPS-only matches expected shape byte-for-byte", () => {
|
|
27
27
|
const spec: IngressSpec = {
|
|
28
28
|
tunnelId: "abc-123",
|
|
29
29
|
credentialsPath: "/home/admin/.maxy/cloudflared/abc-123.json",
|
|
@@ -99,7 +99,7 @@ test("renderConfigYml: empty string hostnames are treated as absent", () => {
|
|
|
99
99
|
// renderTunnelState
|
|
100
100
|
// ---------------------------------------------------------------------------
|
|
101
101
|
|
|
102
|
-
test("renderTunnelState:
|
|
102
|
+
test("renderTunnelState: base fields preserved when ssh/smb absent", () => {
|
|
103
103
|
const state: TunnelState = {
|
|
104
104
|
tunnelId: "abc",
|
|
105
105
|
tunnelName: "maxy",
|
|
@@ -55,11 +55,10 @@ import { fileURLToPath } from "node:url";
|
|
|
55
55
|
// source-of-truth contract:
|
|
56
56
|
// Runtime: brand.json `cdpPort` at `${MAXY_PLATFORM_ROOT}/config/brand.json`
|
|
57
57
|
// is authoritative. Wrapper exports MAXY_PLATFORM_ROOT and BRAND. Missing
|
|
58
|
-
// env / file / field → loud-fail with one of three named reasons.
|
|
59
|
-
//
|
|
60
|
-
// `cdp-unreachable
|
|
61
|
-
//
|
|
62
|
-
// never falls back silently.
|
|
58
|
+
// env / file / field → loud-fail with one of three named reasons. A
|
|
59
|
+
// silent CDP-port default would make every non-default brand fail
|
|
60
|
+
// `cdp-unreachable`; runtime config never falls back silently
|
|
61
|
+
// (NEO4J_URI sets the same precedent).
|
|
63
62
|
//
|
|
64
63
|
// Test overrides: when BOTH `LIST_CF_DOMAINS_CDP_HOST` and
|
|
65
64
|
// `LIST_CF_DOMAINS_CDP_PORT` are set, they win over brand.json. The
|
|
@@ -531,10 +530,9 @@ async function dumpHtml(
|
|
|
531
530
|
// truncated mid-document. The false positive (outerHTML exactly at the
|
|
532
531
|
// ceiling) is a rounding coincidence on the order of 1-in-5M and is
|
|
533
532
|
// acceptable — a loud truncation signal with a ~10^-7 false-positive
|
|
534
|
-
// rate is strictly better than silent truncation
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
// can re-scrape with a larger cap or `outerHTML` unsliced.
|
|
533
|
+
// rate is strictly better than silent truncation. A downstream
|
|
534
|
+
// investigator seeing `truncated=true` can re-scrape with a larger
|
|
535
|
+
// cap or `outerHTML` unsliced.
|
|
538
536
|
const truncated = htmlStr.length >= OUTER_HTML_CAPTURE_MAX_CHARS;
|
|
539
537
|
return { path: dumpPath, truncated };
|
|
540
538
|
} catch (err) {
|
|
@@ -358,8 +358,8 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
358
358
|
SPAWN_STDERR="$(tr '\n' ' ' < "${CHROMIUM_SPAWN_ERR}" | head -c 300 || echo unavailable)"
|
|
359
359
|
rm -f "${CHROMIUM_SPAWN_ERR}"
|
|
360
360
|
# Loud-fail rather than fire-and-forget fallback: a systemd-run failure
|
|
361
|
-
#
|
|
362
|
-
#
|
|
361
|
+
# leaves no teardown handle. Operator should see the bus-not-running
|
|
362
|
+
# / linger-not-enabled cause.
|
|
363
363
|
phase_line setup-tunnel step=browser-spawn result=error \
|
|
364
364
|
reason=systemd-run-failed exit="${SPAWN_RC}" stderr="${SPAWN_STDERR}" \
|
|
365
365
|
unit="${CHROMIUM_UNIT}"
|
|
@@ -456,15 +456,13 @@ fi
|
|
|
456
456
|
# --------------------------------------------------------------------------
|
|
457
457
|
# Step 2+3: Resolve the tunnel identity from operator input.
|
|
458
458
|
#
|
|
459
|
-
#
|
|
460
|
-
# -
|
|
461
|
-
#
|
|
462
|
-
#
|
|
463
|
-
#
|
|
464
|
-
# said "Done. tunnel=maxy-maxytest" while the operator's `maxytest` hostname
|
|
465
|
-
# kept resolving via a stale orphan.
|
|
459
|
+
# The local hostname has no authority over which tunnel the operator's
|
|
460
|
+
# logged-in Cloudflare account holds. Deriving TUNNEL_NAME locally (e.g.
|
|
461
|
+
# "${BRAND}-$(hostname -s)") and reusing-or-creating it produces a new
|
|
462
|
+
# tunnel when the device is renamed while existing CNAMEs continue to
|
|
463
|
+
# point at the old one.
|
|
466
464
|
#
|
|
467
|
-
#
|
|
465
|
+
# Contract: the form (rendered via /api/admin/cloudflare/tunnels list) is
|
|
468
466
|
# the source of truth. The endpoint passes exactly one of:
|
|
469
467
|
# TUNNEL_ID — operator selected an existing tunnel from the list
|
|
470
468
|
# TUNNEL_NAME — operator typed a name to create a new tunnel
|
|
@@ -505,8 +503,8 @@ if [ -n "${TUNNEL_ID:-}" ]; then
|
|
|
505
503
|
TUNNEL_SOURCE="operator-selected"
|
|
506
504
|
else
|
|
507
505
|
# Operator-create branch. Refuse to silently reuse an existing tunnel of
|
|
508
|
-
# the same name —
|
|
509
|
-
# The operator should have picked it from the list.
|
|
506
|
+
# the same name — silent reuse hides which tunnel CNAMEs actually point
|
|
507
|
+
# at. The operator should have picked it from the list.
|
|
510
508
|
EXISTING_ID="$(cloudflared --origincert "${CFG_DIR}/cert.pem" tunnel list --output json 2>/dev/null \
|
|
511
509
|
| jq -r --arg N "${TUNNEL_NAME}" '.[]? | select(.name == $N) | .id' | head -1)"
|
|
512
510
|
if [ -n "${EXISTING_ID}" ] && [ "${EXISTING_ID}" != "null" ]; then
|
|
@@ -111,9 +111,9 @@ export function renderConfigYml(spec: IngressSpec): string {
|
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
113
|
* Render the tunnel.state JSON, preserving SSH/SMB hostnames as additive
|
|
114
|
-
* fields so re-runs can rehydrate them. The shape
|
|
115
|
-
*
|
|
116
|
-
*
|
|
114
|
+
* fields so re-runs can rehydrate them. The base shape (tunnelId,
|
|
115
|
+
* tunnelName, domain, configPath, credentialsPath) is a superset that
|
|
116
|
+
* consumers without ssh/smb knowledge continue to read correctly.
|
|
117
117
|
*/
|
|
118
118
|
export function renderTunnelState(state: TunnelState): string {
|
|
119
119
|
const out: Record<string, string> = {
|
|
@@ -163,24 +163,10 @@ Each installed brand runs two per-brand `--user` systemd units (earlier platform
|
|
|
163
163
|
- `{hostname}.service` — the admin + public HTTP server on `127.0.0.1:19201` (public port + 1). Restarted by the upgrade flow; short downtime is expected during steps 8→11 of an upgrade. An earlier fix: the unit carries two port env vars — `PORT=<public>` (canonical public port, read by the upgrade detector) and `MAXY_UI_INTERNAL_PORT=<public+1>` (the port maxy-ui actually binds).
|
|
164
164
|
- `{hostname}-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to the main brand service and handles `/websockify` (VNC) WebSocket upgrades locally. An earlier fix: also hosts `/api/admin/actions/*` and `/api/admin/version*` — the Software Update modal's own routes — so the log stream survives the brand service's restart window. Does NOT restart during an upgrade — the browser WebSocket stays connected by construction.
|
|
165
165
|
|
|
166
|
-
**Port-drift recovery.** Devices upgraded between Tasks 647 and 666 may have drifted +1 on every upgrade because the pre-Task-666 installer wrote `Environment=PORT=<internal>` into `{hostname}.service` and the upgrade reader correctly treated `PORT=` as public. The first post-Task-666 install detects this (comparing maxy's PORT against the edge's EDGE_PORT) and emits a one-shot loud log: `[port-recovery] detected drift maxy=<X> edge=<Y> — pinning at <Y>`. Subsequent upgrades are silent. If your Cloudflare tunnel was pointing at a drifted port, the ingress `config.yml` still needs a one-time manual fix: `sed -i 's|localhost:<old>|localhost:<current>|' ~/.{configDir}/cloudflared/config.yml && cloudflared tunnel ingress validate`. {{productName}} never rewrites cloudflared config programmatically.
|
|
167
|
-
|
|
168
166
|
Upgrade and Cloudflare setup run as detached actions: `systemd-run --user` transient units per invocation with stdout+stderr persisted to `~/.maxy/logs/actions/<actionId>.log` and streamed to the UI via SSE. No boot-time service file exists for these.
|
|
169
167
|
|
|
170
168
|
If an action looks stuck, read `~/.maxy/logs/actions/<actionId>.log` directly for the full output, or `journalctl --user --identifier=maxy-action-<actionId>` for systemd's record.
|
|
171
169
|
|
|
172
|
-
**Pre-Task-662 / pre-Task-664 upgrade** — devices that ran an installer have legacy shared `maxy-edge.service` / `maxy-ttyd.service` units; devices that ran have per-brand `{hostname}-ttyd.service` units plus a pinned `/usr/local/bin/ttyd` binary. Neither is removed automatically — do this cleanup once per device before re-running any installer:
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
systemctl --user stop maxy-edge maxy-ttyd realagent-ttyd 2>/dev/null || true
|
|
176
|
-
systemctl --user disable maxy-edge maxy-ttyd realagent-ttyd 2>/dev/null || true
|
|
177
|
-
rm -f ~/.config/systemd/user/maxy-edge.service \
|
|
178
|
-
~/.config/systemd/user/maxy-ttyd.service \
|
|
179
|
-
~/.config/systemd/user/realagent-ttyd.service
|
|
180
|
-
sudo rm -f /usr/local/bin/ttyd
|
|
181
|
-
systemctl --user daemon-reload
|
|
182
|
-
```
|
|
183
|
-
|
|
184
170
|
## Linux laptops: snap-confined Chromium replacement
|
|
185
171
|
|
|
186
172
|
On Ubuntu 24.04 (Noble) the system Chromium binary at `/usr/bin/chromium` is a symlink into the snap. Snap's AppArmor profile denies writes to hidden directories under your home folder, so the per-brand Chromium profile at `~/.{brand}/chromium-profile/` is unwritable and the VNC browser never starts. Pi installs (Debian Bookworm) are unaffected because Bookworm ships a real `.deb` chromium.
|
|
@@ -241,6 +227,8 @@ Skills, agents, hooks, and commands directories at the plugin root are auto-disc
|
|
|
241
227
|
|
|
242
228
|
**Diagnostic path** — `grep "\[plugin-install\]" ~/.<brand>/logs/install-*.log | tail -50`; compare row count against `cat brand.json | jq '.externalPlugins | length'` plus the on-disk plugin count under `<INSTALL_DIR>/platform/plugins/` and `<INSTALL_DIR>/premium-plugins/`.
|
|
243
229
|
|
|
230
|
+
**Premium MCP dependency install** — Premium-plugin MCP servers ship `dist/` + `package.json` in the bundle but not `node_modules` (npm pack strips them, same as `server/`). `buildPlatform()` discovers every `<INSTALL_DIR>/premium-plugins/<bundle>/plugins/<plugin>/mcp/package.json` and runs `npm install --omit=dev` there, wiping any prior `node_modules` first. The summary log line `[install] premium-mcp-install dirs=<n>` is emitted before the loop runs, so `dirs=0` is itself a regression signal when a brand ships premium plugins.
|
|
231
|
+
|
|
244
232
|
## Running multiple brands on one device
|
|
245
233
|
|
|
246
234
|
A single Pi or laptop can host more than one brand (for example Maxy and Real Agent) side by side. Each brand runs as its own service on its own port, with its own install directory and its own data. Installing one brand does not touch the other.
|
|
@@ -70,11 +70,11 @@ There is no dashboard, no settings panel, no menus. Everything is done through c
|
|
|
70
70
|
|
|
71
71
|
The chat input auto-grows as you type — it expands to fit your message and shrinks back when you delete text. You can also drag the resize handle above the input to set a custom height.
|
|
72
72
|
|
|
73
|
-
The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (the database-operator recorder, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with Maxy and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The chat and artefact divider is drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Open JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
|
|
73
|
+
The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (the database-operator recorder, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with Maxy and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The sidebar's right edge is drag-resizable on every admin page (Sessions root, Graph, and Data): drag the handle to widen or narrow the sidebar, and your chosen width is remembered across reloads and shared across all three pages. The drag handle is mounted by each AdminShell consumer rather than by AdminShell itself, so any new admin route must include `<SidebarSplitter />` as a direct child of its `<AdminShell>` to pick up the shared width. The chat and artefact divider is also drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Open JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
|
|
74
74
|
|
|
75
75
|
Page titles are brand-aware: the browser tab shows your product name (e.g. `Real Agent` instead of `Maxy`) on every shell — chat, graph, and data — so a non-default brand never leaks the default name in tab strips or browser history.
|
|
76
76
|
|
|
77
|
-
**Session lifecycle and reconcile model.** The sidebar Sessions list is driven by a single Server-Sent Events feed at `/api/admin/claude-sessions/events`. The session manager watches the two directories Claude Code writes (`${CLAUDE_CONFIG_DIR}/sessions/<pid>.json` for live state, `${CLAUDE_CONFIG_DIR}/projects/<slug>/<sid>.jsonl` for transcripts) and emits `row-created`, `row-updated`, `row-archived`, or `row-removed` deltas to every connected browser tab. Three real delete shapes map to deltas — there is no fourth: PID file gone with JSONL surviving demotes the row to `row-archived`; PID file gone with no JSONL ever written (the per-turn recorder case) emits `row-removed` against the unindexed sessionId; a JSONL deletion against an already-unindexed row also emits `row-removed`. The recorder branch is what reconciles transient hidden spawns — without it, ghost rows persist after the recorder exits. On connect the manager replays the current row index so a freshly-opened tab catches up without polling, then streams deltas as files change on disk. Two open tabs see the same list within ~300ms of any spawn, status flip, or exit; no refresh button required for state to be current. The legacy `/list` fetch and `useAdminSessions` hook stay mounted to serve the ConversationsModal and the post-action reconcile path in `session-actions`, but the sidebar's visible rows come from the row store, not from `/list`. Each EventSource open emits `[admin-events] client-connected ip=<…> seeded-rows=<n>` server-side and `[admin-ui] session-row-store connected events-received=<n>` in the browser console; transport drops log `[admin-ui] session-row-store reconnect trigger=<auto|manual> attempt=<n> delay-ms=<n>` until the EventSource reattaches. The small dot at the right edge of the Active/Archived/All segmented control is the live-updates indicator: sage when the SSE feed is connected, grey when the feed has dropped. The grey state is an actionable button — clicking it cancels any pending backoff and re-opens the feed immediately, with the click logged as `trigger=manual` so manual retries are distinguishable from automatic ones in the console.
|
|
77
|
+
**Session lifecycle and reconcile model.** The sidebar Sessions list is driven by a single Server-Sent Events feed at `/api/admin/claude-sessions/events`. The session manager watches the two directories Claude Code writes (`${CLAUDE_CONFIG_DIR}/sessions/<pid>.json` for live state, `${CLAUDE_CONFIG_DIR}/projects/<slug>/<sid>.jsonl` for transcripts) and emits `row-created`, `row-updated`, `row-archived`, or `row-removed` deltas to every connected browser tab. Three real delete shapes map to deltas — there is no fourth: PID file gone with JSONL surviving demotes the row to `row-archived`; PID file gone with no JSONL ever written (the per-turn recorder case) emits `row-removed` against the unindexed sessionId; a JSONL deletion against an already-unindexed row also emits `row-removed`. The recorder branch is what reconciles transient hidden spawns — without it, ghost rows persist after the recorder exits. On connect the manager replays the current row index so a freshly-opened tab catches up without polling, then streams deltas as files change on disk. Two open tabs see the same list within ~300ms of any spawn, status flip, or exit; no refresh button required for state to be current. The legacy `/list` fetch and `useAdminSessions` hook stay mounted to serve the ConversationsModal and the post-action reconcile path in `session-actions`, but the sidebar's visible rows come from the row store, not from `/list`. Each EventSource open emits `[admin-events] client-connected ip=<…> seeded-rows=<n>` server-side and `[admin-ui] session-row-store connected events-received=<n>` in the browser console; transport drops log `[admin-ui] session-row-store reconnect trigger=<auto|manual> attempt=<n> delay-ms=<n>` until the EventSource reattaches. The small dot at the right edge of the Active/Archived/All segmented control is the live-updates indicator: sage when the SSE feed is connected, grey when the feed has dropped. The grey state is an actionable button — clicking it cancels any pending backoff and re-opens the feed immediately, with the click logged as `trigger=manual` so manual retries are distinguishable from automatic ones in the console. The refresh icon at the top of the Sessions list is the operator-recoverable reconcile path against any SSE gap: it fetches `/api/admin/claude-sessions` and passes the authoritative id set to the row store, which evicts any indexed row that the server no longer reports. SSE replay only re-asserts currently-indexed rows and never emits `row-removed` for a row that vanished while disconnected, so without this manual surface a stale row can persist until the operator reloads the tab. Each click logs `[admin-ui] session-row-store reconcile evicted=<n> kept=<n>` when at least one row is evicted, and is silent otherwise.
|
|
78
78
|
|
|
79
79
|
The row feed sits behind `requireAdminSession` like every other admin route, so the URL must carry `?session_key=<cacheKey>` — `EventSource` cannot send custom headers, so the query string is the only viable transport. Every admin URL (fetch and EventSource alike) routes through the shared `appendAdminSessionKey(url, cacheKey)` helper exported from `app/lib/useAdminFetch.ts`, which is the single source of truth for the convention; no caller constructs the query string by hand. On a 4xx rejection the browser-side store probes the same URL once per reconnect (suppressed after a successful `open`, capped at one fetch per attempt) and logs `[admin-ui] session-row-store sse-error status=<n> code=<code> attempt=<n>`. The `code` field uses the closed `AdminSessionRejectCode` taxonomy (`session-missing | session-not-registered | session-expired-age | grant-expired`, plus a default `unknown` bucket) that mirrors the server-side rejection emitted by `requireAdminSession`, so a single grep correlates client and server timelines on the same code.
|
|
80
80
|
|
|
@@ -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 (10 sub-plugins) | UK estate agency skills — sales, listings, vendor management, buyer management, lead generation, coaching, business operations, teaching, Loop CRM, and property brochures. 3 specialist roles (negotiator, valuer, compliance) | 4 sub-plugins (estate-sales, buyers, estate-coaching, estate-teaching) |
|
|
76
|
+
| `real-agent` | Bundle (10 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), and property brochures. 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.
|
|
@@ -178,7 +178,7 @@ If the initial Cloudflare login fails during setup, {{productName}} will fall ba
|
|
|
178
178
|
|
|
179
179
|
**Continuous `err-class=econnrefused-coldstart` for >30 s past the last `[edge] listening` line** indicates the upstream never binds — the upgrade or boot has stalled. Recover via `sudo systemctl --user status maxy.service` and check the action runner log per the next section. Permanent-failure UI escalation (turning the holding page into an error after N seconds) is intentionally deferred.
|
|
180
180
|
|
|
181
|
-
**The literal string `maxy-ui` should never appear in `edge.log` or in any user-visible 502 body**, regardless of brand. If it does, the edge is running
|
|
181
|
+
**The literal string `maxy-ui` should never appear in `edge.log` or in any user-visible 502 body**, regardless of brand. If it does, the edge is running stale code — re-bundle and re-publish.
|
|
182
182
|
|
|
183
183
|
**Verifying the holding page locally:** `curl -sS -H 'Accept: text/html' http://127.0.0.1:<EDGE_PORT>/` while `maxy.service` is stopped should return HTML containing the brand `productName`. The `Accept: text/html` header is required — non-html clients (default `curl`, `fetch`, XHR) get the branded plain-text 502 instead, so the holding page's own `/api/health` polls don't break themselves during cold-start.
|
|
184
184
|
|
|
@@ -262,7 +262,7 @@ Failure modes:
|
|
|
262
262
|
|
|
263
263
|
## Software Update click shows an error instead of opening the terminal
|
|
264
264
|
|
|
265
|
-
> **
|
|
265
|
+
> **First-line diagnostic for the byte-stream xterm.js terminal surface:** `sudo systemctl --user status maxy-ttyd` plus `sudo grep 'ttyd-proxy' ~/.maxy/logs/edge-boot.log | tail -20`. Failure mode signals: `ttyd-ws-upgrade accepted` with no `ttyd-proxy-open` → `maxy-ttyd.service` is down; `ttyd-proxy-open` with no `ttyd-proxy-chunk dir=upstream→client` → ttyd/tmux is not attaching a PTY.
|
|
266
266
|
|
|
267
267
|
**Symptom:** You clicked **Upgrade** in the Software Update modal, but instead of the VNC terminal overlay appearing, the modal shows a red error row like:
|
|
268
268
|
|
|
@@ -337,7 +337,7 @@ sudo grep 'started.*reason=upgrade' ~/.maxy/logs/terminal-launch.log | tail -3
|
|
|
337
337
|
# Expected shape: started pid=<N> display=:99 cmd="/usr/bin/xterm... -e bash -c 'npx -y @rubytech/create-maxy@latest; exec bash'" transport=vnc windowPresent=true reason=upgrade
|
|
338
338
|
```
|
|
339
339
|
|
|
340
|
-
If the `cmd=` field does not contain `-e bash -c`, re-run the installer — the vnc.sh on the device is
|
|
340
|
+
If the `cmd=` field does not contain `-e bash -c`, re-run the installer — the vnc.sh on the device is stale. If the command IS logged correctly but nothing is running, open the VNC overlay and type `history | tail` inside the shell — if the npx line is there, it ran and exited (check `~/.maxy/logs/install-*.log` for the exit status).
|
|
341
341
|
|
|
342
342
|
---
|
|
343
343
|
|
|
@@ -533,9 +533,9 @@ sudo systemctl --user start maxy-ui
|
|
|
533
533
|
|
|
534
534
|
## Admin DevTools console floods with `onboarding-banner-mount` or `sessions-poll` lines
|
|
535
535
|
|
|
536
|
-
**
|
|
536
|
+
**Regression symptom.** Open DevTools on the admin shell at `/` with `onboardingComplete=false`, leave the page idle for a minute, then scroll back through the console. Thousands of `[admin-ui] onboarding-banner-mount onboardingComplete=false` lines (one per AdminShell render, ~40/min driven by the 3s sessions poll) with no per-tick poll telemetry indicates the banner-mount log has regressed back into the render body.
|
|
537
537
|
|
|
538
|
-
**Steady-state invariants at
|
|
538
|
+
**Steady-state invariants at `/`:**
|
|
539
539
|
|
|
540
540
|
- `grep -c '\[admin-ui\] onboarding-banner-mount' ~/.maxy/logs/admin-ui-console.log` equals page-load count plus onboarding-flip count, not the render count. Sustained climb at idle means the banner mount log regressed back into the render body (fix).
|
|
541
541
|
- `grep -c '\[admin-ui\] sessions-poll' ~/.maxy/logs/admin-ui-console.log` over a 60-minute idle window equals zero. The hook no longer installs a `setInterval`; every `sessions-poll` line is operator-triggered (initial mount, refresh button, post-mutation refetch). One or more lines during operator idle means `setInterval` was reinstated.
|