@rubytech/create-maxy-code 0.1.143 → 0.1.146
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/neo4j/schema.cypher +154 -4
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/post-turn-graph-pass.sh +251 -0
- package/payload/platform/plugins/admin/skills/datetime/SKILL.md +63 -5
- package/payload/platform/plugins/docs/references/internals.md +16 -0
- package/payload/platform/plugins/docs/references/memory-guide.md +10 -0
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +25 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js +309 -11
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +33 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.js +30 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/graph-write-gate-label.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.js +51 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/phase-prune-revisions.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.js +68 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relative-date.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js +41 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/timeline-extractor.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts +43 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js +33 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-revision.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts +18 -5
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js +64 -17
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.d.ts +44 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.js +14 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/index.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts +26 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js +219 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-citation-audit.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.d.ts +19 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.js +39 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-dead-edges.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts +19 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js +42 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-orphans.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.d.ts +21 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.js +27 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-prune-revisions.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.d.ts +18 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.js +60 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/dream-cycle/phase-stale-truth.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts +20 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +47 -12
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.d.ts +13 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.js +191 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/relative-date.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts +7 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js +37 -9
- package/payload/platform/plugins/memory/mcp/dist/lib/timeline-extractor.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js +81 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-compiled-truth-history.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js +115 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-report-tools.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-emit.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts +19 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js +50 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-compiled-truth-history.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.d.ts +45 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.js +170 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-dream-run.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.d.ts +28 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.js +64 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-list.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.d.ts +24 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.js +49 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-read-latest.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts +34 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js +99 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-report-write.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts +29 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js +66 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-review-queue.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +122 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts +49 -13
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +184 -16
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +53 -10
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +6 -0
- package/payload/platform/plugins/memory/references/schema-base.md +4 -0
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +3 -4
- package/payload/platform/plugins/scheduling/skills/briefing/SKILL.md +13 -2
- package/payload/platform/scripts/seed-neo4j.sh +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +9 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +30 -2
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/server.js +195 -91
- package/payload/platform/plugins/admin/hooks/signal-detector-stop.sh +0 -309
- 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.
|