@rubytech/create-maxy-code 0.1.102 → 0.1.104
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/pre-tool-use.sh +1 -1
- package/payload/platform/plugins/admin/hooks/session-end-retrospective.sh +268 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +69 -8
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/file-presentation/SKILL.md +14 -4
- package/payload/platform/plugins/admin/skills/unzip-attachment/SKILL.md +1 -1
- package/payload/platform/plugins/docs/PLUGIN.md +2 -0
- package/payload/platform/plugins/docs/references/admin-ui.md +235 -0
- package/payload/platform/plugins/docs/references/session-retrospective.md +20 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +2 -2
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- 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/platform/templates/specialists/agents/content-producer.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/make-brochure/SKILL.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/index.html +8 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/template.html +8 -0
- 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
|
|
@@ -1781,8 +1781,9 @@ eagerTool(server, "skill-load", "Load a plugin skill's SKILL.md body by skill na
|
|
|
1781
1781
|
// primitives the legacy tool wrapped; calling it logged a "rendered"
|
|
1782
1782
|
// sentinel that read like a successful render to the agent while the
|
|
1783
1783
|
// chat surface saw nothing. Admin flows now use plain markdown for
|
|
1784
|
-
// reviewable artefacts
|
|
1785
|
-
// `file-attach`
|
|
1784
|
+
// reviewable artefacts and the `file-presentation` skill for downloadable
|
|
1785
|
+
// artefacts (the skill picks `SendUserFile` or `file-attach` by inventory).
|
|
1786
|
+
// The :Component / :KnowledgeDocument
|
|
1786
1787
|
// persistence pipeline is preserved for historical conversations that
|
|
1787
1788
|
// already carry the rows; no new conversations write to it.
|
|
1788
1789
|
eagerTool(server, "session-reset", "Reset the current session. Compacts conversation history to memory, clears the visible conversation, " +
|
|
@@ -2480,12 +2481,15 @@ server.tool("premium-deliver", "Deliver a purchased premium plugin. Copies plugi
|
|
|
2480
2481
|
// ---------------------------------------------------------------------------
|
|
2481
2482
|
// file-attach — copy a generated file into the attachment store for download
|
|
2482
2483
|
// ---------------------------------------------------------------------------
|
|
2483
|
-
eagerTool(server, "file-attach", "Attach a file from the account directory for delivery to the user
|
|
2484
|
-
"
|
|
2485
|
-
"
|
|
2486
|
-
"
|
|
2487
|
-
"
|
|
2488
|
-
"and
|
|
2484
|
+
eagerTool(server, "file-attach", "Attach a file from the account directory for delivery to the user on non-PTY chat " +
|
|
2485
|
+
"surfaces (web chat, WhatsApp) where the platform UI mediates attachment rendering. " +
|
|
2486
|
+
"On the native Claude Code PTY surface, `SendUserFile` is the delivery — `file-attach` " +
|
|
2487
|
+
"returns an `attachmentId` envelope only and renders no chip there. Tool selection is " +
|
|
2488
|
+
"by inventory: see the `file-presentation` skill. Copies the file into the attachment " +
|
|
2489
|
+
"store and returns metadata the chat surface uses to present the download. The file " +
|
|
2490
|
+
"must be within the account directory tree. The returned metadata is the operator-visible " +
|
|
2491
|
+
"record of the attachment; quote it back to the user in plain text so they have the " +
|
|
2492
|
+
"filename and size on screen.", {
|
|
2489
2493
|
filePath: z.string().describe("Absolute path to the file to attach"),
|
|
2490
2494
|
}, async ({ filePath }) => {
|
|
2491
2495
|
if (!ACCOUNT_ID)
|
|
@@ -3001,6 +3005,63 @@ eagerTool(server, "action-edit", "Edit a pending action's input and then execute
|
|
|
3001
3005
|
};
|
|
3002
3006
|
}
|
|
3003
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
|
+
});
|
|
3004
3065
|
// Cleanup on exit (SIGTERM for systemd service stops on Pi)
|
|
3005
3066
|
const cleanup = async () => {
|
|
3006
3067
|
await closeDriver();
|