@rubytech/create-maxy-code 0.1.103 → 0.1.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +5 -1
- package/payload/platform/plugins/admin/hooks/__tests__/session-end-retrospective.test.sh +396 -0
- package/payload/platform/plugins/admin/hooks/session-end-retrospective.sh +268 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +57 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/docs/references/admin-ui.md +7 -5
- package/payload/platform/plugins/docs/references/session-retrospective.md +20 -0
- package/payload/platform/scripts/seed-neo4j.sh +18 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +12 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +4 -0
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +43 -2
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +5 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts +9 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.js +12 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/permission-mode-tail.d.ts +20 -0
- package/payload/platform/services/claude-session-manager/dist/permission-mode-tail.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/permission-mode-tail.js +71 -0
- package/payload/platform/services/claude-session-manager/dist/permission-mode-tail.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +7 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +49 -2
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +11 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +10 -0
- package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +16 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/brand-design/SKILL.md +3 -2
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/build.md +10 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/registers.md +1 -1
- package/payload/server/public/assets/{ChatInput-DeBN_eCm.css → ChatInput-CJ50oqWt.css} +1 -1
- package/payload/server/public/assets/{Checkbox-Dd6lh79E.js → Checkbox-DGZG9BKc.js} +1 -1
- package/payload/server/public/assets/{admin-B_-v60En.js → admin-BvzMvMGo.js} +54 -54
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Nnicj9Oi.js → architectureDiagram-Q4EWVU46-Dw16BhiX.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-NuGXhtpA.js → blockDiagram-DXYQGD6D-DIOpmf5Y.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DusOs1B8.js → c4Diagram-AHTNJAMY-Tdb_HZeX.js} +1 -1
- package/payload/server/public/assets/channel-DThrH4QF.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-L4okIbRn.js → chunk-336JU56O-CebpwDDe.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-WhvqbTM0.js → chunk-426QAEUC-BtRCmfDU.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-Bby5RW_e.js → chunk-4TB4RGXK-BZ3GEWs3.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-veZxFPER.js → chunk-5FUZZQ4R-iDI6Xu0U.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-BwG8uJC1.js → chunk-5PVQY5BW-SQD_EpYa.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-DgQhTM1z.js → chunk-EDXVE4YY-CtCcA7_e.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-DpBKyLeu.js → chunk-ENJZ2VHE-pXVGVCbb.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-Bb8kEMah.js → chunk-ICPOFSXX-Dkzg9o2N.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-BUrpyQuA.js → chunk-OYMX7WX6-1ZZWzf9F.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-D0mOjNKD.js → chunk-U2HBQHQK-CpQ3kzO0.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-CQ7VnONI.js → chunk-X2U36JSP-C2LkxroC.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-BWgvd9aw.js → chunk-YZCP3GAM-Bl5jBOt5.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-UFB_t9gw.js → chunk-ZZ45TVLE-CHtnptPS.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-BC6oGTNX.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-COwC5Umh.js +1 -0
- package/payload/server/public/assets/clone-Chp7hvnA.js +1 -0
- package/payload/server/public/assets/{dagre-VrERS_-G.js → dagre-BziN0Nkh.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-w7RChnHk.js → dagre-KV5264BT-B7OG1g1Y.js} +1 -1
- package/payload/server/public/assets/data-BAgaPs4j.js +1 -0
- package/payload/server/public/assets/{diagram-5BDNPKRD-DDdrN3bf.js → diagram-5BDNPKRD-Bf31nIDs.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-B_lLNyeb.js → diagram-G4DWMVQ6-DQu85hhH.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-Da0x1Yf0.js → diagram-MMDJMWI5-5tstbs4y.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-DoFKKnVr.js → diagram-TYMM5635--MOV1U4o.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-DBPmAPTe.js → erDiagram-SMLLAGMA-BtAnOJmd.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CnkJzYU4.js → flowDiagram-DWJPFMVM-CybiCIih.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-BM9c9VN5.js → ganttDiagram-T4ZO3ILL-B44jN7Cy.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-C-1OqQDw.js → gitGraphDiagram-UUTBAWPF-CT7iYcwg.js} +1 -1
- package/payload/server/public/assets/graph-Cma7EArf.js +1 -0
- package/payload/server/public/assets/{graph-labels-BkDqelbQ.js → graph-labels-DyKk6Sxf.js} +1 -1
- package/payload/server/public/assets/{graphlib-Be3_mRCh.js → graphlib-DP4o0pYL.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-o4FB6Zjb.js → infoDiagram-42DDH7IO-CvxFpFHN.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-BlnRfx6Z.js → ishikawaDiagram-UXIWVN3A-CUwWLPeR.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-Da26YPBj.js → journeyDiagram-VCZTEJTY-y-OnGgLi.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-C1jVbsV0.js → kanban-definition-6JOO6SKY-BhrSe8R4.js} +1 -1
- package/payload/server/public/assets/{lib-CKcXekKF.js → lib-CpkYtEDz.js} +5 -5
- package/payload/server/public/assets/{line-BS0h3YlR.js → line-C9uS7z4J.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-4FY7A26h.js → mermaid-parser.core-Cg4ZdKp-.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-CD1JRYmG.js → mermaid.core-BHdKOsex.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-BmCJ5q81.js → mindmap-definition-QFDTVHPH-NBYiXHo7.js} +1 -1
- package/payload/server/public/assets/{page-D6QVxITy.js → page-B4IWl3aZ.js} +1 -1
- package/payload/server/public/assets/{page-1atlhFFT.js → page-D9YpwhIu.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-ClXRAXST.js → pieDiagram-DEJITSTG-CbojC64C.js} +1 -1
- package/payload/server/public/assets/{public-7fuvQJwF.js → public-BtOXjy3A.js} +5 -5
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-D17aBgPP.js → quadrantDiagram-34T5L4WZ-CnnoZXcL.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-CUpkUJlh.js → requirementDiagram-MS252O5E-DFRFRtyJ.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DnrrGp2c.js → sankeyDiagram-XADWPNL6-DU5gcpzO.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-CMprmJg9.js → sequenceDiagram-FGHM5R23-BN7HZ6Hq.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-BdpBUtXc.js → stateDiagram-FHFEXIEX-DG6cCjvg.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CvVd1Q_q.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-u7JucCMs.js → timeline-definition-GMOUNBTQ-qmV-8SEV.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-CP1Q9zf2.js → vennDiagram-DHZGUBPP-BMXQ1s3n.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BOVxInz5.js → wardleyDiagram-NUSXRM2D-CoAdP1Gw.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-Dcx1f04K.js → xychartDiagram-5P7HB3ND-CZlfGTEn.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/public/assets/channel-sqt9QaiU.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-rYq1YLUo.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Cc4tKtqG.js +0 -1
- package/payload/server/public/assets/clone-SdNqIUjS.js +0 -1
- package/payload/server/public/assets/data-DQafmlMy.js +0 -1
- package/payload/server/public/assets/graph-CTM-V4lS.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DPekwBDP.js +0 -1
- /package/payload/server/public/assets/{ChatInput-DBdyKbiU.js → ChatInput-DJsqm_Gf.js} +0 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
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.
|
|
6
|
+
#
|
|
7
|
+
# Trigger detection. End-intent is the operator's most recent real-user
|
|
8
|
+
# message matching one of the literal token strings:
|
|
9
|
+
# /end | /archive | end session | archive this session
|
|
10
|
+
# "Real user" follows the same shape used by
|
|
11
|
+
# askuserquestion-investigate-gate.sh — `type=user, message.role=user,
|
|
12
|
+
# content` is a string OR a list with at least one non-`tool_result` block.
|
|
13
|
+
# `tool_result`-only user records are tool replies, not turn starts, and
|
|
14
|
+
# never count as the latest user message.
|
|
15
|
+
#
|
|
16
|
+
# The Sidebar Archive button POSTs `/:sessionId/archive` which kills the
|
|
17
|
+
# PTY directly — no Stop fires after that, so clicking Archive is the
|
|
18
|
+
# operator's de-facto skip path. The spec lists opt-out as a deferred
|
|
19
|
+
# follow-up; archive-click is its current shape and acceptable behaviour.
|
|
20
|
+
#
|
|
21
|
+
# Sentinel detection. Completion is recognised by greping the operator's
|
|
22
|
+
# own JSONL (transcript_path on the Stop envelope) for a `tool_use` block
|
|
23
|
+
# whose `name` is exactly `session-retrospective-mark-complete`. The
|
|
24
|
+
# sentinel-grep doubles as the re-entry guard: every Stop that fires after
|
|
25
|
+
# the agent's sentinel call sees the sentinel and exits 0 with
|
|
26
|
+
# `gate-released`, so the agent's wrap-up turn never re-blocks.
|
|
27
|
+
# [[feedback_no_stdout_parsing_for_control_flow]],
|
|
28
|
+
# [[feedback_doctrine_paragraph_is_not_a_gate]] — the LLM is removed from
|
|
29
|
+
# the per-decision path entirely.
|
|
30
|
+
#
|
|
31
|
+
# Gating (emits via `/api/admin/log-ingest`; hook stderr stays silent on
|
|
32
|
+
# every skip path; the gate-blocked path is the only stderr emitter, by
|
|
33
|
+
# Stop-hook contract — exit 2 + stderr is how the agent receives the
|
|
34
|
+
# retrospective instruction block):
|
|
35
|
+
# - MAXY_SESSION_ROLE must equal "admin" → reason=role-not-admin
|
|
36
|
+
# - MAXY_SPECIALIST must NOT equal → reason=is-specialist
|
|
37
|
+
# "database-operator" (and any other (recursion guard — a
|
|
38
|
+
# specialist value) specialist subagent stop
|
|
39
|
+
# must not re-enter this gate)
|
|
40
|
+
# - Stop-hook stdin must be non-empty → reason=empty-stdin
|
|
41
|
+
# - transcript_path must exist on disk → reason=missing-transcript
|
|
42
|
+
# - latest real-user message must match an → reason=no-intent-match
|
|
43
|
+
# end-intent token
|
|
44
|
+
#
|
|
45
|
+
# Releases:
|
|
46
|
+
# - gate-released sessionId=<id> (when sentinel present in transcript)
|
|
47
|
+
#
|
|
48
|
+
# Blocks:
|
|
49
|
+
# - gate-blocked sessionId=<id> reason=sentinel-absent
|
|
50
|
+
# (exit 2; stderr carries the three-pass retrospective instruction)
|
|
51
|
+
#
|
|
52
|
+
# Input: Claude Code's Stop hook stdin shape
|
|
53
|
+
# { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
|
|
54
|
+
|
|
55
|
+
set -uo pipefail
|
|
56
|
+
|
|
57
|
+
UI_PORT="${MAXY_UI_INTERNAL_PORT:-}"
|
|
58
|
+
if [ -z "$UI_PORT" ]; then
|
|
59
|
+
# No log-ingest reachable; emit to stderr instead of silently swallowing.
|
|
60
|
+
# Skip the gate rather than block — fail-open on infra absence so the
|
|
61
|
+
# operator is never bricked by a missing port.
|
|
62
|
+
echo "[session-retrospective] trigger-skipped sessionId=unknown reason=missing-env env=MAXY_UI_INTERNAL_PORT" >&2
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
UI_BASE="http://127.0.0.1:${UI_PORT}"
|
|
66
|
+
LOG_INGEST_URL="${UI_BASE}/api/admin/log-ingest"
|
|
67
|
+
|
|
68
|
+
emit_log() {
|
|
69
|
+
local line="$1"
|
|
70
|
+
curl -sS -o /dev/null -X POST \
|
|
71
|
+
-H 'Content-Type: application/json' \
|
|
72
|
+
--max-time 2 \
|
|
73
|
+
--data "$(python3 -c '
|
|
74
|
+
import sys, json
|
|
75
|
+
print(json.dumps({"tag": "session-retrospective", "level": "info", "line": sys.argv[1]}))
|
|
76
|
+
' "$line")" \
|
|
77
|
+
"$LOG_INGEST_URL" 2>/dev/null || true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
INPUT=""
|
|
81
|
+
if [ -t 0 ]; then
|
|
82
|
+
INPUT=""
|
|
83
|
+
else
|
|
84
|
+
INPUT=$(cat)
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
PARSED=$(printf '%s' "$INPUT" | python3 -c '
|
|
88
|
+
import sys, json
|
|
89
|
+
try:
|
|
90
|
+
d = json.load(sys.stdin)
|
|
91
|
+
sid = d.get("session_id", "") or ""
|
|
92
|
+
tpath = d.get("transcript_path", "") or ""
|
|
93
|
+
print(f"{sid}\t{tpath}")
|
|
94
|
+
except Exception:
|
|
95
|
+
print("\t")
|
|
96
|
+
' 2>/dev/null)
|
|
97
|
+
SESSION_ID="${PARSED%% *}"
|
|
98
|
+
TRANSCRIPT_PATH="${PARSED#* }"
|
|
99
|
+
SESSION_ID_REPORTED="${SESSION_ID:-unknown}"
|
|
100
|
+
|
|
101
|
+
if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
|
|
102
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=role-not-admin"
|
|
103
|
+
exit 0
|
|
104
|
+
fi
|
|
105
|
+
# Recursion guard. A specialist subagent dispatched via the Task tool
|
|
106
|
+
# (e.g. database-operator running the operator's graph writes) carries
|
|
107
|
+
# `MAXY_SPECIALIST=<name>` on the PTY env. Skip — the gate only enforces
|
|
108
|
+
# end-of-session for the operator's own admin session.
|
|
109
|
+
#
|
|
110
|
+
# Invariant the PTY spawner is expected to honour: `MAXY_SPECIALIST` is
|
|
111
|
+
# either set to a non-empty specialist name OR absent. An explicit empty
|
|
112
|
+
# string from a wrapper would slip the recursion guard here (`-n ""` is
|
|
113
|
+
# false) — that would be a defect at the spawn site, not here.
|
|
114
|
+
if [ -n "${MAXY_SPECIALIST:-}" ]; then
|
|
115
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=is-specialist specialist=${MAXY_SPECIALIST}"
|
|
116
|
+
exit 0
|
|
117
|
+
fi
|
|
118
|
+
if [ -z "$INPUT" ]; then
|
|
119
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=empty-stdin"
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
if [ -z "$SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
123
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=missing-transcript"
|
|
124
|
+
exit 0
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# Single Python pass walks the transcript and reports two facts:
|
|
128
|
+
# 1. The latest end-intent token in the most recent real-user message,
|
|
129
|
+
# or empty if no real user message matches.
|
|
130
|
+
# 2. Whether any `tool_use` block in the transcript names the sentinel
|
|
131
|
+
# tool `session-retrospective-mark-complete`.
|
|
132
|
+
# Output one line: <intent-token-or-empty>\t<sentinel-present-yes|no>
|
|
133
|
+
INSPECTION=$(TRANSCRIPT_PATH="$TRANSCRIPT_PATH" python3 - <<'PY'
|
|
134
|
+
import os, json
|
|
135
|
+
|
|
136
|
+
SENTINEL_TOOL = "session-retrospective-mark-complete"
|
|
137
|
+
INTENT_TOKENS = ("/end", "/archive", "end session", "archive this session")
|
|
138
|
+
|
|
139
|
+
def is_real_user(rec):
|
|
140
|
+
if not isinstance(rec, dict) or rec.get("type") != "user":
|
|
141
|
+
return False
|
|
142
|
+
msg = rec.get("message")
|
|
143
|
+
if not isinstance(msg, dict) or msg.get("role") != "user":
|
|
144
|
+
return False
|
|
145
|
+
content = msg.get("content")
|
|
146
|
+
if isinstance(content, str):
|
|
147
|
+
return True
|
|
148
|
+
if isinstance(content, list):
|
|
149
|
+
for b in content:
|
|
150
|
+
if isinstance(b, dict) and b.get("type") != "tool_result":
|
|
151
|
+
return True
|
|
152
|
+
return False
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
def extract_user_text(rec):
|
|
156
|
+
msg = rec.get("message", {}) or {}
|
|
157
|
+
content = msg.get("content")
|
|
158
|
+
if isinstance(content, str):
|
|
159
|
+
return content
|
|
160
|
+
if isinstance(content, list):
|
|
161
|
+
out = []
|
|
162
|
+
for b in content:
|
|
163
|
+
if not isinstance(b, dict):
|
|
164
|
+
continue
|
|
165
|
+
if b.get("type") == "text":
|
|
166
|
+
t = b.get("text")
|
|
167
|
+
if isinstance(t, str):
|
|
168
|
+
out.append(t)
|
|
169
|
+
return "".join(out)
|
|
170
|
+
return ""
|
|
171
|
+
|
|
172
|
+
def assistant_tool_use_names(rec):
|
|
173
|
+
if not isinstance(rec, dict) or rec.get("type") != "assistant":
|
|
174
|
+
return []
|
|
175
|
+
msg = rec.get("message", {}) or {}
|
|
176
|
+
if msg.get("role") != "assistant":
|
|
177
|
+
return []
|
|
178
|
+
content = msg.get("content")
|
|
179
|
+
if not isinstance(content, list):
|
|
180
|
+
return []
|
|
181
|
+
names = []
|
|
182
|
+
for b in content:
|
|
183
|
+
if isinstance(b, dict) and b.get("type") == "tool_use":
|
|
184
|
+
n = b.get("name")
|
|
185
|
+
if isinstance(n, str):
|
|
186
|
+
names.append(n)
|
|
187
|
+
return names
|
|
188
|
+
|
|
189
|
+
path = os.environ["TRANSCRIPT_PATH"]
|
|
190
|
+
records = []
|
|
191
|
+
try:
|
|
192
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
193
|
+
for raw in f:
|
|
194
|
+
raw = raw.strip()
|
|
195
|
+
if not raw:
|
|
196
|
+
continue
|
|
197
|
+
try:
|
|
198
|
+
records.append(json.loads(raw))
|
|
199
|
+
except Exception:
|
|
200
|
+
continue
|
|
201
|
+
except Exception:
|
|
202
|
+
print("\tno")
|
|
203
|
+
raise SystemExit(0)
|
|
204
|
+
|
|
205
|
+
# Sentinel presence — any assistant tool_use of the sentinel tool anywhere
|
|
206
|
+
# in this session's JSONL.
|
|
207
|
+
sentinel_present = False
|
|
208
|
+
for r in records:
|
|
209
|
+
if SENTINEL_TOOL in assistant_tool_use_names(r):
|
|
210
|
+
sentinel_present = True
|
|
211
|
+
break
|
|
212
|
+
|
|
213
|
+
# Latest real-user message text.
|
|
214
|
+
latest_user_text = ""
|
|
215
|
+
for r in reversed(records):
|
|
216
|
+
if is_real_user(r):
|
|
217
|
+
latest_user_text = extract_user_text(r).strip()
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
matched = ""
|
|
221
|
+
if latest_user_text:
|
|
222
|
+
# Case-insensitive whole-message match against the token set. A token
|
|
223
|
+
# must appear as the entire user message (after .strip()), or as a
|
|
224
|
+
# standalone line within it — not embedded inside a paragraph. Keeps
|
|
225
|
+
# the gate from false-positiving on "I'll end session by 5pm" prose.
|
|
226
|
+
haystacks = {latest_user_text.lower()}
|
|
227
|
+
for line in latest_user_text.splitlines():
|
|
228
|
+
s = line.strip().lower()
|
|
229
|
+
if s:
|
|
230
|
+
haystacks.add(s)
|
|
231
|
+
for tok in INTENT_TOKENS:
|
|
232
|
+
if tok.lower() in haystacks:
|
|
233
|
+
matched = tok
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
print(f"{matched}\t{'yes' if sentinel_present else 'no'}")
|
|
237
|
+
PY
|
|
238
|
+
)
|
|
239
|
+
INTENT_TOKEN="${INSPECTION%% *}"
|
|
240
|
+
SENTINEL_PRESENT="${INSPECTION#* }"
|
|
241
|
+
|
|
242
|
+
if [ -z "$INTENT_TOKEN" ]; then
|
|
243
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID} reason=no-intent-match"
|
|
244
|
+
exit 0
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
emit_log "trigger sessionId=${SESSION_ID} token=${INTENT_TOKEN}"
|
|
248
|
+
|
|
249
|
+
if [ "$SENTINEL_PRESENT" = "yes" ]; then
|
|
250
|
+
emit_log "gate-released sessionId=${SESSION_ID}"
|
|
251
|
+
exit 0
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
emit_log "gate-blocked sessionId=${SESSION_ID} reason=sentinel-absent"
|
|
255
|
+
|
|
256
|
+
cat >&2 <<'INSTRUCTION'
|
|
257
|
+
The operator signalled session end. Before this session can close, run the three-pass retrospective inside this same admin session — do not spawn a new session, do not background anything. Use the tools you already have.
|
|
258
|
+
|
|
259
|
+
Pass 1 — Technical learnings. Walk this session for operator corrections and self-detected mistakes. For each one, delegate one graph write to `database-operator` via the Task tool, persisting a Learning or Correction node per the schema. Count what you write.
|
|
260
|
+
|
|
261
|
+
Pass 2 — Operator-relationship updates. Walk this session for tonal signals, rejected phrasings, and working-style preferences. For each one worth carrying forward, Write the observation into `SOUL.md` at the account-scoped path. Count what you write.
|
|
262
|
+
|
|
263
|
+
Pass 3 — Graph completeness sweep. Walk this session for any node, edge, or commitment that was discussed but not written to the graph in-flight. For each missing write, delegate to `database-operator` via the Task tool. Count what you write.
|
|
264
|
+
|
|
265
|
+
Then call `session-retrospective-mark-complete` with the three counts and reply with one short paragraph in your house voice summarising what you wrote.
|
|
266
|
+
INSTRUCTION
|
|
267
|
+
|
|
268
|
+
exit 2
|
|
@@ -3005,6 +3005,63 @@ eagerTool(server, "action-edit", "Edit a pending action's input and then execute
|
|
|
3005
3005
|
};
|
|
3006
3006
|
}
|
|
3007
3007
|
});
|
|
3008
|
+
// ===================================================================
|
|
3009
|
+
// Session-end retrospective sentinel (Task 282)
|
|
3010
|
+
// ===================================================================
|
|
3011
|
+
//
|
|
3012
|
+
// Deterministic completion signal for the session-end retrospective gate
|
|
3013
|
+
// hook (`platform/plugins/admin/hooks/session-end-retrospective.sh`). The
|
|
3014
|
+
// hook walks the operator's JSONL for any `tool_use` of THIS tool's exact
|
|
3015
|
+
// name and, when present, releases the Stop gate. Prose retrospectives
|
|
3016
|
+
// without this call leave the gate blocked
|
|
3017
|
+
// ([[feedback_doctrine_paragraph_is_not_a_gate]]).
|
|
3018
|
+
//
|
|
3019
|
+
// The tool's only side-effect is one structured log line through
|
|
3020
|
+
// /api/admin/log-ingest so server.log carries the full chain
|
|
3021
|
+
// trigger → gate-blocked → complete → gate-released
|
|
3022
|
+
// against one operator sessionId. The release decision itself is the
|
|
3023
|
+
// JSONL grep — never the log line.
|
|
3024
|
+
server.tool("session-retrospective-mark-complete", "Marks the end-of-session retrospective complete. Call this AFTER walking the session for technical learnings, operator-relationship updates, and missing graph writes and persisting each one (Task-tool delegations to database-operator for graph writes, Write to SOUL.md for tonal updates). The counts you pass are the number of items you actually persisted in each pass. The session-end Stop hook gate releases on the presence of this tool_use in the transcript — calling it is the deterministic signal that the retrospective ran.", {
|
|
3025
|
+
learningsCount: z.number().int().nonnegative().describe("Technical-learning / correction nodes you delegated to database-operator in this retrospective."),
|
|
3026
|
+
preferencesCount: z.number().int().nonnegative().describe("SOUL.md writes you made for tonal / working-style observations in this retrospective."),
|
|
3027
|
+
graphWriteCount: z.number().int().nonnegative().describe("Graph nodes/edges/commitments you delegated to database-operator for content that surfaced in the session but was not written in-flight."),
|
|
3028
|
+
}, async ({ learningsCount, preferencesCount, graphWriteCount }) => {
|
|
3029
|
+
const sessionId = process.env.SESSION_ID || "unknown";
|
|
3030
|
+
const line = `complete sessionId=${sessionId} learningsCount=${learningsCount} preferencesCount=${preferencesCount} graphWriteCount=${graphWriteCount}`;
|
|
3031
|
+
if (!PLATFORM_PORT) {
|
|
3032
|
+
// Module-init deliberately tolerates a missing PLATFORM_PORT
|
|
3033
|
+
// (lines ~62-71) so the MCP server does not loud-fail on plugin-
|
|
3034
|
+
// system spawn enumeration. The hook's release decision is the
|
|
3035
|
+
// JSONL tool_use entry, not this POST — so we still return success
|
|
3036
|
+
// to the caller and emit the line on stderr so it lands in the
|
|
3037
|
+
// MCP server's stderr log instead of disappearing into a NaN URL.
|
|
3038
|
+
console.error(`[session-retrospective] log-ingest-skipped sessionId=${sessionId} reason=missing-env env=PLATFORM_PORT line=${JSON.stringify(line)}`);
|
|
3039
|
+
}
|
|
3040
|
+
else {
|
|
3041
|
+
try {
|
|
3042
|
+
const port = parseInt(PLATFORM_PORT, 10);
|
|
3043
|
+
await fetch(`http://127.0.0.1:${port}/api/admin/log-ingest`, {
|
|
3044
|
+
method: "POST",
|
|
3045
|
+
headers: { "Content-Type": "application/json" },
|
|
3046
|
+
body: JSON.stringify({ tag: "session-retrospective", level: "info", line }),
|
|
3047
|
+
signal: AbortSignal.timeout(2000),
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
catch (err) {
|
|
3051
|
+
// Falling back to stderr keeps the event in the MCP server's
|
|
3052
|
+
// stderr log even when the loopback POST fails. The hook's
|
|
3053
|
+
// release decision is JSONL-based and does not depend on this
|
|
3054
|
+
// POST succeeding.
|
|
3055
|
+
console.error(`[session-retrospective] log-ingest-failed sessionId=${sessionId} err=${err instanceof Error ? err.message : String(err)}`);
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
return {
|
|
3059
|
+
content: [{
|
|
3060
|
+
type: "text",
|
|
3061
|
+
text: `Retrospective marked complete (learnings=${learningsCount} preferences=${preferencesCount} graphWrites=${graphWriteCount}). Session is now free to end.`,
|
|
3062
|
+
}],
|
|
3063
|
+
};
|
|
3064
|
+
});
|
|
3008
3065
|
// Cleanup on exit (SIGTERM for systemd service stops on Pi)
|
|
3009
3066
|
const cleanup = async () => {
|
|
3010
3067
|
await closeDriver();
|