@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.
Files changed (112) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  3. package/payload/platform/plugins/admin/PLUGIN.md +5 -1
  4. package/payload/platform/plugins/admin/hooks/__tests__/session-end-retrospective.test.sh +396 -0
  5. package/payload/platform/plugins/admin/hooks/session-end-retrospective.sh +268 -0
  6. package/payload/platform/plugins/admin/mcp/dist/index.js +57 -0
  7. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  8. package/payload/platform/plugins/docs/references/admin-ui.md +7 -5
  9. package/payload/platform/plugins/docs/references/session-retrospective.md +20 -0
  10. package/payload/platform/scripts/seed-neo4j.sh +18 -1
  11. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +12 -1
  12. package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -1
  13. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +4 -0
  14. package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -1
  15. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  16. package/payload/platform/services/claude-session-manager/dist/http-server.js +43 -2
  17. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  18. package/payload/platform/services/claude-session-manager/dist/index.js +5 -0
  19. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  20. package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts +9 -0
  21. package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts.map +1 -1
  22. package/payload/platform/services/claude-session-manager/dist/jsonl-path.js +12 -0
  23. package/payload/platform/services/claude-session-manager/dist/jsonl-path.js.map +1 -1
  24. package/payload/platform/services/claude-session-manager/dist/permission-mode-tail.d.ts +20 -0
  25. package/payload/platform/services/claude-session-manager/dist/permission-mode-tail.d.ts.map +1 -0
  26. package/payload/platform/services/claude-session-manager/dist/permission-mode-tail.js +71 -0
  27. package/payload/platform/services/claude-session-manager/dist/permission-mode-tail.js.map +1 -0
  28. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +7 -0
  29. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  30. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +49 -2
  31. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  32. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts +11 -1
  33. package/payload/platform/services/claude-session-manager/dist/session-sidecar.d.ts.map +1 -1
  34. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js +10 -0
  35. package/payload/platform/services/claude-session-manager/dist/session-sidecar.js.map +1 -1
  36. package/payload/platform/templates/agents/admin/IDENTITY.md +16 -0
  37. package/payload/premium-plugins/real-agent/plugins/brochures/skills/brand-design/SKILL.md +3 -2
  38. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +1 -1
  39. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/build.md +10 -0
  40. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/references/registers.md +1 -1
  41. package/payload/server/public/assets/{ChatInput-DeBN_eCm.css → ChatInput-CJ50oqWt.css} +1 -1
  42. package/payload/server/public/assets/{Checkbox-Dd6lh79E.js → Checkbox-DGZG9BKc.js} +1 -1
  43. package/payload/server/public/assets/{admin-B_-v60En.js → admin-BvzMvMGo.js} +54 -54
  44. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-Nnicj9Oi.js → architectureDiagram-Q4EWVU46-Dw16BhiX.js} +1 -1
  45. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-NuGXhtpA.js → blockDiagram-DXYQGD6D-DIOpmf5Y.js} +1 -1
  46. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-DusOs1B8.js → c4Diagram-AHTNJAMY-Tdb_HZeX.js} +1 -1
  47. package/payload/server/public/assets/channel-DThrH4QF.js +1 -0
  48. package/payload/server/public/assets/{chunk-336JU56O-L4okIbRn.js → chunk-336JU56O-CebpwDDe.js} +2 -2
  49. package/payload/server/public/assets/{chunk-426QAEUC-WhvqbTM0.js → chunk-426QAEUC-BtRCmfDU.js} +1 -1
  50. package/payload/server/public/assets/{chunk-4TB4RGXK-Bby5RW_e.js → chunk-4TB4RGXK-BZ3GEWs3.js} +1 -1
  51. package/payload/server/public/assets/{chunk-5FUZZQ4R-veZxFPER.js → chunk-5FUZZQ4R-iDI6Xu0U.js} +1 -1
  52. package/payload/server/public/assets/{chunk-5PVQY5BW-BwG8uJC1.js → chunk-5PVQY5BW-SQD_EpYa.js} +1 -1
  53. package/payload/server/public/assets/{chunk-EDXVE4YY-DgQhTM1z.js → chunk-EDXVE4YY-CtCcA7_e.js} +1 -1
  54. package/payload/server/public/assets/{chunk-ENJZ2VHE-DpBKyLeu.js → chunk-ENJZ2VHE-pXVGVCbb.js} +1 -1
  55. package/payload/server/public/assets/{chunk-ICPOFSXX-Bb8kEMah.js → chunk-ICPOFSXX-Dkzg9o2N.js} +1 -1
  56. package/payload/server/public/assets/{chunk-OYMX7WX6-BUrpyQuA.js → chunk-OYMX7WX6-1ZZWzf9F.js} +1 -1
  57. package/payload/server/public/assets/{chunk-U2HBQHQK-D0mOjNKD.js → chunk-U2HBQHQK-CpQ3kzO0.js} +1 -1
  58. package/payload/server/public/assets/{chunk-X2U36JSP-CQ7VnONI.js → chunk-X2U36JSP-C2LkxroC.js} +1 -1
  59. package/payload/server/public/assets/{chunk-YZCP3GAM-BWgvd9aw.js → chunk-YZCP3GAM-Bl5jBOt5.js} +1 -1
  60. package/payload/server/public/assets/{chunk-ZZ45TVLE-UFB_t9gw.js → chunk-ZZ45TVLE-CHtnptPS.js} +1 -1
  61. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BC6oGTNX.js +1 -0
  62. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-COwC5Umh.js +1 -0
  63. package/payload/server/public/assets/clone-Chp7hvnA.js +1 -0
  64. package/payload/server/public/assets/{dagre-VrERS_-G.js → dagre-BziN0Nkh.js} +1 -1
  65. package/payload/server/public/assets/{dagre-KV5264BT-w7RChnHk.js → dagre-KV5264BT-B7OG1g1Y.js} +1 -1
  66. package/payload/server/public/assets/data-BAgaPs4j.js +1 -0
  67. package/payload/server/public/assets/{diagram-5BDNPKRD-DDdrN3bf.js → diagram-5BDNPKRD-Bf31nIDs.js} +1 -1
  68. package/payload/server/public/assets/{diagram-G4DWMVQ6-B_lLNyeb.js → diagram-G4DWMVQ6-DQu85hhH.js} +1 -1
  69. package/payload/server/public/assets/{diagram-MMDJMWI5-Da0x1Yf0.js → diagram-MMDJMWI5-5tstbs4y.js} +1 -1
  70. package/payload/server/public/assets/{diagram-TYMM5635-DoFKKnVr.js → diagram-TYMM5635--MOV1U4o.js} +1 -1
  71. package/payload/server/public/assets/{erDiagram-SMLLAGMA-DBPmAPTe.js → erDiagram-SMLLAGMA-BtAnOJmd.js} +1 -1
  72. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CnkJzYU4.js → flowDiagram-DWJPFMVM-CybiCIih.js} +1 -1
  73. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-BM9c9VN5.js → ganttDiagram-T4ZO3ILL-B44jN7Cy.js} +1 -1
  74. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-C-1OqQDw.js → gitGraphDiagram-UUTBAWPF-CT7iYcwg.js} +1 -1
  75. package/payload/server/public/assets/graph-Cma7EArf.js +1 -0
  76. package/payload/server/public/assets/{graph-labels-BkDqelbQ.js → graph-labels-DyKk6Sxf.js} +1 -1
  77. package/payload/server/public/assets/{graphlib-Be3_mRCh.js → graphlib-DP4o0pYL.js} +1 -1
  78. package/payload/server/public/assets/{infoDiagram-42DDH7IO-o4FB6Zjb.js → infoDiagram-42DDH7IO-CvxFpFHN.js} +1 -1
  79. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-BlnRfx6Z.js → ishikawaDiagram-UXIWVN3A-CUwWLPeR.js} +1 -1
  80. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-Da26YPBj.js → journeyDiagram-VCZTEJTY-y-OnGgLi.js} +1 -1
  81. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-C1jVbsV0.js → kanban-definition-6JOO6SKY-BhrSe8R4.js} +1 -1
  82. package/payload/server/public/assets/{lib-CKcXekKF.js → lib-CpkYtEDz.js} +5 -5
  83. package/payload/server/public/assets/{line-BS0h3YlR.js → line-C9uS7z4J.js} +1 -1
  84. package/payload/server/public/assets/{mermaid-parser.core-4FY7A26h.js → mermaid-parser.core-Cg4ZdKp-.js} +1 -1
  85. package/payload/server/public/assets/{mermaid.core-CD1JRYmG.js → mermaid.core-BHdKOsex.js} +3 -3
  86. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-BmCJ5q81.js → mindmap-definition-QFDTVHPH-NBYiXHo7.js} +1 -1
  87. package/payload/server/public/assets/{page-D6QVxITy.js → page-B4IWl3aZ.js} +1 -1
  88. package/payload/server/public/assets/{page-1atlhFFT.js → page-D9YpwhIu.js} +1 -1
  89. package/payload/server/public/assets/{pieDiagram-DEJITSTG-ClXRAXST.js → pieDiagram-DEJITSTG-CbojC64C.js} +1 -1
  90. package/payload/server/public/assets/{public-7fuvQJwF.js → public-BtOXjy3A.js} +5 -5
  91. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-D17aBgPP.js → quadrantDiagram-34T5L4WZ-CnnoZXcL.js} +1 -1
  92. package/payload/server/public/assets/{requirementDiagram-MS252O5E-CUpkUJlh.js → requirementDiagram-MS252O5E-DFRFRtyJ.js} +1 -1
  93. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DnrrGp2c.js → sankeyDiagram-XADWPNL6-DU5gcpzO.js} +1 -1
  94. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-CMprmJg9.js → sequenceDiagram-FGHM5R23-BN7HZ6Hq.js} +1 -1
  95. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-BdpBUtXc.js → stateDiagram-FHFEXIEX-DG6cCjvg.js} +1 -1
  96. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CvVd1Q_q.js +1 -0
  97. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-u7JucCMs.js → timeline-definition-GMOUNBTQ-qmV-8SEV.js} +1 -1
  98. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-CP1Q9zf2.js → vennDiagram-DHZGUBPP-BMXQ1s3n.js} +1 -1
  99. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BOVxInz5.js → wardleyDiagram-NUSXRM2D-CoAdP1Gw.js} +1 -1
  100. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-Dcx1f04K.js → xychartDiagram-5P7HB3ND-CZlfGTEn.js} +1 -1
  101. package/payload/server/public/data.html +5 -5
  102. package/payload/server/public/graph.html +6 -6
  103. package/payload/server/public/index.html +8 -8
  104. package/payload/server/public/public.html +5 -5
  105. package/payload/server/public/assets/channel-sqt9QaiU.js +0 -1
  106. package/payload/server/public/assets/classDiagram-6PBFFD2Q-rYq1YLUo.js +0 -1
  107. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-Cc4tKtqG.js +0 -1
  108. package/payload/server/public/assets/clone-SdNqIUjS.js +0 -1
  109. package/payload/server/public/assets/data-DQafmlMy.js +0 -1
  110. package/payload/server/public/assets/graph-CTM-V4lS.js +0 -1
  111. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-DPekwBDP.js +0 -1
  112. /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();