@rubytech/create-maxy-code 0.1.144 → 0.1.147

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 (130) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/neo4j/schema.cypher +154 -4
  3. package/payload/platform/plugins/admin/PLUGIN.md +1 -1
  4. package/payload/platform/plugins/admin/hooks/post-turn-graph-pass.sh +251 -0
  5. package/payload/platform/plugins/admin/skills/datetime/SKILL.md +63 -5
  6. package/payload/platform/plugins/docs/references/internals.md +16 -0
  7. package/payload/platform/plugins/docs/references/memory-guide.md +10 -0
  8. package/payload/platform/plugins/docs/references/platform.md +1 -1
  9. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  10. package/payload/platform/plugins/memory/PLUGIN.md +25 -3
  11. package/payload/platform/plugins/memory/mcp/dist/index.js +309 -11
  12. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  13. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.d.ts +2 -0
  14. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.d.ts.map +1 -0
  15. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +33 -0
  16. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -0
  17. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.d.ts +2 -0
  18. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.d.ts.map +1 -0
  19. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.js +30 -0
  20. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.js.map +1 -0
  21. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.d.ts +2 -0
  22. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.d.ts.map +1 -0
  23. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.js +51 -0
  24. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.js.map +1 -0
  25. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.d.ts +2 -0
  26. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.d.ts.map +1 -0
  27. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.js +68 -0
  28. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.js.map +1 -0
  29. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js +41 -0
  30. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js.map +1 -1
  31. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts +43 -0
  32. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts.map +1 -0
  33. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js +33 -0
  34. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js.map +1 -0
  35. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts +18 -5
  36. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts.map +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js +64 -17
  38. package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.d.ts +44 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.d.ts.map +1 -0
  41. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.js +14 -0
  42. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.js.map +1 -0
  43. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts +26 -0
  44. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts.map +1 -0
  45. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js +219 -0
  46. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js.map +1 -0
  47. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.d.ts +19 -0
  48. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.d.ts.map +1 -0
  49. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.js +39 -0
  50. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.js.map +1 -0
  51. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts +19 -0
  52. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts.map +1 -0
  53. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js +42 -0
  54. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js.map +1 -0
  55. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.d.ts +21 -0
  56. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.d.ts.map +1 -0
  57. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.js +27 -0
  58. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.js.map +1 -0
  59. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.d.ts +18 -0
  60. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.d.ts.map +1 -0
  61. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.js +60 -0
  62. package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.js.map +1 -0
  63. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +20 -3
  64. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -1
  65. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +47 -12
  66. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
  67. package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.d.ts +13 -0
  68. package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.d.ts.map +1 -0
  69. package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.js +191 -0
  70. package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.js.map +1 -0
  71. package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts +7 -1
  72. package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts.map +1 -1
  73. package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js +37 -9
  74. package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js.map +1 -1
  75. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.d.ts +2 -0
  76. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.d.ts.map +1 -0
  77. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js +81 -0
  78. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js.map +1 -0
  79. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.d.ts +2 -0
  80. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.d.ts.map +1 -0
  81. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js +115 -0
  82. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js.map +1 -0
  83. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js +1 -0
  84. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js.map +1 -1
  85. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js +1 -0
  86. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js.map +1 -1
  87. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts +19 -0
  88. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts.map +1 -0
  89. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js +50 -0
  90. package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js.map +1 -0
  91. package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.d.ts +45 -0
  92. package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.d.ts.map +1 -0
  93. package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.js +170 -0
  94. package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.js.map +1 -0
  95. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.d.ts +28 -0
  96. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.d.ts.map +1 -0
  97. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.js +64 -0
  98. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.js.map +1 -0
  99. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.d.ts +24 -0
  100. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.d.ts.map +1 -0
  101. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.js +49 -0
  102. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.js.map +1 -0
  103. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts +34 -0
  104. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts.map +1 -0
  105. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js +99 -0
  106. package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js.map +1 -0
  107. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts +29 -0
  108. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts.map +1 -0
  109. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js +66 -0
  110. package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js.map +1 -0
  111. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
  112. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +122 -2
  113. package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
  114. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts +49 -13
  115. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  116. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +184 -16
  117. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  118. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  119. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +53 -10
  120. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  121. package/payload/platform/plugins/memory/mcp/vitest.config.ts +6 -0
  122. package/payload/platform/plugins/memory/references/schema-base.md +4 -0
  123. package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +3 -4
  124. package/payload/platform/plugins/scheduling/skills/briefing/SKILL.md +13 -2
  125. package/payload/platform/scripts/seed-neo4j.sh +1 -1
  126. package/payload/platform/templates/agents/admin/IDENTITY.md +9 -1
  127. package/payload/platform/templates/specialists/agents/database-operator.md +30 -2
  128. package/payload/server/server.js +195 -91
  129. package/payload/platform/plugins/admin/hooks/signal-detector-stop.sh +0 -309
  130. package/payload/platform/templates/specialists/agents/signal-detector.md +0 -129
@@ -1,309 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Stop hook — gbrain signal-detector dispatch. Task 303.
3
- #
4
- # Fires on every admin-agent assistant end_turn. Inspects the operator's
5
- # JSONL transcript and decides whether to invoke the `signal-detector`
6
- # specialist to extract entity mentions and original-thinking candidates
7
- # from the conversation slice since the last dispatch.
8
- #
9
- # Trigger logic (must clear both gates to fire):
10
- # - >= MIN_NEW_TURNS new real-user messages since the most recent
11
- # in-transcript Task dispatch with subagent_type='signal-detector'
12
- # (default 3 — overridable via MAXY_SIGNAL_DETECTOR_MIN_NEW_TURNS).
13
- # - >= MIN_IDLE_MS milliseconds since the latest real-user message's
14
- # `timestamp` (default 30_000 — overridable via
15
- # MAXY_SIGNAL_DETECTOR_MIN_IDLE_MS). Captures "operator paused, not
16
- # mid-thought".
17
- #
18
- # Recursion guard. The dispatch resolves through a Task tool call carrying
19
- # `subagent_type=signal-detector`; once such a call appears in the transcript
20
- # after the latest real-user message, the gate has already fired for this
21
- # turn and must NOT re-fire. Same shape as
22
- # session-end-retrospective.sh's sentinel grep
23
- # ([[feedback_no_stdout_parsing_for_control_flow]] /
24
- # [[feedback_doctrine_paragraph_is_not_a_gate]]) — control flow gates on a
25
- # structural tool_use, never on prose.
26
- #
27
- # Specialist auto-install on first fire. The template ships at
28
- # `$MAXY_TEMPLATES_DIR/specialists/agents/signal-detector.md` (or
29
- # `$ACCOUNT_DIR/../templates/...` via PLATFORM_ROOT). On the first fire
30
- # that decides to dispatch, the hook idempotently copies the template into
31
- # `$ACCOUNT_DIR/specialists/agents/` and appends the AGENTS.md routing line
32
- # if absent — so signal-detector is platform-core (no operator install
33
- # step) without surgery on the onboarding flow.
34
- #
35
- # Gating (emits via `/api/admin/log-ingest`; stderr stays silent on every
36
- # skip path; the gate-blocked path is the only stderr emitter, by Stop-hook
37
- # contract — exit 2 + stderr is how the agent receives the dispatch
38
- # directive):
39
- # - MAXY_SESSION_ROLE must equal "admin" → reason=role-not-admin
40
- # - MAXY_SPECIALIST must be empty → reason=is-specialist
41
- # - Stop-hook stdin must be non-empty → reason=empty-stdin
42
- # - transcript_path must exist on disk → reason=missing-transcript
43
- # - new real-user turns since last fire < MIN_NEW → reason=below-turn-threshold
44
- # - idle_ms < MIN_IDLE_MS → reason=below-idle-threshold
45
- # - latest real-user message older than the latest → reason=already-dispatched
46
- # signal-detector dispatch
47
- #
48
- # Blocks:
49
- # - gate-blocked sessionId=<id> reason=dispatching newTurns=<n> idleMs=<n>
50
- #
51
- # Input: Claude Code's Stop hook stdin shape
52
- # { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
53
-
54
- set -uo pipefail
55
-
56
- MIN_NEW_TURNS="${MAXY_SIGNAL_DETECTOR_MIN_NEW_TURNS:-3}"
57
- MIN_IDLE_MS="${MAXY_SIGNAL_DETECTOR_MIN_IDLE_MS:-30000}"
58
-
59
- UI_PORT="${MAXY_UI_INTERNAL_PORT:-}"
60
- if [ -z "$UI_PORT" ]; then
61
- echo "[signal-detector-hook] trigger-skipped sessionId=unknown reason=missing-env env=MAXY_UI_INTERNAL_PORT" >&2
62
- exit 0
63
- fi
64
- UI_BASE="http://127.0.0.1:${UI_PORT}"
65
- LOG_INGEST_URL="${UI_BASE}/api/admin/log-ingest"
66
-
67
- 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 "$(python3 -c '
73
- import sys, json
74
- print(json.dumps({"tag": "signal-detector-hook", "level": "info", "line": sys.argv[1]}))
75
- ' "$line")" \
76
- "$LOG_INGEST_URL" 2>/dev/null || true
77
- }
78
-
79
- INPUT=""
80
- if [ -t 0 ]; then
81
- INPUT=""
82
- else
83
- INPUT=$(cat)
84
- fi
85
-
86
- PARSED=$(printf '%s' "$INPUT" | python3 -c '
87
- import sys, json
88
- try:
89
- d = json.load(sys.stdin)
90
- sid = d.get("session_id", "") or ""
91
- tpath = d.get("transcript_path", "") or ""
92
- print(f"{sid}\t{tpath}")
93
- except Exception:
94
- print("\t")
95
- ' 2>/dev/null)
96
- SESSION_ID="${PARSED%% *}"
97
- TRANSCRIPT_PATH="${PARSED#* }"
98
- SESSION_ID_REPORTED="${SESSION_ID:-unknown}"
99
-
100
- if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
101
- emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=role-not-admin"
102
- exit 0
103
- fi
104
- # Recursion guard. A specialist subagent (database-operator, signal-detector
105
- # itself, etc.) stamps `MAXY_SPECIALIST` on its PTY env. Skip — the hook
106
- # only fires on the operator's own admin session, never on a subagent's
107
- # own stop.
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 "$INPUT" ]; then
113
- emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=empty-stdin"
114
- exit 0
115
- fi
116
- if [ -z "$SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
117
- emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=missing-transcript"
118
- exit 0
119
- fi
120
-
121
- # Single Python pass over the transcript. Returns three tab-separated
122
- # fields:
123
- # 1. count of real-user messages whose timestamp is more recent than the
124
- # latest in-transcript Task dispatch with subagent_type=signal-detector
125
- # (or, when no such dispatch exists yet, the total real-user count)
126
- # 2. idle_ms — milliseconds between the latest real-user message's
127
- # timestamp and now (hook fire time)
128
- # 3. excerpt — the latest real-user message's text, truncated to 2000
129
- # chars and base64-encoded so newlines and quotes survive the shell
130
- # round-trip
131
- INSPECTION=$(TRANSCRIPT_PATH="$TRANSCRIPT_PATH" python3 - <<'PY'
132
- import os, json, base64
133
- from datetime import datetime, timezone
134
-
135
- def is_real_user(rec):
136
- if not isinstance(rec, dict) or rec.get("type") != "user":
137
- return False
138
- msg = rec.get("message")
139
- if not isinstance(msg, dict) or msg.get("role") != "user":
140
- return False
141
- content = msg.get("content")
142
- if isinstance(content, str):
143
- return True
144
- if isinstance(content, list):
145
- for b in content:
146
- if isinstance(b, dict) and b.get("type") != "tool_result":
147
- return True
148
- return False
149
- return True
150
-
151
- def extract_user_text(rec):
152
- msg = rec.get("message", {}) or {}
153
- content = msg.get("content")
154
- if isinstance(content, str):
155
- return content
156
- if isinstance(content, list):
157
- out = []
158
- for b in content:
159
- if isinstance(b, dict) and b.get("type") == "text":
160
- t = b.get("text")
161
- if isinstance(t, str):
162
- out.append(t)
163
- return "".join(out)
164
- return ""
165
-
166
- def latest_signal_detector_dispatch_index(records):
167
- """Return the index of the most recent assistant tool_use of `Task`
168
- whose `subagent_type` is `signal-detector`, or -1 if none."""
169
- for i in range(len(records) - 1, -1, -1):
170
- rec = records[i]
171
- if not isinstance(rec, dict) or rec.get("type") != "assistant":
172
- continue
173
- msg = rec.get("message", {}) or {}
174
- if msg.get("role") != "assistant":
175
- continue
176
- content = msg.get("content")
177
- if not isinstance(content, list):
178
- continue
179
- for b in content:
180
- if not isinstance(b, dict) or b.get("type") != "tool_use":
181
- continue
182
- if b.get("name") != "Task":
183
- continue
184
- inp = b.get("input", {})
185
- if isinstance(inp, dict) and inp.get("subagent_type") == "signal-detector":
186
- return i
187
- return -1
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
- # Fail-open — print empty fields, the bash side will treat as missing.
203
- print("0\t0\t")
204
- raise SystemExit(0)
205
-
206
- last_dispatch_idx = latest_signal_detector_dispatch_index(records)
207
-
208
- # Count real-user messages after last_dispatch_idx (exclusive); when
209
- # last_dispatch_idx is -1, count from the start.
210
- new_user_count = 0
211
- latest_user_ts = None
212
- latest_user_text = ""
213
- start = last_dispatch_idx + 1
214
- for i in range(start, len(records)):
215
- rec = records[i]
216
- if is_real_user(rec):
217
- new_user_count += 1
218
- ts = rec.get("timestamp") or ""
219
- if isinstance(ts, str) and ts:
220
- latest_user_ts = ts
221
- latest_user_text = extract_user_text(rec).strip()
222
-
223
- if latest_user_ts is None:
224
- print(f"{new_user_count}\t0\t")
225
- raise SystemExit(0)
226
-
227
- try:
228
- # JSONL timestamps from Claude Code are ISO 8601 with Z suffix.
229
- parsed = datetime.fromisoformat(latest_user_ts.replace("Z", "+00:00"))
230
- now = datetime.now(timezone.utc)
231
- idle_ms = max(0, int((now - parsed).total_seconds() * 1000))
232
- except Exception:
233
- idle_ms = 0
234
-
235
- excerpt = latest_user_text[:2000]
236
- b64 = base64.b64encode(excerpt.encode("utf-8")).decode("ascii")
237
- print(f"{new_user_count}\t{idle_ms}\t{b64}")
238
- PY
239
- )
240
- NEW_TURNS="${INSPECTION%% *}"
241
- REMAINING="${INSPECTION#* }"
242
- IDLE_MS="${REMAINING%% *}"
243
- EXCERPT_B64="${REMAINING#* }"
244
-
245
- # Throttle gates.
246
- if [ "$NEW_TURNS" -eq 0 ] 2>/dev/null; then
247
- emit_log "trigger-skipped sessionId=${SESSION_ID} reason=already-dispatched newTurns=0"
248
- exit 0
249
- fi
250
- if [ "$NEW_TURNS" -lt "$MIN_NEW_TURNS" ] 2>/dev/null; then
251
- emit_log "trigger-skipped sessionId=${SESSION_ID} reason=below-turn-threshold newTurns=${NEW_TURNS} threshold=${MIN_NEW_TURNS}"
252
- exit 0
253
- fi
254
- if [ "$IDLE_MS" -lt "$MIN_IDLE_MS" ] 2>/dev/null; then
255
- emit_log "trigger-skipped sessionId=${SESSION_ID} reason=below-idle-threshold idleMs=${IDLE_MS} threshold=${MIN_IDLE_MS}"
256
- exit 0
257
- fi
258
-
259
- # Specialist auto-install. The template lives under
260
- # `$PLATFORM_ROOT/templates/specialists/agents/signal-detector.md`. The
261
- # installer / seeder copies templates to per-account dirs at onboarding;
262
- # this fallback heals an account that pre-dates the template's existence
263
- # without forcing the operator to re-run onboarding.
264
- PLATFORM_ROOT_FALLBACK="${MAXY_PLATFORM_ROOT:-}"
265
- ACCOUNT_DIR_LOCAL="${ACCOUNT_DIR:-}"
266
- SPECIALIST_TARGET="${ACCOUNT_DIR_LOCAL}/specialists/agents/signal-detector.md"
267
- if [ -n "$ACCOUNT_DIR_LOCAL" ] && [ ! -f "$SPECIALIST_TARGET" ] && [ -n "$PLATFORM_ROOT_FALLBACK" ]; then
268
- TEMPLATE_SRC="${PLATFORM_ROOT_FALLBACK}/templates/specialists/agents/signal-detector.md"
269
- if [ -f "$TEMPLATE_SRC" ]; then
270
- mkdir -p "$(dirname "$SPECIALIST_TARGET")" 2>/dev/null
271
- cp "$TEMPLATE_SRC" "$SPECIALIST_TARGET" 2>/dev/null
272
- AGENTS_MD="${ACCOUNT_DIR_LOCAL}/agents/admin/AGENTS.md"
273
- if [ -f "$AGENTS_MD" ]; then
274
- if ! grep -q "specialists:signal-detector" "$AGENTS_MD" 2>/dev/null; then
275
- printf '%s\n' "- **specialists:signal-detector**: Background brain-compounder dispatched only by the admin-agent Stop hook (\`platform/plugins/admin/hooks/signal-detector-stop.sh\`). Never invoke manually." >> "$AGENTS_MD"
276
- fi
277
- fi
278
- emit_log "bootstrap-installed sessionId=${SESSION_ID} target=${SPECIALIST_TARGET}"
279
- fi
280
- fi
281
-
282
- EXCERPT_PREVIEW=$(printf '%s' "$EXCERPT_B64" | base64 --decode 2>/dev/null | head -c 200 | tr '\n' ' ')
283
- emit_log "gate-blocked sessionId=${SESSION_ID} reason=dispatching newTurns=${NEW_TURNS} idleMs=${IDLE_MS} preview=$(printf '%s' "$EXCERPT_PREVIEW" | sed 's/[\"]//g' | head -c 80)"
284
-
285
- # Emit the dispatch directive on stderr per Stop-hook contract. The admin
286
- # IDENTITY.md "Stop-hook system directives" section names this token prefix
287
- # and the binding response shape — agent must perform the single Agent
288
- # dispatch and emit an empty acknowledgement on return, nothing else.
289
- EXCERPT_DECODED=$(printf '%s' "$EXCERPT_B64" | base64 --decode 2>/dev/null)
290
-
291
- cat >&2 <<DIRECTIVE
292
- [system: signal-detector-stop-hook]
293
- Dispatch the signal-detector specialist NOW using the Task tool. Do not respond to the operator; this is a background brain-compounding task and the operator does not see this turn. After the specialist returns its one-line summary, emit a single space character and stop.
294
-
295
- Agent dispatch parameters:
296
- subagent_type: signal-detector
297
- prompt: |
298
- accountId: ${ACCOUNT_ID:-<unset>}
299
- sessionId: ${SESSION_ID}
300
- newUserTurnsSinceLastRun: ${NEW_TURNS}
301
- idleMsBeforeFire: ${IDLE_MS}
302
-
303
- The operator's latest turn (truncated to 2000 chars):
304
- ${EXCERPT_DECODED}
305
-
306
- Resolve the operator's most recent :Message in this conversation via memory-search (filter by conversationId from your MCP server env), then write :MENTIONS edges and :Idea nodes per your specialist body. Return one line in the documented output shape.
307
- DIRECTIVE
308
-
309
- exit 2
@@ -1,129 +0,0 @@
1
- ---
2
- name: signal-detector
3
- description: "Background brain-compounder. Dispatched only by the admin-agent Stop hook (`platform/plugins/admin/hooks/signal-detector-stop.sh`) when per-conversation throttle thresholds are met (default ≥3 new operator turns AND ≥30s idle since last user message). Extracts entity mentions and original-thinking candidates from the just-completed conversation slice, looks them up against the graph, and writes `:Message-[:MENTIONS]->Entity` edges and `:Idea` nodes through the schema-bounded `memory-write` gate. The dispatch prompt names accountId, conversationId, userMessageId, and the conversation excerpt — the specialist has no other context. Never invoked by operator intent; not routed via `<specialist-domains>`. Operator-facing channels only (admin web chat, WhatsApp-to-admin, Telegram-to-admin) by Maxy doctrine: visitor utterances on public channels are customer-intent, not high-value brain signal."
4
- summary: "Background process the operator never explicitly invokes. Runs after each operator turn settles to capture entity mentions and original-thinking candidates into the graph, so the brain compounds turn-by-turn instead of relying on the agent to remember to write things down."
5
- model: claude-haiku-4-5
6
- tools: mcp__memory__memory-search, mcp__memory__memory-find-candidates, mcp__memory__memory-write
7
- ---
8
-
9
- # Signal Detector
10
-
11
- You are dispatched by the admin-agent Stop hook with a self-contained brief: an `accountId`, a `conversationId`, the most-recent user `:Message` elementId, and the textual excerpt of the operator's last turn. Your single job is to compound that turn into the operator's graph.
12
-
13
- ## Prerogatives
14
-
15
- Four rules govern every dispatch. They override any conflicting general instinct.
16
-
17
- **PRECISE.** Use exact label names, exact edge types, exact tool names. When the schema does not name a label or edge for what you observed, omit the candidate — never invent a synonym. *Failure symptoms:* writing `:Mentions` (wrong case), inventing `:Mentioned_By` (synonym of canonical `:MENTIONS`), label-coining `:Topic` when the schema only knows `:Concept`.
18
-
19
- **EVIDENCE-BASED.** Every proposed entity must be lookup-checked against the graph before write. Use `memory-search` or `memory-find-candidates` scoped by `accountId` to resolve "Adam Langley" to an existing `:Person` if present. Only mint a new stub when the search returns zero candidates. *Failure symptoms:* writing a new `:Person` for a name that already exists three rows; coining `:Organization {name: "Anthropic"}` when an `:Organization {name: "Anthropic, PBC"}` is already in the graph.
20
-
21
- **CONCISE.** Your output to the calling agent is a single line: `signal-detector: mentions=<n> ideas=<n> new-entities=<n> ms=<elapsed>`. No prose summary, no listing of names. The operator does not see your output; the calling agent sees it as a tool-result and proceeds to natural stop. *Failure symptoms:* a paragraph describing what you extracted; listing every mention by name; explaining why no ideas were found.
22
-
23
- **LOUD-FAIL.** If `memory-write` rejects a proposal with a `graph-write-gate` reason (`zero-relationships`, `unresolved-target`, `missing-provenance`, `removed-feature`), surface the reason in your single-line output and stop — never improvise via raw cypher (you do not hold those tools), never retry with a different shape. *Failure symptoms:* re-trying the same write three times hoping for a different answer; coining a `:PRODUCED` edge from a label that the gate does not accept.
24
-
25
- ---
26
-
27
- ## Inputs you receive from the Stop hook
28
-
29
- The dispatch prompt names exactly:
30
-
31
- - `accountId` — the operator's account scope. Every search and write carries this.
32
- - `conversationId` — the active `:Conversation` elementId.
33
- - `userMessageId` — the elementId of the `:Message` that triggered the agent turn just ended.
34
- - `excerpt` — the operator's last turn text (and optionally the assistant's response, when context matters).
35
- - `lastRunAt` — ISO timestamp of the most-recent signal-detector dispatch for this conversation in this session, or `null` for first run.
36
-
37
- If any input is missing, return `signal-detector: error reason=missing-input field=<name>` and stop.
38
-
39
- ---
40
-
41
- ## What you extract
42
-
43
- Two kinds of signal:
44
-
45
- 1. **Mentions** — named entities the operator referred to: people, organisations, services, concepts. Examples: "I caught up with Adam Langley at Rubytech yesterday" yields candidates `:Person "Adam Langley"`, `:Organization "Rubytech"`. "We're going to use the new EU AI Act framework" yields `:Concept "EU AI Act"`. Filter aggressively — a passing pronoun ("she said yes") is not a mention; a named subject ("Adam said yes") is.
46
-
47
- 2. **Ideas** — original-thinking candidates: substantive operator-authored statements that are not requests, questions, or commands. Examples: "I think we should reposition the wedge as cross-CRM rather than vertical-specific"; "The compounding curve only kicks in once mentions hit ~30 per entity". Filter aggressively — operator asks, agreements, and tool-instructions are not ideas. An idea has a claim, a position, or a hypothesis embedded in it.
48
-
49
- When in doubt, skip. Landfill compounds against the brain; sparse high-quality signal compounds for it.
50
-
51
- ---
52
-
53
- ## Write shape
54
-
55
- All writes use `scope: "admin"` — signal-detector only runs on admin-side conversations by Maxy doctrine. The `direction` field is relative to the node being written: `"incoming"` means the edge points INTO this node from `targetNodeId`.
56
-
57
- **Mention to existing entity** — `memory-search` already resolved an `elementId`:
58
-
59
- ```
60
- memory-write(
61
- labels: ["Person"], // or Organization, Service, Concept — match the resolved candidate
62
- properties: { elementId: <resolved> },
63
- scope: "admin",
64
- relationships: [
65
- { type: "MENTIONS", direction: "incoming", targetNodeId: <userMessageId> }
66
- ]
67
- )
68
- ```
69
-
70
- The `:MENTIONS` edge runs FROM `:Message` TO the entity — that is `incoming` relative to the entity. Direction matters — get it wrong and retrieval breaks.
71
-
72
- **Mention to new entity stub** (only when `memory-search` returned zero):
73
-
74
- ```
75
- memory-write(
76
- labels: ["Person"], // or Organization, Service, Concept
77
- properties: { name: "Adam Langley", accountId: <accountId> },
78
- scope: "admin",
79
- relationships: [
80
- { type: "PRODUCED", direction: "incoming", targetNodeId: <userMessageId> },
81
- { type: "MENTIONS", direction: "incoming", targetNodeId: <userMessageId> }
82
- ]
83
- )
84
- ```
85
-
86
- Two edges, one write call. `:PRODUCED` satisfies the schema-bounded `ACTION_PROVENANCE_LABELS` gate (Person, Organization, LocalBusiness require provenance); `:MENTIONS` carries the semantic. The gate rejects the write if either edge is missing on a labeled-for-provenance node.
87
-
88
- **Idea:**
89
-
90
- ```
91
- memory-write(
92
- labels: ["Idea"],
93
- properties: { body: <verbatim text>, accountId: <accountId>, conversationId: <conversationId> },
94
- scope: "admin",
95
- relationships: [
96
- { type: "HAS_IDEA", direction: "incoming", targetNodeId: <conversationId> }
97
- ]
98
- )
99
- ```
100
-
101
- `:Idea` is not in `ACTION_PROVENANCE_LABELS` — one edge to the `:Conversation` is enough.
102
-
103
- ---
104
-
105
- ## What you do NOT do
106
-
107
- - Do not write `:Mention` *nodes*. The edge is `:MENTIONS`, written between `:Message` and the entity. There is no `:Mention` node type.
108
- - Do not delete, update, or merge any existing entity. You only add.
109
- - Do not write `:CustomerInquiry` or any visitor-channel shape. Visitor signal does not route through this specialist; you only run on operator channels.
110
- - Do not respond conversationally. Your output is the one-line summary, nothing else.
111
- - Do not call any tool not listed in your `tools:` frontmatter. If a write needs a tool you do not hold, return `signal-detector: error reason=missing-tool tool=<name>` and stop.
112
-
113
- ---
114
-
115
- ## Output contract
116
-
117
- Exactly one line, returned to the admin agent's tool-result channel:
118
-
119
- ```
120
- signal-detector: mentions=<n-mentions> ideas=<n-ideas> new-entities=<n-stubs-created> ms=<elapsed-from-dispatch>
121
- ```
122
-
123
- On error:
124
-
125
- ```
126
- signal-detector: error reason=<kebab-case-reason> [field=<name>]
127
- ```
128
-
129
- The admin agent acknowledges this output, emits its natural stop, and the Stop hook recognises the successful dispatch via transcript inspection on its next fire and allows the session to settle.