@rubytech/create-maxy-code 0.1.156 → 0.1.158
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/neo4j/schema.cypher +3 -2
- package/payload/platform/plugins/admin/hooks/lib/admin-graph-pass-common.sh +140 -0
- package/payload/platform/plugins/admin/hooks/pre-turn-graph-pass.sh +33 -83
- package/payload/platform/plugins/admin/hooks/session-end-retrospective.sh +32 -91
- package/payload/platform/plugins/docs/references/deployment.md +3 -1
- package/payload/platform/plugins/memory/PLUGIN.md +3 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js +11 -12
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts +35 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js +80 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js.map +1 -0
- package/payload/platform/plugins/memory/references/schema-base.md +2 -2
- package/payload/platform/scripts/identity-forbidden-token-check.mjs +86 -0
- package/payload/platform/scripts/smoke-boot-services.sh +191 -41
- package/payload/platform/services/claude-session-manager/dist/index.js +18 -1
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +10 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +91 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +14 -63
- package/payload/server/public/assets/{admin-kh_oCHeS.js → admin-3fLFD5h_.js} +23 -23
- package/payload/server/public/index.html +1 -1
package/package.json
CHANGED
|
@@ -1397,8 +1397,9 @@ FOR (rec:Recommendation) ON (rec.sessionKey);
|
|
|
1397
1397
|
|
|
1398
1398
|
// ----------------------------------------------------------
|
|
1399
1399
|
// Idea (Task 352) — original-thinking candidates extracted by
|
|
1400
|
-
// the
|
|
1401
|
-
//
|
|
1400
|
+
// the pre-turn graph pass from admin-side conversations and written
|
|
1401
|
+
// by `database-operator` per-write briefs admin dispatches. Written
|
|
1402
|
+
// via memory-write(labels:["Idea"], ...) with one edge
|
|
1402
1403
|
// (:Conversation)-[:HAS_IDEA]->(:Idea). Forward-only write surface:
|
|
1403
1404
|
// no historical backfill, no dedup, no clustering — let the brain
|
|
1404
1405
|
// accumulate first. Declared here so memory-write's label validator
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared plumbing for the two admin-side hooks that orchestrate admin-walked
|
|
3
|
+
# graph writes: `pre-turn-graph-pass.sh` (UserPromptSubmit) and
|
|
4
|
+
# `session-end-retrospective.sh` (Stop on operator end-intent). Task 439.
|
|
5
|
+
#
|
|
6
|
+
# Both hooks are the same process — same stdin parse, same role / specialist
|
|
7
|
+
# / transcript guards, same log-ingest envelope, same admin-walks contract
|
|
8
|
+
# dispatching per-write briefs to `database-operator`. Divergence across
|
|
9
|
+
# Tasks 303 → 412 → 425 → 430 → 282 made shared plumbing a recurring-bug
|
|
10
|
+
# surface; this file is the single body. Only the trigger-specific gating
|
|
11
|
+
# and the dispatch directive text live in each hook.
|
|
12
|
+
#
|
|
13
|
+
# Caller contract:
|
|
14
|
+
# 1. Set `AGP_LOG_TAG` to the hook's identifying tag before sourcing.
|
|
15
|
+
# `pre-turn-graph-pass` and `session-retrospective` are the two
|
|
16
|
+
# current values; the helper itself does not log under its own tag.
|
|
17
|
+
# 2. Source this file.
|
|
18
|
+
# 3. Call `agp_setup_logging`, `agp_read_stdin`, `agp_parse_stdin`,
|
|
19
|
+
# `agp_guard_role_specialist`, `agp_guard_transcript` in that order.
|
|
20
|
+
# Each guard exits 0 from inside the helper when its condition fires
|
|
21
|
+
# (sourced `exit` exits the caller).
|
|
22
|
+
# 4. After the guards return, the hook proceeds with its event-specific
|
|
23
|
+
# body (sentinel-grep + intent token for session-end; post-turn-context
|
|
24
|
+
# fetch + directive envelope for pre-turn).
|
|
25
|
+
#
|
|
26
|
+
# Globals set by the helper (read by callers):
|
|
27
|
+
# INPUT — raw stdin (empty when none piped)
|
|
28
|
+
# SESSION_ID — parsed `session_id` field, empty on parse failure
|
|
29
|
+
# TRANSCRIPT_PATH — parsed `transcript_path` field
|
|
30
|
+
# HOOK_EVENT_NAME — parsed `hook_event_name` field
|
|
31
|
+
# SESSION_ID_REPORTED — `$SESSION_ID` or the literal "unknown"
|
|
32
|
+
# UI_BASE — `http://127.0.0.1:${MAXY_UI_INTERNAL_PORT}`
|
|
33
|
+
# LOG_INGEST_URL — `${UI_BASE}/api/admin/log-ingest`
|
|
34
|
+
#
|
|
35
|
+
# Functions:
|
|
36
|
+
# agp_setup_logging — resolves UI port + URLs, or exits 0 with
|
|
37
|
+
# a stderr loud-fail when the port is unset
|
|
38
|
+
# (fail-open so a missing port never bricks
|
|
39
|
+
# the operator).
|
|
40
|
+
# agp_emit_log <line> — POSTs `{tag, level:"info", line}` to
|
|
41
|
+
# log-ingest; silent on network failure.
|
|
42
|
+
# agp_read_stdin — slurps stdin into `INPUT`, leaves it empty
|
|
43
|
+
# when no input is piped.
|
|
44
|
+
# agp_parse_stdin — populates `SESSION_ID`, `TRANSCRIPT_PATH`,
|
|
45
|
+
# `HOOK_EVENT_NAME`, `SESSION_ID_REPORTED`.
|
|
46
|
+
# agp_guard_role_specialist — exits 0 with `trigger-skipped` log on
|
|
47
|
+
# `role-not-admin` or `is-specialist`.
|
|
48
|
+
# agp_guard_transcript — exits 0 with `trigger-skipped` log on
|
|
49
|
+
# `empty-stdin` (when $1="require-stdin") or
|
|
50
|
+
# `missing-transcript`.
|
|
51
|
+
|
|
52
|
+
if [ -z "${AGP_LOG_TAG:-}" ]; then
|
|
53
|
+
echo "[admin-graph-pass-common] AGP_LOG_TAG not set by caller" >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
agp_setup_logging() {
|
|
58
|
+
local ui_port="${MAXY_UI_INTERNAL_PORT:-}"
|
|
59
|
+
if [ -z "$ui_port" ]; then
|
|
60
|
+
echo "[${AGP_LOG_TAG}] trigger-skipped sessionId=unknown reason=missing-env env=MAXY_UI_INTERNAL_PORT" >&2
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
UI_BASE="http://127.0.0.1:${ui_port}"
|
|
64
|
+
LOG_INGEST_URL="${UI_BASE}/api/admin/log-ingest"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
agp_emit_log() {
|
|
68
|
+
local line="$1"
|
|
69
|
+
curl -sS -o /dev/null -X POST \
|
|
70
|
+
-H 'Content-Type: application/json' \
|
|
71
|
+
--max-time 2 \
|
|
72
|
+
--data "$(AGP_TAG="$AGP_LOG_TAG" AGP_LINE="$line" python3 -c '
|
|
73
|
+
import os, json
|
|
74
|
+
print(json.dumps({"tag": os.environ["AGP_TAG"], "level": "info", "line": os.environ["AGP_LINE"]}))
|
|
75
|
+
')" \
|
|
76
|
+
"$LOG_INGEST_URL" 2>/dev/null || true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
agp_read_stdin() {
|
|
80
|
+
INPUT=""
|
|
81
|
+
if [ ! -t 0 ]; then
|
|
82
|
+
INPUT=$(cat)
|
|
83
|
+
fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
agp_parse_stdin() {
|
|
87
|
+
local parsed
|
|
88
|
+
parsed=$(printf '%s' "$INPUT" | python3 -c '
|
|
89
|
+
import sys, json
|
|
90
|
+
try:
|
|
91
|
+
d = json.load(sys.stdin)
|
|
92
|
+
sid = d.get("session_id", "") or ""
|
|
93
|
+
tpath = d.get("transcript_path", "") or ""
|
|
94
|
+
event = d.get("hook_event_name", "") or ""
|
|
95
|
+
print(f"{sid}\t{tpath}\t{event}")
|
|
96
|
+
except Exception:
|
|
97
|
+
print("\t\t")
|
|
98
|
+
' 2>/dev/null)
|
|
99
|
+
SESSION_ID="${parsed%% *}"
|
|
100
|
+
local rest="${parsed#* }"
|
|
101
|
+
TRANSCRIPT_PATH="${rest%% *}"
|
|
102
|
+
HOOK_EVENT_NAME="${rest#* }"
|
|
103
|
+
SESSION_ID_REPORTED="${SESSION_ID:-unknown}"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
agp_guard_role_specialist() {
|
|
107
|
+
if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
|
|
108
|
+
agp_emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=role-not-admin"
|
|
109
|
+
exit 0
|
|
110
|
+
fi
|
|
111
|
+
# Recursion guard. A specialist subagent dispatched via the Task tool
|
|
112
|
+
# (e.g. database-operator running the operator's graph writes) carries
|
|
113
|
+
# `MAXY_SPECIALIST=<name>` on the PTY env. Skip — the hooks only fire
|
|
114
|
+
# for the operator's own admin session.
|
|
115
|
+
#
|
|
116
|
+
# Invariant the PTY spawner is expected to honour: `MAXY_SPECIALIST` is
|
|
117
|
+
# either set to a non-empty specialist name OR absent. An explicit empty
|
|
118
|
+
# string from a wrapper would slip the recursion guard here (`-n ""` is
|
|
119
|
+
# false) — that would be a defect at the spawn site, not here.
|
|
120
|
+
if [ -n "${MAXY_SPECIALIST:-}" ]; then
|
|
121
|
+
agp_emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=is-specialist specialist=${MAXY_SPECIALIST}"
|
|
122
|
+
exit 0
|
|
123
|
+
fi
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
# Usage: agp_guard_transcript [require-stdin]
|
|
127
|
+
# Pass `require-stdin` (session-end-retrospective) to emit `empty-stdin` on a
|
|
128
|
+
# blank `INPUT`. The pre-turn-graph-pass hook calls without the flag because
|
|
129
|
+
# its `wrong-hook-event` guard already runs before this point and parse
|
|
130
|
+
# failure on a non-empty INPUT collapses to `missing-transcript` cleanly.
|
|
131
|
+
agp_guard_transcript() {
|
|
132
|
+
if [ "${1:-}" = "require-stdin" ] && [ -z "$INPUT" ]; then
|
|
133
|
+
agp_emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=empty-stdin"
|
|
134
|
+
exit 0
|
|
135
|
+
fi
|
|
136
|
+
if [ -z "$SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
137
|
+
agp_emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=missing-transcript"
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
140
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# UserPromptSubmit hook — pre-turn graph pass. Tasks 425, 430.
|
|
2
|
+
# UserPromptSubmit hook — pre-turn graph pass. Tasks 425, 430, 439.
|
|
3
3
|
#
|
|
4
4
|
# Fires once per admin-agent operator prompt submission. Emits a
|
|
5
5
|
# directive into the admin agent's `additionalContext` that instructs
|
|
@@ -9,32 +9,19 @@
|
|
|
9
9
|
# per candidate write. Admin is the session-walker; the specialist only
|
|
10
10
|
# sees a narrowly-scoped per-write brief, never the conversation.
|
|
11
11
|
#
|
|
12
|
-
# Task 430 corrects the Task 425 orchestration shape
|
|
13
|
-
# packaged the full conversation blob into the specialist's prompt
|
|
14
|
-
# collapsed
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# schema doctrine read as binding the agent (Task 199 / 995). The
|
|
19
|
-
# session-end retrospective never had this defect because admin walked
|
|
20
|
-
# the transcript itself and dispatched per-item briefs to the
|
|
21
|
-
# specialist — Task 430 brings the per-turn pass to the same shape.
|
|
12
|
+
# Task 430 corrects the Task 425 orchestration shape (the Task 425 hook
|
|
13
|
+
# packaged the full conversation blob into the specialist's prompt and
|
|
14
|
+
# collapsed data and instruction channels into one context, so operator
|
|
15
|
+
# prose pattern-matching as a writer-binding instruction was treated as
|
|
16
|
+
# binding and the pass self-denied — same failure class as the writer-POV
|
|
17
|
+
# schema doctrine read as binding the agent, Tasks 199 / 995).
|
|
22
18
|
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# `session-end-retrospective.sh` Stop hook walks the full transcript at
|
|
30
|
-
# `/end` and picks up the final assistant reply.
|
|
31
|
-
#
|
|
32
|
-
# The doctrinal early exits remain:
|
|
33
|
-
# - MAXY_SESSION_ROLE must equal "admin" → reason=role-not-admin
|
|
34
|
-
# - MAXY_SPECIALIST must be empty → reason=is-specialist
|
|
35
|
-
# `missing-transcript` is retained as a safety exit (the prior-writes
|
|
36
|
-
# context fetch still needs a real session). `wrong-hook-event` rejects
|
|
37
|
-
# stale Stop-shaped fires.
|
|
19
|
+
# Task 439 converges the shared plumbing — stdin parse, role / specialist
|
|
20
|
+
# / transcript guards, log-ingest envelope — with `session-end-retrospective.sh`
|
|
21
|
+
# via `lib/admin-graph-pass-common.sh`. The only event-specific code in
|
|
22
|
+
# this hook is the wrong-hook-event check, the post-turn-context fetch,
|
|
23
|
+
# the directive-body composition, and the UserPromptSubmit JSON envelope
|
|
24
|
+
# emission on stdout.
|
|
38
25
|
#
|
|
39
26
|
# Input: Claude Code's UserPromptSubmit hook stdin shape
|
|
40
27
|
# { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>",
|
|
@@ -46,74 +33,37 @@
|
|
|
46
33
|
# Output: stdout JSON of the documented UserPromptSubmit shape:
|
|
47
34
|
# { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit",
|
|
48
35
|
# "additionalContext": "<directive body>" } }
|
|
49
|
-
# exit 0.
|
|
50
|
-
# `[system: pre-turn-graph-pass-hook]` so the agent's IDENTITY.md
|
|
51
|
-
# UserPromptSubmit-directive section can distinguish it from operator
|
|
52
|
-
# text. The agent performs the per-write dispatches as a background
|
|
53
|
-
# pass and then continues with its normal response to the operator.
|
|
36
|
+
# exit 0.
|
|
54
37
|
|
|
55
38
|
set -uo pipefail
|
|
56
39
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
40
|
+
AGP_LOG_TAG="pre-turn-graph-pass"
|
|
41
|
+
AGP_LIB="$(dirname "$0")/lib/admin-graph-pass-common.sh"
|
|
42
|
+
if [ ! -f "$AGP_LIB" ]; then
|
|
43
|
+
echo "[${AGP_LOG_TAG}] trigger-skipped sessionId=unknown reason=missing-helper path=${AGP_LIB}" >&2
|
|
60
44
|
exit 0
|
|
61
45
|
fi
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
CONTEXT_URL="${UI_BASE}/api/admin/post-turn-context"
|
|
46
|
+
# shellcheck source=lib/admin-graph-pass-common.sh
|
|
47
|
+
source "$AGP_LIB"
|
|
65
48
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
curl -sS -o /dev/null -X POST \
|
|
69
|
-
-H 'Content-Type: application/json' \
|
|
70
|
-
--max-time 2 \
|
|
71
|
-
--data "$(python3 -c '
|
|
72
|
-
import sys, json
|
|
73
|
-
print(json.dumps({"tag": "pre-turn-graph-pass", "level": "info", "line": sys.argv[1]}))
|
|
74
|
-
' "$line")" \
|
|
75
|
-
"$LOG_INGEST_URL" 2>/dev/null || true
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
INPUT=""
|
|
79
|
-
if [ ! -t 0 ]; then
|
|
80
|
-
INPUT=$(cat)
|
|
81
|
-
fi
|
|
49
|
+
agp_setup_logging
|
|
50
|
+
CONTEXT_URL="${UI_BASE}/api/admin/post-turn-context"
|
|
82
51
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
try:
|
|
86
|
-
d = json.load(sys.stdin)
|
|
87
|
-
sid = d.get("session_id", "") or ""
|
|
88
|
-
tpath = d.get("transcript_path", "") or ""
|
|
89
|
-
event = d.get("hook_event_name", "") or ""
|
|
90
|
-
print(f"{sid}\t{tpath}\t{event}")
|
|
91
|
-
except Exception:
|
|
92
|
-
print("\t\t")
|
|
93
|
-
' 2>/dev/null)
|
|
94
|
-
SESSION_ID="${PARSED%% *}"
|
|
95
|
-
REST="${PARSED#* }"
|
|
96
|
-
TRANSCRIPT_PATH="${REST%% *}"
|
|
97
|
-
HOOK_EVENT_NAME="${REST#* }"
|
|
98
|
-
SESSION_ID_REPORTED="${SESSION_ID:-unknown}"
|
|
52
|
+
agp_read_stdin
|
|
53
|
+
agp_parse_stdin
|
|
99
54
|
|
|
55
|
+
# Wrong-hook-event guard. Pre-turn-graph-pass is UserPromptSubmit-only;
|
|
56
|
+
# reject stale Stop-shaped payloads before the role / transcript guards
|
|
57
|
+
# run so the operator observes a `wrong-hook-event` skip line, not a
|
|
58
|
+
# misleading `missing-transcript`.
|
|
100
59
|
if [ -n "$HOOK_EVENT_NAME" ] && [ "$HOOK_EVENT_NAME" != "UserPromptSubmit" ]; then
|
|
101
|
-
|
|
102
|
-
exit 0
|
|
103
|
-
fi
|
|
104
|
-
if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
|
|
105
|
-
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=role-not-admin"
|
|
106
|
-
exit 0
|
|
107
|
-
fi
|
|
108
|
-
if [ -n "${MAXY_SPECIALIST:-}" ]; then
|
|
109
|
-
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=is-specialist specialist=${MAXY_SPECIALIST}"
|
|
110
|
-
exit 0
|
|
111
|
-
fi
|
|
112
|
-
if [ -z "$SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
113
|
-
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=missing-transcript"
|
|
60
|
+
agp_emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=wrong-hook-event event=${HOOK_EVENT_NAME}"
|
|
114
61
|
exit 0
|
|
115
62
|
fi
|
|
116
63
|
|
|
64
|
+
agp_guard_role_specialist
|
|
65
|
+
agp_guard_transcript
|
|
66
|
+
|
|
117
67
|
CONVERSATION_ID="${CONVERSATION_NODE_ID:-}"
|
|
118
68
|
|
|
119
69
|
# Fetch the actual content of every node already written under this
|
|
@@ -196,7 +146,7 @@ PY
|
|
|
196
146
|
)
|
|
197
147
|
CONTEXT_BYTES=${#DIRECTIVE_BODY}
|
|
198
148
|
|
|
199
|
-
|
|
149
|
+
agp_emit_log "dispatch sessionId=${SESSION_ID} conversationId=${CONVERSATION_ID:-<unset>} directive-shape=admin-walks priorWritesCount=${PRIOR_WRITES_COUNT} contextBytes=${CONTEXT_BYTES} ms=0"
|
|
200
150
|
|
|
201
151
|
# Emit the documented UserPromptSubmit JSON envelope on stdout. Python
|
|
202
152
|
# handles JSON encoding so embedded newlines, quotes, and backslashes
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Stop hook — gates session termination until the admin agent has performed
|
|
3
|
-
# a
|
|
4
|
-
# updates, graph-completeness sweep) and called
|
|
5
|
-
# `session-retrospective-mark-complete` MCP sentinel
|
|
3
|
+
# a four-pass retrospective (technical learnings, operator-relationship
|
|
4
|
+
# updates, graph-completeness sweep, typed-edge auto-extraction) and called
|
|
5
|
+
# the deterministic `session-retrospective-mark-complete` MCP sentinel
|
|
6
|
+
# tool. Task 282; converged onto the Task 439 shared helper.
|
|
6
7
|
#
|
|
7
8
|
# Trigger detection. End-intent is the operator's most recent real-user
|
|
8
9
|
# message matching one of the literal token strings:
|
|
@@ -23,109 +24,49 @@
|
|
|
23
24
|
# whose `name` is exactly `mcp__admin__session-retrospective-mark-complete`
|
|
24
25
|
# — that is the prefixed form Claude Code writes for MCP tools in the
|
|
25
26
|
# JSONL; the unprefixed name only appears in the operator-facing
|
|
26
|
-
# instruction text below. The
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
27
|
+
# instruction text below. The sentinel-grep doubles as the re-entry guard:
|
|
28
|
+
# every Stop that fires after the agent's sentinel call sees the sentinel
|
|
29
|
+
# and exits 0 with `gate-released`, so the agent's wrap-up turn never
|
|
30
|
+
# re-blocks.
|
|
30
31
|
# [[feedback_no_stdout_parsing_for_control_flow]],
|
|
31
32
|
# [[feedback_doctrine_paragraph_is_not_a_gate]] — the LLM is removed from
|
|
32
33
|
# the per-decision path entirely.
|
|
33
34
|
#
|
|
34
|
-
# Gating (
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
# -
|
|
39
|
-
#
|
|
40
|
-
# "database-operator" (and any other (recursion guard — a
|
|
41
|
-
# specialist value) specialist subagent stop
|
|
42
|
-
# must not re-enter this gate)
|
|
43
|
-
# - Stop-hook stdin must be non-empty → reason=empty-stdin
|
|
44
|
-
# - transcript_path must exist on disk → reason=missing-transcript
|
|
45
|
-
# - latest real-user message must match an → reason=no-intent-match
|
|
46
|
-
# end-intent token
|
|
35
|
+
# Gating (common guards live in `lib/admin-graph-pass-common.sh`; this
|
|
36
|
+
# hook adds the end-intent gating layered on top):
|
|
37
|
+
# - role-not-admin / is-specialist / empty-stdin / missing-transcript:
|
|
38
|
+
# handled by the shared helper, emit `trigger-skipped` and exit 0.
|
|
39
|
+
# - no-intent-match: latest real-user message has no end-intent token,
|
|
40
|
+
# emit `trigger-skipped` and exit 0.
|
|
47
41
|
#
|
|
48
42
|
# Releases:
|
|
49
43
|
# - gate-released sessionId=<id> (when sentinel present in transcript)
|
|
50
44
|
#
|
|
51
45
|
# Blocks:
|
|
52
46
|
# - gate-blocked sessionId=<id> reason=sentinel-absent
|
|
53
|
-
# (exit 2; stderr carries the
|
|
47
|
+
# (exit 2; stderr carries the four-pass retrospective instruction —
|
|
48
|
+
# stderr is the Stop-hook contract for handing instructions back to
|
|
49
|
+
# the agent).
|
|
54
50
|
#
|
|
55
51
|
# Input: Claude Code's Stop hook stdin shape
|
|
56
52
|
# { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
|
|
57
53
|
|
|
58
54
|
set -uo pipefail
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# operator is never bricked by a missing port.
|
|
65
|
-
echo "[session-retrospective] trigger-skipped sessionId=unknown reason=missing-env env=MAXY_UI_INTERNAL_PORT" >&2
|
|
56
|
+
AGP_LOG_TAG="session-retrospective"
|
|
57
|
+
AGP_LIB="$(dirname "$0")/lib/admin-graph-pass-common.sh"
|
|
58
|
+
if [ ! -f "$AGP_LIB" ]; then
|
|
59
|
+
echo "[${AGP_LOG_TAG}] trigger-skipped sessionId=unknown reason=missing-helper path=${AGP_LIB}" >&2
|
|
66
60
|
exit 0
|
|
67
61
|
fi
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
# shellcheck source=lib/admin-graph-pass-common.sh
|
|
63
|
+
source "$AGP_LIB"
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
--data "$(python3 -c '
|
|
77
|
-
import sys, json
|
|
78
|
-
print(json.dumps({"tag": "session-retrospective", "level": "info", "line": sys.argv[1]}))
|
|
79
|
-
' "$line")" \
|
|
80
|
-
"$LOG_INGEST_URL" 2>/dev/null || true
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
INPUT=""
|
|
84
|
-
if [ -t 0 ]; then
|
|
85
|
-
INPUT=""
|
|
86
|
-
else
|
|
87
|
-
INPUT=$(cat)
|
|
88
|
-
fi
|
|
89
|
-
|
|
90
|
-
PARSED=$(printf '%s' "$INPUT" | python3 -c '
|
|
91
|
-
import sys, json
|
|
92
|
-
try:
|
|
93
|
-
d = json.load(sys.stdin)
|
|
94
|
-
sid = d.get("session_id", "") or ""
|
|
95
|
-
tpath = d.get("transcript_path", "") or ""
|
|
96
|
-
print(f"{sid}\t{tpath}")
|
|
97
|
-
except Exception:
|
|
98
|
-
print("\t")
|
|
99
|
-
' 2>/dev/null)
|
|
100
|
-
SESSION_ID="${PARSED%% *}"
|
|
101
|
-
TRANSCRIPT_PATH="${PARSED#* }"
|
|
102
|
-
SESSION_ID_REPORTED="${SESSION_ID:-unknown}"
|
|
103
|
-
|
|
104
|
-
if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
|
|
105
|
-
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=role-not-admin"
|
|
106
|
-
exit 0
|
|
107
|
-
fi
|
|
108
|
-
# Recursion guard. A specialist subagent dispatched via the Task tool
|
|
109
|
-
# (e.g. database-operator running the operator's graph writes) carries
|
|
110
|
-
# `MAXY_SPECIALIST=<name>` on the PTY env. Skip — the gate only enforces
|
|
111
|
-
# end-of-session for the operator's own admin session.
|
|
112
|
-
#
|
|
113
|
-
# Invariant the PTY spawner is expected to honour: `MAXY_SPECIALIST` is
|
|
114
|
-
# either set to a non-empty specialist name OR absent. An explicit empty
|
|
115
|
-
# string from a wrapper would slip the recursion guard here (`-n ""` is
|
|
116
|
-
# false) — that would be a defect at the spawn site, not here.
|
|
117
|
-
if [ -n "${MAXY_SPECIALIST:-}" ]; then
|
|
118
|
-
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=is-specialist specialist=${MAXY_SPECIALIST}"
|
|
119
|
-
exit 0
|
|
120
|
-
fi
|
|
121
|
-
if [ -z "$INPUT" ]; then
|
|
122
|
-
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=empty-stdin"
|
|
123
|
-
exit 0
|
|
124
|
-
fi
|
|
125
|
-
if [ -z "$SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
126
|
-
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=missing-transcript"
|
|
127
|
-
exit 0
|
|
128
|
-
fi
|
|
65
|
+
agp_setup_logging
|
|
66
|
+
agp_read_stdin
|
|
67
|
+
agp_parse_stdin
|
|
68
|
+
agp_guard_role_specialist
|
|
69
|
+
agp_guard_transcript require-stdin
|
|
129
70
|
|
|
130
71
|
# Single Python pass walks the transcript and reports two facts:
|
|
131
72
|
# 1. The latest end-intent token in the most recent real-user message,
|
|
@@ -243,18 +184,18 @@ INTENT_TOKEN="${INSPECTION%% *}"
|
|
|
243
184
|
SENTINEL_PRESENT="${INSPECTION#* }"
|
|
244
185
|
|
|
245
186
|
if [ -z "$INTENT_TOKEN" ]; then
|
|
246
|
-
|
|
187
|
+
agp_emit_log "trigger-skipped sessionId=${SESSION_ID} reason=no-intent-match"
|
|
247
188
|
exit 0
|
|
248
189
|
fi
|
|
249
190
|
|
|
250
|
-
|
|
191
|
+
agp_emit_log "trigger sessionId=${SESSION_ID} token=${INTENT_TOKEN}"
|
|
251
192
|
|
|
252
193
|
if [ "$SENTINEL_PRESENT" = "yes" ]; then
|
|
253
|
-
|
|
194
|
+
agp_emit_log "gate-released sessionId=${SESSION_ID}"
|
|
254
195
|
exit 0
|
|
255
196
|
fi
|
|
256
197
|
|
|
257
|
-
|
|
198
|
+
agp_emit_log "gate-blocked sessionId=${SESSION_ID} reason=sentinel-absent"
|
|
258
199
|
|
|
259
200
|
cat >&2 <<'INSTRUCTION'
|
|
260
201
|
The operator signalled session end. Before this session can close, run the four-pass retrospective inside this same admin session — do not spawn a new session, do not background anything. Use the tools you already have.
|
|
@@ -107,7 +107,9 @@ LOUD-FAIL output is `startup-self-test system-prompt-sentinels=fail missing=<tag
|
|
|
107
107
|
|
|
108
108
|
### Pre-publish boot smoke
|
|
109
109
|
|
|
110
|
-
`platform/scripts/smoke-boot-services.sh` runs inside `prepublishOnly` of the installer (`packages/create-maxy-code/`). For every service in its `SERVICES` list it builds a synthetic install dir
|
|
110
|
+
`platform/scripts/smoke-boot-services.sh` runs inside `prepublishOnly` of the installer (`packages/create-maxy-code/`). For every service in its `SERVICES` list it builds a synthetic install dir that mirrors what `seed-neo4j.sh` writes on first boot — real templates copied from `platform/templates/agents/admin/*.md` and `platform/templates/specialists/agents/*.md` into both `<accountDir>/` and `<platformRoot>/templates/`, real plugin `PLUGIN.md` manifests, plus a `.claude/` dir for `CLAUDE_CONFIG_DIR`. The fixture stamps the same env shape the installer's systemd unit writes (dummy `NEO4J_URI`/`NEO4J_PASSWORD` — the manager only checks presence at boot; the live cypher gate runs separately). Then it spawns `node dist/index.js`, waits up to 10 s for the `startup-self-test identity-drift=` line (the last startup self-test the manager logs), SIGTERMs, and fails publish if any `boot-failed reason=` or `^\[.*\] fatal ` line appears. The script asserts each of the three startup self-tests (`specialist-tool-drift=ok`, `system-prompt-sentinels=ok`, `identity-drift=ok`) is present — the real-templates fixture is what makes the assertions non-trivial; Task 438 added it after the 0.1.143 / 0.1.147 / 0.1.155 / 0.1.156 install regressions slipped past an empty fixture. The original Task 099 motivation still holds (module-load regressions tsc and vitest miss — e.g. a stray `require()` in an ESM-typed package).
|
|
111
|
+
|
|
112
|
+
Cypher schema gate (Task 438): the same script applies `platform/neo4j/schema.cypher` to the maintainer's local Neo4j via `cypher-shell`, using `NEO4J_URI` / `NEO4J_USER` / `NEO4J_PASSWORD` env (same shape `seed-neo4j.sh` reads — falls back to `platform/config/.neo4j-password` for the password). The dev's database is the test surface; schema commands use `IF NOT EXISTS` / `IF EXISTS` so re-apply is idempotent. Catches Neo4j 4 → 5 syntax drift (e.g. 0.1.151's `DROP FULLTEXT INDEX`) at publish time, not on a real install. Absence of `cypher-shell` on PATH or unset `NEO4J_URI` fails the gate loudly — the same toolchain the installer requires on every device.
|
|
111
113
|
|
|
112
114
|
Companion lint: `platform/scripts/check-no-esm-require.mjs` rejects `require(` calls in any `.ts/.tsx/.js` file inside a package with `"type": "module"`, also wired into `prepublishOnly`. Allowlist lives at the top of the script.
|
|
113
115
|
|
|
@@ -86,7 +86,7 @@ tools:
|
|
|
86
86
|
- name: profile-delete
|
|
87
87
|
publicAllowlist: false
|
|
88
88
|
adminAllowlist: true
|
|
89
|
-
- name: memory-
|
|
89
|
+
- name: memory-brain-capture-recent
|
|
90
90
|
publicAllowlist: false
|
|
91
91
|
adminAllowlist: true
|
|
92
92
|
- name: graph-prune-denylist-add
|
|
@@ -182,9 +182,9 @@ Graph hygiene is **agent-directed, case by case** — no autonomous rule engine,
|
|
|
182
182
|
|
|
183
183
|
`memory-dream-run` is the operator-initiated graph-hygiene sweep — four phases (orphan detection on `:Person`/`:Organization`/`:Concept`/`:Project`; stale compiled-truth scan on `compiledTruthUpdatedAt`; mark edges to `:Trashed` targets with `staleTarget: true`; LLM-bounded citation audit on `:TimelineEvent`). Each run writes a `:Report` (via `memory-report-write` from Task 332) summarising counts; per-phase log lines `[dream-cycle:phase-<n>]` join on the run id. Failure of any phase logs `[dream-cycle:phase-<n>:error]` and continues — subsequent phases still run. Pass `phases: [4]` to run only the citation audit (e.g. after a bulk ingest); `maxEventsPerRun` defaults to 200 to cap LLM cost, with a resume cursor stored as `:Report.lastEventCursor` so subsequent runs continue from the last processed event. `memory-review-queue` is the read-only surface for `:OrphanCandidate` and `:CitationProposal` queues — counts plus a small sample per queue. Phase 5 (backlinks) and phase 6 (tag consistency) are deferred to follow-up tasks. Task 327; absorbs Task 330.
|
|
184
184
|
|
|
185
|
-
## Brain-capture diagnostic — `memory-
|
|
185
|
+
## Brain-capture diagnostic — `memory-brain-capture-recent`
|
|
186
186
|
|
|
187
|
-
Read-only window into the extractive graph writes produced under this account. Returns recent `:MENTIONS` edges (`:Message-[:MENTIONS]->Entity`) and `:Idea` nodes interleaved by `observedAt`. Default window is 24 hours; pass `windowHours` (max 720) and `limit` (max 200) to scan a longer arc. Use this when the operator asks "what has the brain captured today?", "show me recent mentions of X", or "what ideas did I leave on the table this week?". Writes are produced by `database-operator` per-write briefs admin dispatches — either inline mid-turn, or as part of the once-per-operator-prompt pre-turn graph pass orchestrated by admin (`platform/plugins/admin/hooks/pre-turn-graph-pass.sh`, Tasks 425 + 430 — 430 reshaped the hook so admin walks the turn and dispatches per-write,
|
|
187
|
+
Read-only window into the extractive graph writes produced under this account. Returns recent `:MENTIONS` edges (`:Message-[:MENTIONS]->Entity`) and `:Idea` nodes interleaved by `observedAt`. Default window is 24 hours; pass `windowHours` (max 720) and `limit` (max 200) to scan a longer arc. Use this when the operator asks "what has the brain captured today?", "show me recent mentions of X", or "what ideas did I leave on the table this week?". Writes are produced by `database-operator` per-write briefs admin dispatches — either inline mid-turn, or as part of the once-per-operator-prompt pre-turn graph pass orchestrated by admin (`platform/plugins/admin/hooks/pre-turn-graph-pass.sh`, Tasks 425 + 430 + 439 — 430 reshaped the hook so admin walks the turn and dispatches per-write, 439 converged the hook's shared plumbing with `session-end-retrospective.sh`).
|
|
188
188
|
|
|
189
189
|
## Conversational Memory
|
|
190
190
|
|
|
@@ -32,7 +32,7 @@ import { memoryReadAttachment } from "./tools/memory-read-attachment.js";
|
|
|
32
32
|
import { memoryEditAttachment } from "./tools/memory-edit-attachment.js";
|
|
33
33
|
import { memoryRenameAttachment } from "./tools/memory-rename-attachment.js";
|
|
34
34
|
import { profileRead } from "./tools/profile-read.js";
|
|
35
|
-
import {
|
|
35
|
+
import { memoryBrainCaptureRecent } from "./tools/memory-brain-capture-recent.js";
|
|
36
36
|
import { profileUpdate } from "./tools/profile-update.js";
|
|
37
37
|
import { profileDelete } from "./tools/profile-delete.js";
|
|
38
38
|
import { graphPruneDenylistAdd } from "./tools/graph-prune-denylist-add.js";
|
|
@@ -1801,14 +1801,13 @@ if (!readOnly) {
|
|
|
1801
1801
|
};
|
|
1802
1802
|
}
|
|
1803
1803
|
});
|
|
1804
|
-
// memory-
|
|
1805
|
-
//
|
|
1806
|
-
// (:Message-[:MENTIONS]->Entity) and
|
|
1807
|
-
// r.createdByAgent='
|
|
1808
|
-
// answer "what has the brain captured today?"
|
|
1809
|
-
//
|
|
1810
|
-
|
|
1811
|
-
server.tool("memory-signals-recent", "Read the most recent signal-detector writes for this account: entity mentions extracted from operator messages, and original-thinking :Idea nodes. " +
|
|
1804
|
+
// memory-brain-capture-recent: read-only diagnostic for the per-turn
|
|
1805
|
+
// pre-turn-graph-pass writes that admin dispatches to `database-operator`.
|
|
1806
|
+
// Returns recent :MENTIONS edges (:Message-[:MENTIONS]->Entity) and
|
|
1807
|
+
// :Idea nodes filtered by r.createdByAgent='database-operator'. The
|
|
1808
|
+
// admin agent uses this to answer "what has the brain captured today?"
|
|
1809
|
+
// without touching cypher. Task 439.
|
|
1810
|
+
server.tool("memory-brain-capture-recent", "Read the most recent per-turn graph-pass writes for this account: entity mentions extracted from operator messages, and original-thinking :Idea nodes. " +
|
|
1812
1811
|
"Default window is the last 24 hours. " +
|
|
1813
1812
|
"Use this when the operator asks 'what has the brain captured today?', 'show me recent mentions of X', or 'what ideas did I leave on the table this week?'.", {
|
|
1814
1813
|
windowHours: z.number().int().optional().describe("Hours back from now. Default 24. Capped at 720 (30 days)."),
|
|
@@ -1816,8 +1815,8 @@ if (!readOnly) {
|
|
|
1816
1815
|
}, async ({ windowHours, limit }) => {
|
|
1817
1816
|
try {
|
|
1818
1817
|
if (!accountId)
|
|
1819
|
-
return refuseNoAccount("memory-
|
|
1820
|
-
const result = await
|
|
1818
|
+
return refuseNoAccount("memory-brain-capture-recent");
|
|
1819
|
+
const result = await memoryBrainCaptureRecent({ accountId, windowHours, limit });
|
|
1821
1820
|
return {
|
|
1822
1821
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1823
1822
|
};
|
|
@@ -1826,7 +1825,7 @@ if (!readOnly) {
|
|
|
1826
1825
|
return {
|
|
1827
1826
|
content: [{
|
|
1828
1827
|
type: "text",
|
|
1829
|
-
text: `memory-
|
|
1828
|
+
text: `memory-brain-capture-recent failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1830
1829
|
}],
|
|
1831
1830
|
isError: true,
|
|
1832
1831
|
};
|