@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.
Files changed (25) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/neo4j/schema.cypher +3 -2
  3. package/payload/platform/plugins/admin/hooks/lib/admin-graph-pass-common.sh +140 -0
  4. package/payload/platform/plugins/admin/hooks/pre-turn-graph-pass.sh +33 -83
  5. package/payload/platform/plugins/admin/hooks/session-end-retrospective.sh +32 -91
  6. package/payload/platform/plugins/docs/references/deployment.md +3 -1
  7. package/payload/platform/plugins/memory/PLUGIN.md +3 -3
  8. package/payload/platform/plugins/memory/mcp/dist/index.js +11 -12
  9. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  10. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts +35 -0
  11. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.d.ts.map +1 -0
  12. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js +80 -0
  13. package/payload/platform/plugins/memory/mcp/dist/tools/memory-brain-capture-recent.js.map +1 -0
  14. package/payload/platform/plugins/memory/references/schema-base.md +2 -2
  15. package/payload/platform/scripts/identity-forbidden-token-check.mjs +86 -0
  16. package/payload/platform/scripts/smoke-boot-services.sh +191 -41
  17. package/payload/platform/services/claude-session-manager/dist/index.js +18 -1
  18. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  19. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +10 -0
  20. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  21. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +91 -0
  22. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  23. package/payload/platform/templates/agents/admin/IDENTITY.md +14 -63
  24. package/payload/server/public/assets/{admin-kh_oCHeS.js → admin-3fLFD5h_.js} +23 -23
  25. package/payload/server/public/index.html +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.156",
3
+ "version": "0.1.158",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -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 signal-detector specialist from admin-side conversations.
1401
- // Written via memory-write(labels:["Idea"], ...) with one edge
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. The Task 425 hook
13
- # packaged the full conversation blob into the specialist's prompt; that
14
- # collapsed the data channel and the instruction channel into one
15
- # context, so operator prose pattern-matching as a writer-binding
16
- # instruction ("memory-update is denied this session") was treated as
17
- # binding and the pass self-denied. Same failure class as the writer-POV
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
- # Supersedes Task 412's Stop-hook registration. The Stop hook fired on
24
- # every assistant `end_turn`, including the agent's own ack after the
25
- # dispatch, producing an unbounded loop. Firing on `UserPromptSubmit`
26
- # fires once per operator turn and removes the loop primitive entirely.
27
- # The agent's response to the most recent operator turn is not covered
28
- # by a per-turn pass until the next operator turn; the
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. The directive carries the literal token
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
- UI_PORT="${MAXY_UI_INTERNAL_PORT:-}"
58
- if [ -z "$UI_PORT" ]; then
59
- echo "[pre-turn-graph-pass] trigger-skipped sessionId=unknown reason=missing-env env=MAXY_UI_INTERNAL_PORT" >&2
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
- UI_BASE="http://127.0.0.1:${UI_PORT}"
63
- LOG_INGEST_URL="${UI_BASE}/api/admin/log-ingest"
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
- emit_log() {
67
- local line="$1"
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
- PARSED=$(printf '%s' "$INPUT" | python3 -c '
84
- import sys, json
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
- emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=wrong-hook-event event=${HOOK_EVENT_NAME}"
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
- emit_log "dispatch sessionId=${SESSION_ID} conversationId=${CONVERSATION_ID:-<unset>} directive-shape=admin-walks priorWritesCount=${PRIOR_WRITES_COUNT} contextBytes=${CONTEXT_BYTES} ms=0"
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 three-pass retrospective (technical learnings, operator-relationship
4
- # updates, graph-completeness sweep) and called the deterministic
5
- # `session-retrospective-mark-complete` MCP sentinel tool. Task 282.
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
- # sentinel-grep doubles as the re-entry guard: every Stop that fires after
28
- # the agent's sentinel call sees the sentinel and exits 0 with
29
- # `gate-released`, so the agent's wrap-up turn never re-blocks.
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 (emits via `/api/admin/log-ingest`; hook stderr stays silent on
35
- # every skip path; the gate-blocked path is the only stderr emitter, by
36
- # Stop-hook contract — exit 2 + stderr is how the agent receives the
37
- # retrospective instruction block):
38
- # - MAXY_SESSION_ROLE must equal "admin" → reason=role-not-admin
39
- # - MAXY_SPECIALIST must NOT equal → reason=is-specialist
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 three-pass retrospective instruction)
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
- UI_PORT="${MAXY_UI_INTERNAL_PORT:-}"
61
- if [ -z "$UI_PORT" ]; then
62
- # No log-ingest reachable; emit to stderr instead of silently swallowing.
63
- # Skip the gate rather than block — fail-open on infra absence so the
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
- UI_BASE="http://127.0.0.1:${UI_PORT}"
69
- LOG_INGEST_URL="${UI_BASE}/api/admin/log-ingest"
62
+ # shellcheck source=lib/admin-graph-pass-common.sh
63
+ source "$AGP_LIB"
70
64
 
71
- emit_log() {
72
- local line="$1"
73
- curl -sS -o /dev/null -X POST \
74
- -H 'Content-Type: application/json' \
75
- --max-time 2 \
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
- emit_log "trigger-skipped sessionId=${SESSION_ID} reason=no-intent-match"
187
+ agp_emit_log "trigger-skipped sessionId=${SESSION_ID} reason=no-intent-match"
247
188
  exit 0
248
189
  fi
249
190
 
250
- emit_log "trigger sessionId=${SESSION_ID} token=${INTENT_TOKEN}"
191
+ agp_emit_log "trigger sessionId=${SESSION_ID} token=${INTENT_TOKEN}"
251
192
 
252
193
  if [ "$SENTINEL_PRESENT" = "yes" ]; then
253
- emit_log "gate-released sessionId=${SESSION_ID}"
194
+ agp_emit_log "gate-released sessionId=${SESSION_ID}"
254
195
  exit 0
255
196
  fi
256
197
 
257
- emit_log "gate-blocked sessionId=${SESSION_ID} reason=sentinel-absent"
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 (`brand.json`, empty `plugins/`, a single `account.json` with `agents/admin/{IDENTITY,SOUL}.md`), spawns `node dist/index.js`, waits up to 5 s for the canonical `[claude-session-manager] boot port=…` line, SIGTERMs, and fails publish if any `^\[.*\] fatal ` line appears in stdout/stderr. Catches the class of regressions tsc and vitest miss because they evaluate TS source rather than the compiled ESM module (e.g. a stray `require()` in an ESM-typed package: tsc allows it because `@types/node` declares `require` globally, and Vite's vitest loader shims it, but native Node ESM throws `ReferenceError: require is not defined` at boot).
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-signals-recent
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-signals-recent`
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, after the 425 shape was found to self-deny when operator prose pattern-matched as a writer instruction; 425 itself superseded the Task 412 Stop-hook variant, which superseded the original `signal-detector` specialist + throttled hook from Task 303).
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 { memorySignalsRecent } from "./tools/memory-signals-recent.js";
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-signals-recent: read-only diagnostic for the signal-detector
1805
- // specialist's writes. Returns recent :MENTIONS edges
1806
- // (:Message-[:MENTIONS]->Entity) and :Idea nodes filtered by
1807
- // r.createdByAgent='signal-detector'. The admin agent uses this to
1808
- // answer "what has the brain captured today?" without touching cypher.
1809
- // The signal-detector specialist itself is dispatched only by the
1810
- // admin-agent Stop hook never by operator intent. Task 303.
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-signals-recent");
1820
- const result = await memorySignalsRecent({ accountId, windowHours, limit });
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-signals-recent failed: ${err instanceof Error ? err.message : String(err)}`,
1828
+ text: `memory-brain-capture-recent failed: ${err instanceof Error ? err.message : String(err)}`,
1830
1829
  }],
1831
1830
  isError: true,
1832
1831
  };