@rubytech/create-maxy-code 0.1.281 → 0.1.283

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 (76) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +0 -1
  3. package/payload/platform/plugins/admin/PLUGIN.md +2 -1
  4. package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +154 -0
  5. package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-directive.test.sh +24 -0
  6. package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +174 -0
  7. package/payload/platform/plugins/admin/hooks/prompt-optimiser-directive.sh +22 -2
  8. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +5 -5
  9. package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +378 -56
  10. package/payload/platform/plugins/cloudflare/.claude-plugin/plugin.json +1 -1
  11. package/payload/platform/plugins/cloudflare/PLUGIN.md +5 -5
  12. package/payload/platform/plugins/cloudflare/references/api.md +107 -34
  13. package/payload/platform/plugins/cloudflare/references/d1-data-capture.md +29 -21
  14. package/payload/platform/plugins/cloudflare/references/dashboard-guide.md +3 -2
  15. package/payload/platform/plugins/cloudflare/references/hosting-sites.md +7 -5
  16. package/payload/platform/plugins/cloudflare/references/manual-setup.md +3 -2
  17. package/payload/platform/plugins/cloudflare/skills/cloudflare/SKILL.md +6 -6
  18. package/payload/platform/plugins/docs/references/cloudflare.md +1 -1
  19. package/payload/platform/plugins/docs/references/telegram-guide.md +3 -3
  20. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-resolve.test.js +25 -1
  21. package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-resolve.test.js.map +1 -1
  22. package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.d.ts +7 -0
  23. package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.d.ts.map +1 -1
  24. package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.js +9 -0
  25. package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.js.map +1 -1
  26. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -1
  27. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +6 -2
  28. package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -1
  29. package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +1 -0
  30. package/payload/platform/plugins/telegram/PLUGIN.md +7 -2
  31. package/payload/platform/plugins/telegram/mcp/dist/index.js +66 -11
  32. package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
  33. package/payload/platform/plugins/telegram/mcp/dist/lib/account-token.d.ts +6 -0
  34. package/payload/platform/plugins/telegram/mcp/dist/lib/account-token.d.ts.map +1 -0
  35. package/payload/platform/plugins/telegram/mcp/dist/lib/account-token.js +20 -0
  36. package/payload/platform/plugins/telegram/mcp/dist/lib/account-token.js.map +1 -0
  37. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts +21 -0
  38. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts.map +1 -0
  39. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js +52 -0
  40. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js.map +1 -0
  41. package/payload/platform/plugins/telegram/mcp/dist/tools/message.d.ts.map +1 -1
  42. package/payload/platform/plugins/telegram/mcp/dist/tools/message.js +5 -2
  43. package/payload/platform/plugins/telegram/mcp/dist/tools/message.js.map +1 -1
  44. package/payload/platform/plugins/telegram/references/setup-guide.md +10 -7
  45. package/payload/platform/plugins/telegram/skills/configure/SKILL.md +79 -0
  46. package/payload/platform/plugins/whatsapp/.claude-plugin/plugin.json +1 -1
  47. package/payload/platform/plugins/whatsapp/PLUGIN.md +7 -11
  48. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +10 -56
  49. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  50. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +26 -2
  51. package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +9 -14
  52. package/payload/platform/scripts/setup-account.sh +7 -0
  53. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  54. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +0 -2
  55. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  56. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  57. package/payload/platform/services/claude-session-manager/dist/http-server.js +80 -3
  58. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  59. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +6 -0
  60. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  61. package/payload/platform/services/whatsapp-channel/dist/notification.js +14 -0
  62. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  63. package/payload/platform/services/whatsapp-channel/dist/server.js +5 -3
  64. package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
  65. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  66. package/payload/server/public/assets/AdminShell-8Y7-maNi.js +1 -0
  67. package/payload/server/public/assets/{admin-0_VxffD5.js → admin-DyuZ4NT5.js} +1 -1
  68. package/payload/server/public/assets/{browser-Cxjkhtsk.js → browser-XrVAUQdG.js} +1 -1
  69. package/payload/server/public/assets/{data-BOgwOWnw.js → data-tcu3MXmK.js} +1 -1
  70. package/payload/server/public/assets/{graph-BqD2GjQd.js → graph-I4GyC2MW.js} +1 -1
  71. package/payload/server/public/browser.html +2 -2
  72. package/payload/server/public/data.html +2 -2
  73. package/payload/server/public/graph.html +2 -2
  74. package/payload/server/public/index.html +2 -2
  75. package/payload/server/server.js +882 -767
  76. package/payload/server/public/assets/AdminShell-FO766lOW.js +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.281",
3
+ "version": "0.1.283",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -55,7 +55,6 @@
55
55
  },
56
56
 
57
57
  "externalPlugins": [
58
- { "name": "telegram", "marketplace": "claude-plugins-official", "channelPlugin": true },
59
58
  { "name": "discord", "marketplace": "claude-plugins-official", "channelPlugin": true },
60
59
  { "name": "imessage", "marketplace": "claude-plugins-official", "channelPlugin": true },
61
60
  { "name": "superpowers", "marketplace": "claude-plugins-official" },
@@ -147,7 +147,8 @@ Tools are available via the `admin` MCP server.
147
147
  - `hooks/mcp-tool-missing.sh` — **PostToolUse hook on `mcp__.*` (Task 502, directive 3).** Defence-in-depth for the `No such tool available: mcp__…` failure class that the Task 502 name-binding is built to eliminate. Fires on any MCP tool call; no-op unless the `tool_response` carries `No such tool available` AND the qualified name resolves to a maxy plugin (read from the generated `hooks/lib/maxy-mcp-plugins.txt`). On a maxy match it logs one deterministic `[mcp-tool-missing] server=<server> tool=<tool>` line and exits 2 with a fixed envelope on stderr, so the agent relays a named server-unavailable failure instead of narrating "warming up" or blind-retrying. A missing non-maxy bridge tool (Playwright etc., upstream-owned) passes through (exit 0). The maxy-plugin list is regenerated and gate-diffed by `platform/scripts/check-canonical-tool-names.mjs`.
148
148
  - `hooks/post-tool-use-agent.sh` — **PostToolUse hook on `Agent` (Task 560).** Drains any subagent hook-decision buffers under `~/.maxy-code/logs/hook-decisions/` modified since this parent's previous PostToolUse-Agent fire (cursor file keyed by parent session id), prints one `[hook-propagate]` line per record to stdout — Claude Code attaches the stdout as a `hook_success` attachment on the parent JSONL, making the records grep-queryable from the parent session alone (Task 559 motivating case). Rotates consumed buffers to `consumed/`. Emits one `[hook-propagate-census] parentSession=<…> subagentHooksObserved=<N> attachmentsEmitted=<M>` line per fire to stdout and server.log; `N != M` is the propagation regression signal. The companion emitter library `hooks/lib/hook-emit.sh` is sourced by `post-tool-use-agent.sh` and any other hook that records a block decision (4 KB stderr truncation, `truncated=true` set on the record).
149
149
  - `hooks/admin-authoring-observer.sh` — **PostToolUse hook on Write and Edit (Task 486).** Observation only — never blocks; exits 0 on every path. Fires when the admin agent (not a specialist subagent — gated by `MAXY_SPECIALIST` env) writes or edits a file under `<accountDir>/output/`. Walks the session transcript from the latest real-user turn forward to detect any prior `Task` `tool_use` whose `subagent_type` starts with `specialists:`. Emits one stderr line `[admin-authoring] inline-write path=<rel> priorSpecialistSpawnInTurn=<true|false|unknown>`. A `false` value on a long-form prose file is the regression signal Task 486 was designed to make visible — the BioSymm proposal session (admin authored a customer-facing proposal inline despite content-producer being installed) is the failure mode this surfaces mechanically. Mechanical enforcement (refuse the write, force a re-spawn) is deferred per the task spec.
150
- - `hooks/prompt-optimiser-directive.sh` — **UserPromptSubmit hook (Task 698).** Injects the standing prompt-optimiser restatement directive plus the per-turn three-tier routing ladder as `additionalContext`: **(1)** delegate to the specialist that owns the deliverable via the Agent tool — the brief states the outcome plus binding constraints, never lines/anchors/literal text; **(2)** only if none fits, load an admin-usable skill with `skill-load`; **(3)** only if neither fits, author inline — freestyle is the named last resort. Re-emits the full agent roster (`agents/admin/AGENTS.md`) and the full admin-usable skills list (`agents/admin/ADMIN-SKILLS.md`) every turn by reading the two generated files from the account dir (the hook fires with the account dir as cwd); it never walks the plugins tree per turn. Fail-open is **visible**: a missing list logs `[prompt-optimiser] missing=<AGENTS.md\|ADMIN-SKILLS.md> emitting-partial` to stderr and the ladder still injects. The trivial-turn skip (one-word confirmation, slash-command, direct continuation) is unchanged. **Staleness:** `ADMIN-SKILLS.md` is regenerated only by `setup-account.sh`; a plugin add/remove since the last setup leaves the list stale — compare `ADMIN-SKILLS.md` mtime against the newest `SKILL.md` mtime and re-run setup to refresh. The list generator is `platform/scripts/lib/admin-skills-bootstrap.sh`; it logs `[admin-skills] scanned=<N> admin-usable=<M> no-declaration=<K>` (failure signature: `admin-usable=0` while `scanned>0`, or any `missing-declaration` line).
150
+ - `hooks/prompt-optimiser-directive.sh` — **UserPromptSubmit hook (Task 698).** Injects the standing prompt-optimiser restatement directive plus the per-turn three-tier routing ladder as `additionalContext`: **(1)** delegate to the specialist that owns the deliverable via the Agent tool — the brief states the outcome plus binding constraints, never lines/anchors/literal text; **(2)** only if none fits, load an admin-usable skill with `skill-load`; **(3)** only if neither fits, author inline — freestyle is the named last resort. Re-emits the full agent roster (`agents/admin/AGENTS.md`) and the full admin-usable skills list (`agents/admin/ADMIN-SKILLS.md`) every turn by reading the two generated files from the account dir (the hook fires with the account dir as cwd); it never walks the plugins tree per turn. Fail-open is **visible**: a missing list logs `[prompt-optimiser] missing=<AGENTS.md\|ADMIN-SKILLS.md> emitting-partial` to stderr and the ladder still injects. The trivial-turn skip (one-word confirmation, slash-command, direct continuation) is unchanged. **Staleness:** `ADMIN-SKILLS.md` is regenerated only by `setup-account.sh`; a plugin add/remove since the last setup leaves the list stale — compare `ADMIN-SKILLS.md` mtime against the newest `SKILL.md` mtime and re-run setup to refresh. The list generator is `platform/scripts/lib/admin-skills-bootstrap.sh`; it logs `[admin-skills] scanned=<N> admin-usable=<M> no-declaration=<K>` (failure signature: `admin-usable=0` while `scanned>0`, or any `missing-declaration` line). **Task 719:** the directive now carries a standing CAPABILITY-QUESTIONS-ARE-OWNED-WORK clause (how-to / "do you have instructions for X" / config questions about platform features are answered from the owning specialist or plugin tool/reference, never from training memory), and the hook appends a durable `<ts> [prompt-optimiser-directive] injected len=<n> session=<id>` breadcrumb to `$LOG_DIR/prompt-optimiser-directive.log` so per-turn injection is greppable, not stderr-only.
151
+ - `hooks/prompt-optimiser-compliance.sh` — **Stop hook (Task 719).** After each admin turn, reads the just-finished turn from `transcript_path` and appends `<ts> [prompt-optimiser-compliance] directive-fired no-route-taken session=<id8> prompt="<clip>"` to `$LOG_DIR/prompt-optimiser-directive.log` (and stderr) when the routing directive fired, the prompt was non-trivial (not a slash-command, not a one-word confirmation), and the turn took **no route** — no `Agent` dispatch, no `Skill` load, no `ToolSearch`, no `mcp__*` tool call. This is the standing compliance signal that surfaces the session-`da0b12d4` failure class (agent answers a capability question from memory) as a visible event instead of a silent stale answer. Directive-fired is detected by the marker `PROMPT-OPTIMISER DIRECTIVE` in the turn slice, so it is robust to the CC-version difference in how `UserPromptSubmit` `additionalContext` is recorded (`attachment`/`hook_success` vs `hook_additional_context`). **Known limitation:** "direct continuation of the prior turn" is not detectable from the transcript, so a continuation turn that legitimately needs no route can be flagged; treat the log as a review signal, not a gate. **Fail-open:** no python3, no `transcript_path`, or an unreadable transcript → exit 0, no output. Lives in the same log as the directive breadcrumb, so a single `grep` interleaves "fired" and "no-route" into one per-session timeline; cross-check via `platform/scripts/logs-read.sh <sessionKey>`. This is a lightweight transcript read, not a per-turn spawn (contrast the Task-626 turn recorder below).
151
152
  - **Turn recorder — removed entirely (Task 626).** The `turn-completed-graph-write.sh` Stop hook, the `/api/admin/claude-sessions` loopback bypass it relied on, the `[turn-recorder]` emitters, the envelope walker, and the recorder-auto-archive subscriber are deleted. It had been dormant since Task 214 (never re-registered in settings.json); the admin now writes to the graph by delegating to `database-operator` via the Task tool inside the live session, and the on-demand `/insight` pass (`skills/insight/SKILL.md`, a registered admin skill — Task 641) is the per-session review. There is no per-turn spawn.
152
153
 
153
154
  ## Session identifiers (Task 135)
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env bash
2
+ # Regression test for prompt-optimiser-compliance.sh (Task 719 Stop hook).
3
+ #
4
+ # A turn is flagged "directive-fired no-route-taken" only when the directive
5
+ # marker is in the turn slice AND the prompt is non-trivial AND no Agent/Skill/
6
+ # ToolSearch/mcp__* tool_use occurred. Covers both transcript record shapes,
7
+ # every route surface, the trivial filters, and fail-open.
8
+
9
+ set -u
10
+
11
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/prompt-optimiser-compliance.sh"
12
+ if [[ ! -x "$HOOK" ]]; then
13
+ echo "FAIL: $HOOK not executable" >&2
14
+ exit 1
15
+ fi
16
+
17
+ PASS=0
18
+ FAIL=0
19
+
20
+ # Build a transcript file from JSONL lines passed as args; echo its path.
21
+ mk_transcript() {
22
+ local f; f=$(mktemp)
23
+ local line
24
+ for line in "$@"; do printf '%s\n' "$line" >> "$f"; done
25
+ printf '%s' "$f"
26
+ }
27
+
28
+ # Run the hook against a transcript; capture combined stderr (where the flag is
29
+ # echoed). $1=transcript path.
30
+ run_hook() {
31
+ printf '{"transcript_path":"%s","session_id":"sess12345678"}' "$1" | bash "$HOOK" 2>&1
32
+ }
33
+
34
+ USER_PROMPT='{"type":"user","message":{"role":"user","content":"how do I set up a Google Workspace email"}}'
35
+ # UserPromptSubmit additionalContext, CC 2.1.x attachment/hook_success shape.
36
+ DIRECTIVE_ATTACH='{"type":"attachment","attachment":{"type":"hook_success","hookEvent":"UserPromptSubmit","stdout":"{\"hookSpecificOutput\":{\"additionalContext\":\"PROMPT-OPTIMISER DIRECTIVE (standing) ...\"}}"}}'
37
+ # Pi shape.
38
+ DIRECTIVE_PI='{"type":"hook_additional_context","additionalContext":"PROMPT-OPTIMISER DIRECTIVE (standing) ..."}'
39
+ ASSISTANT_TEXT='{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"To set up Gmail, enable IMAP in settings."}]}}'
40
+ ASSISTANT_AGENT='{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"Agent","input":{}}]}}'
41
+ ASSISTANT_SKILL='{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"Skill","input":{}}]}}'
42
+ ASSISTANT_MCP='{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"mcp__plugin_email__email-provider-info","input":{}}]}}'
43
+ ASSISTANT_TOOLSEARCH='{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"ToolSearch","input":{}}]}}'
44
+
45
+ assert_flagged() {
46
+ local name="$1" t="$2" out
47
+ out=$(run_hook "$t")
48
+ if printf '%s' "$out" | grep -q "no-route-taken"; then
49
+ echo "PASS: $name"; PASS=$((PASS+1))
50
+ else
51
+ echo "FAIL: $name (expected flag, got: $out)" >&2; FAIL=$((FAIL+1))
52
+ fi
53
+ rm -f "$t"
54
+ }
55
+
56
+ assert_not_flagged() {
57
+ local name="$1" t="$2" out
58
+ out=$(run_hook "$t")
59
+ if printf '%s' "$out" | grep -q "no-route-taken"; then
60
+ echo "FAIL: $name (expected NO flag, got: $out)" >&2; FAIL=$((FAIL+1))
61
+ else
62
+ echo "PASS: $name"; PASS=$((PASS+1))
63
+ fi
64
+ rm -f "$t"
65
+ }
66
+
67
+ # 1. Directive fired (attachment shape) + prose only -> flagged.
68
+ assert_flagged "attachment-shape directive + no route -> flagged" \
69
+ "$(mk_transcript "$USER_PROMPT" "$DIRECTIVE_ATTACH" "$ASSISTANT_TEXT")"
70
+
71
+ # 2. Directive fired (Pi shape) + prose only -> flagged.
72
+ assert_flagged "pi-shape directive + no route -> flagged" \
73
+ "$(mk_transcript "$USER_PROMPT" "$DIRECTIVE_PI" "$ASSISTANT_TEXT")"
74
+
75
+ # 3–6. Each route surface suppresses the flag.
76
+ assert_not_flagged "Agent dispatch -> not flagged" \
77
+ "$(mk_transcript "$USER_PROMPT" "$DIRECTIVE_ATTACH" "$ASSISTANT_AGENT")"
78
+ assert_not_flagged "Skill load -> not flagged" \
79
+ "$(mk_transcript "$USER_PROMPT" "$DIRECTIVE_ATTACH" "$ASSISTANT_SKILL")"
80
+ assert_not_flagged "mcp__ tool -> not flagged" \
81
+ "$(mk_transcript "$USER_PROMPT" "$DIRECTIVE_ATTACH" "$ASSISTANT_MCP")"
82
+ assert_not_flagged "ToolSearch -> not flagged" \
83
+ "$(mk_transcript "$USER_PROMPT" "$DIRECTIVE_ATTACH" "$ASSISTANT_TOOLSEARCH")"
84
+
85
+ # 7. No directive in the slice -> not flagged.
86
+ assert_not_flagged "no directive -> not flagged" \
87
+ "$(mk_transcript "$USER_PROMPT" "$ASSISTANT_TEXT")"
88
+
89
+ # 8. Slash-command prompt -> not flagged.
90
+ assert_not_flagged "slash-command -> not flagged" \
91
+ "$(mk_transcript '{"type":"user","message":{"role":"user","content":"/insight"}}' "$DIRECTIVE_ATTACH" "$ASSISTANT_TEXT")"
92
+
93
+ # 9. One-word confirmation -> not flagged.
94
+ assert_not_flagged "one-word confirmation -> not flagged" \
95
+ "$(mk_transcript '{"type":"user","message":{"role":"user","content":"yes"}}' "$DIRECTIVE_ATTACH" "$ASSISTANT_TEXT")"
96
+
97
+ # Synthetic (isMeta) user rows that Claude Code injects AFTER the real prompt:
98
+ # a skill body and a system-reminder. The boundary must NOT land on these.
99
+ META_SKILL='{"type":"user","isMeta":true,"message":{"role":"user","content":[{"type":"text","text":"Base directory for this skill: /x/y. Follow the instructions."}]}}'
100
+ META_REMINDER='{"type":"user","isMeta":true,"message":{"role":"user","content":[{"type":"text","text":"<system-reminder>The task tools were not used recently.</system-reminder>"}]}}'
101
+ # WhatsApp/native channel inbound: isMeta:true AND channel-wrapped — this IS the prompt.
102
+ CHANNEL_PROMPT='{"type":"user","isMeta":true,"message":{"role":"user","content":"<channel source=\"whatsapp-channel\"> how do I connect a domain </channel>"}}'
103
+
104
+ # 10. Channel-wrapped non-trivial prompt (isMeta, as on the Pi) -> flagged.
105
+ assert_flagged "isMeta channel-wrapped prompt + no route -> flagged" \
106
+ "$(mk_transcript "$CHANNEL_PROMPT" "$DIRECTIVE_ATTACH" "$ASSISTANT_TEXT")"
107
+
108
+ # 10b. Real prompt followed by isMeta skill/reminder injections -> still flagged
109
+ # (boundary stays on the real prompt; the slice is not truncated). Regression for
110
+ # the boundary bug where the last text-bearing user row was a meta injection.
111
+ assert_flagged "prompt then isMeta injections + no route -> flagged (boundary holds)" \
112
+ "$(mk_transcript "$USER_PROMPT" "$DIRECTIVE_ATTACH" "$ASSISTANT_TEXT" "$META_SKILL" "$META_REMINDER")"
113
+
114
+ # 10c. A route taken before the trailing isMeta injections -> NOT flagged. Proves
115
+ # the slice still reaches the tool_use rather than stopping at the meta boundary.
116
+ assert_not_flagged "route then isMeta injections -> not flagged (slice not truncated)" \
117
+ "$(mk_transcript "$USER_PROMPT" "$DIRECTIVE_ATTACH" "$ASSISTANT_AGENT" "$META_SKILL")"
118
+
119
+ # 10d. Slash-command prompt followed by an isMeta skill body -> not flagged. The
120
+ # boundary must read the slash prompt, not the non-slash injected body.
121
+ assert_not_flagged "slash prompt then isMeta skill body -> not flagged" \
122
+ "$(mk_transcript '{"type":"user","message":{"role":"user","content":"/insight"}}' "$DIRECTIVE_ATTACH" "$ASSISTANT_TEXT" "$META_SKILL")"
123
+
124
+ # 11. Fail-open: missing transcript_path -> exit 0, no output.
125
+ fo_out=$(printf '{"session_id":"x"}' | bash "$HOOK" 2>&1); fo_exit=$?
126
+ if [[ "$fo_exit" -eq 0 && -z "$fo_out" ]]; then
127
+ echo "PASS: missing transcript_path -> exit 0, no output (fail-open)"; PASS=$((PASS+1))
128
+ else
129
+ echo "FAIL: fail-open wrong (exit=$fo_exit out=$fo_out)" >&2; FAIL=$((FAIL+1))
130
+ fi
131
+
132
+ # 12. Fail-open: nonexistent transcript path -> exit 0, no output.
133
+ ne_out=$(printf '{"transcript_path":"/nonexistent/x.jsonl","session_id":"x"}' | bash "$HOOK" 2>&1); ne_exit=$?
134
+ if [[ "$ne_exit" -eq 0 && -z "$ne_out" ]]; then
135
+ echo "PASS: nonexistent transcript -> exit 0, no output (fail-open)"; PASS=$((PASS+1))
136
+ else
137
+ echo "FAIL: nonexistent-transcript fail-open wrong (exit=$ne_exit out=$ne_out)" >&2; FAIL=$((FAIL+1))
138
+ fi
139
+
140
+ # 13. Durable log: flag lands in $LOG_DIR/prompt-optimiser-directive.log.
141
+ LD=$(mktemp -d)
142
+ t13=$(mk_transcript "$USER_PROMPT" "$DIRECTIVE_ATTACH" "$ASSISTANT_TEXT")
143
+ printf '{"transcript_path":"%s","session_id":"sess12345678"}' "$t13" | LOG_DIR="$LD" bash "$HOOK" >/dev/null 2>&1
144
+ if grep -q "no-route-taken" "$LD/prompt-optimiser-directive.log" 2>/dev/null \
145
+ && grep -q "session=sess1234" "$LD/prompt-optimiser-directive.log" 2>/dev/null; then
146
+ echo "PASS: flag appended to durable log with session id"; PASS=$((PASS+1))
147
+ else
148
+ echo "FAIL: durable flag missing (log=$(cat "$LD/prompt-optimiser-directive.log" 2>/dev/null))" >&2; FAIL=$((FAIL+1))
149
+ fi
150
+ rm -rf "$LD" "$t13"
151
+
152
+ echo "----"
153
+ echo "PASS=$PASS FAIL=$FAIL"
154
+ [[ "$FAIL" -eq 0 ]]
@@ -92,6 +92,16 @@ if printf '%s' "$ctx" | grep -qi "outcome" \
92
92
  else
93
93
  echo "FAIL: outcome-only-brief contract missing from ladder" >&2; FAIL=$((FAIL+1))
94
94
  fi
95
+ # Capability-question clause (Task 719): capability/how-to/config questions are
96
+ # owned work, answered from the owning specialist or plugin tool/reference,
97
+ # never from training memory.
98
+ if printf '%s' "$ctx" | grep -qi "owned work" \
99
+ && printf '%s' "$ctx" | grep -qi "training memory" \
100
+ && printf '%s' "$ctx" | grep -qi "do you have instructions"; then
101
+ echo "PASS: capability-question clause present in directive"; PASS=$((PASS+1))
102
+ else
103
+ echo "FAIL: capability-question clause missing from directive" >&2; FAIL=$((FAIL+1))
104
+ fi
95
105
 
96
106
  # Case 5 — ADMIN-SKILLS.md absent: fail-open. exit 0, ladder still injected,
97
107
  # and the missing= breadcrumb on stderr (fail-open must be visible).
@@ -107,6 +117,20 @@ else
107
117
  fi
108
118
  rm -rf "$WD"
109
119
 
120
+ # Case 6 — durable breadcrumb (Task 719). With LOG_DIR set and a session_id on
121
+ # stdin, the hook appends one `injected len=` line carrying the session id to
122
+ # $LOG_DIR/prompt-optimiser-directive.log, in addition to the stderr line.
123
+ LD=$(mktemp -d)
124
+ printf '%s' '{"prompt":"x","session_id":"sess-719-test"}' | LOG_DIR="$LD" bash "$HOOK" >/dev/null 2>&1
125
+ if [[ -f "$LD/prompt-optimiser-directive.log" ]] \
126
+ && grep -q "prompt-optimiser-directive] injected len=" "$LD/prompt-optimiser-directive.log" \
127
+ && grep -q "session=sess-719-test" "$LD/prompt-optimiser-directive.log"; then
128
+ echo "PASS: durable breadcrumb appended with session id"; PASS=$((PASS+1))
129
+ else
130
+ echo "FAIL: durable breadcrumb missing (log=$(cat "$LD/prompt-optimiser-directive.log" 2>/dev/null))" >&2; FAIL=$((FAIL+1))
131
+ fi
132
+ rm -rf "$LD"
133
+
110
134
  echo "----"
111
135
  echo "PASS=$PASS FAIL=$FAIL"
112
136
  [[ "$FAIL" -eq 0 ]]
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env bash
2
+ # Stop hook (Task 719): after the admin agent finishes a turn, flag the turn when
3
+ # the per-turn routing directive fired, the turn was non-trivial, and NO route was
4
+ # taken — i.e. the agent answered from memory instead of dispatching a specialist,
5
+ # loading a skill, or calling the owning plugin's tool. This is the standing
6
+ # compliance signal the directive hook (prompt-optimiser-directive.sh) cannot
7
+ # self-emit, because "was a route taken" is only knowable after the turn ends.
8
+ #
9
+ # Classification (all derived from the just-finished turn in the transcript):
10
+ # directive fired = the marker "PROMPT-OPTIMISER DIRECTIVE" appears in the turn
11
+ # slice (robust to CC-version differences in how the
12
+ # UserPromptSubmit additionalContext is recorded: a
13
+ # type:"attachment"/hook_success on 2.1.x, a
14
+ # type:"hook_additional_context" on the Pi).
15
+ # route taken = the turn contains a tool_use named Agent, Skill, ToolSearch,
16
+ # or matching mcp__*.
17
+ # trivial = the prompt is a slash-command or a one-word confirmation.
18
+ # "Direct continuation" is NOT detectable from the transcript and is a stated
19
+ # known limitation, not guessed.
20
+ #
21
+ # Emits, only on (directive fired AND not trivial AND no route):
22
+ # <ts> [prompt-optimiser-compliance] directive-fired no-route-taken session=<id8> prompt="<clip>"
23
+ # to $LOG_DIR/prompt-optimiser-directive.log (same log as the fired breadcrumb,
24
+ # so the two interleave into one greppable timeline) and to stderr.
25
+ #
26
+ # Input (stdin, Stop contract): { "transcript_path": "...", "session_id": "...", ... }
27
+ # Fail-open everywhere: no python3, no transcript_path, unreadable transcript, or
28
+ # any parse error -> exit 0 with no output. A Stop hook must never block a turn.
29
+
30
+ set -uo pipefail
31
+
32
+ INPUT=$(cat 2>/dev/null || true)
33
+ command -v python3 >/dev/null 2>&1 || exit 0
34
+
35
+ RESULT=$(printf '%s' "$INPUT" | python3 -c '
36
+ import json, sys, os, datetime
37
+
38
+ def fail_open():
39
+ sys.exit(0)
40
+
41
+ try:
42
+ inp = json.load(sys.stdin)
43
+ except Exception:
44
+ fail_open()
45
+
46
+ tp = inp.get("transcript_path")
47
+ session = inp.get("session_id") or "?"
48
+ if not isinstance(tp, str) or not tp or not os.path.exists(tp):
49
+ fail_open()
50
+
51
+ rows = []
52
+ try:
53
+ with open(tp, "r", errors="replace") as f:
54
+ for line in f:
55
+ line = line.strip()
56
+ if not line:
57
+ continue
58
+ try:
59
+ rows.append(json.loads(line))
60
+ except Exception:
61
+ continue
62
+ except OSError:
63
+ fail_open()
64
+
65
+ # Flatten a message.content into plain text; mark whether it is purely tool_result
66
+ # (a tool output row, not a user prompt).
67
+ def flatten(content):
68
+ if isinstance(content, str):
69
+ return content, False
70
+ if isinstance(content, list):
71
+ only_tool_result = bool(content)
72
+ parts = []
73
+ for b in content:
74
+ if not isinstance(b, dict):
75
+ only_tool_result = False
76
+ if isinstance(b, str):
77
+ parts.append(b)
78
+ continue
79
+ t = b.get("type")
80
+ if t != "tool_result":
81
+ only_tool_result = False
82
+ if t == "text" and isinstance(b.get("text"), str):
83
+ parts.append(b["text"])
84
+ return " ".join(parts), only_tool_result
85
+ return "", False
86
+
87
+ # A row is synthetic (not a genuine prompt) when Claude Code stamps it isMeta /
88
+ # isSidechain / etc. Claude Code injects such user rows AFTER the real prompt
89
+ # within the same turn — skill bodies, system-reminders, additionalContext
90
+ # echoes — so a naive "last text-bearing user row" would land on the injection,
91
+ # truncate the slice, and read the injected body as the prompt.
92
+ def is_synthetic(r):
93
+ return any(r.get(k) for k in ("isMeta", "isSidechain", "isCompactSummary", "isVisibleInTranscriptOnly"))
94
+
95
+ # Boundary: the last user row that carries a genuine prompt. A genuine prompt is
96
+ # either a non-synthetic user turn (typed claude.ai) OR a synthetic turn whose
97
+ # text is channel-wrapped — WhatsApp/native channels stamp the inbound prompt
98
+ # isMeta:true and wrap it in <channel ...>, and that inbound IS the prompt. Every
99
+ # other synthetic row (skill body, system-reminder) is skipped. The turn slice is
100
+ # everything from the boundary to EOF.
101
+ boundary = None
102
+ prompt_text = ""
103
+ for i, r in enumerate(rows):
104
+ if not isinstance(r, dict) or r.get("type") != "user":
105
+ continue
106
+ msg = r.get("message") if isinstance(r.get("message"), dict) else {}
107
+ text, only_tool_result = flatten(msg.get("content"))
108
+ if only_tool_result:
109
+ continue
110
+ stripped = text.strip()
111
+ if not stripped:
112
+ continue
113
+ if is_synthetic(r) and not stripped.startswith("<channel"):
114
+ continue
115
+ boundary = i
116
+ prompt_text = stripped
117
+
118
+ if boundary is None:
119
+ fail_open()
120
+
121
+ slice_rows = rows[boundary:]
122
+
123
+ # Strip a leading <channel ...>...</channel> wrapper to reach the inner prompt for
124
+ # the trivial check (WhatsApp/native-channel prompts arrive wrapped).
125
+ inner = prompt_text
126
+ if inner.startswith("<channel"):
127
+ end = inner.find(">")
128
+ close = inner.rfind("</channel>")
129
+ if end != -1 and close != -1 and close > end:
130
+ inner = inner[end + 1:close].strip()
131
+
132
+ # Trivial-turn filter.
133
+ is_slash = inner.startswith("/")
134
+ tokens = inner.split()
135
+ is_one_word = len(tokens) <= 1
136
+
137
+ # Directive fired = marker present anywhere in the slice (any record shape).
138
+ marker = "PROMPT-OPTIMISER DIRECTIVE"
139
+ directive_fired = any(marker in json.dumps(r, ensure_ascii=False) for r in slice_rows)
140
+
141
+ # Route taken = an assistant tool_use with a routing tool name.
142
+ ROUTE_TOOLS = ("Agent", "Skill", "ToolSearch")
143
+ def is_route(name):
144
+ return name in ROUTE_TOOLS or (isinstance(name, str) and name.startswith("mcp__"))
145
+
146
+ route_taken = False
147
+ for r in slice_rows:
148
+ if not isinstance(r, dict) or r.get("type") != "assistant":
149
+ continue
150
+ msg = r.get("message") if isinstance(r.get("message"), dict) else {}
151
+ content = msg.get("content")
152
+ if isinstance(content, list):
153
+ for b in content:
154
+ if isinstance(b, dict) and b.get("type") == "tool_use" and is_route(b.get("name")):
155
+ route_taken = True
156
+ break
157
+ if route_taken:
158
+ break
159
+
160
+ if directive_fired and not is_slash and not is_one_word and not route_taken:
161
+ clip = prompt_text.replace("\n", " ")[:80]
162
+ ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
163
+ sid8 = session[:8] if isinstance(session, str) else "?"
164
+ print("%s [prompt-optimiser-compliance] directive-fired no-route-taken session=%s prompt=\"%s\"" % (ts, sid8, clip))
165
+ ' 2>/dev/null) || exit 0
166
+
167
+ [ -n "$RESULT" ] || exit 0
168
+
169
+ # Durable + stderr. Fail-open on an unwritable/absent LOG_DIR.
170
+ if [ -n "${LOG_DIR:-}" ] && [ -d "$LOG_DIR" ]; then
171
+ printf '%s\n' "$RESULT" >> "$LOG_DIR/prompt-optimiser-directive.log" 2>/dev/null || true
172
+ fi
173
+ printf '%s\n' "$RESULT" >&2
174
+ exit 0
@@ -32,8 +32,9 @@
32
32
 
33
33
  set -uo pipefail
34
34
 
35
- # Drain stdin; the directive does not depend on the prompt text.
36
- cat >/dev/null 2>&1 || true
35
+ # Capture stdin: the directive does not depend on the prompt text, but the
36
+ # UserPromptSubmit payload carries session_id, used for the durable breadcrumb.
37
+ HOOK_INPUT=$(cat 2>/dev/null || true)
37
38
 
38
39
  # Fail-open if the JSON encoder is unavailable.
39
40
  command -v python3 >/dev/null 2>&1 || exit 0
@@ -65,6 +66,8 @@ PROMPT-OPTIMISER DIRECTIVE (standing)
65
66
  Before acting on this turn's request, run the prompt-optimiser skill on the user's raw prompt in its in-session restatement mode, then treat the restated task as the authoritative statement of what was asked and proceed from it. Set aside your own first reading of the raw wording; the restatement governs. The raw prompt stays in context for reference only.
66
67
  Skip the restatement when the prompt is a one-word confirmation, a slash-command invocation, or a direct continuation of the immediately preceding turn; restatement adds nothing in those cases.
67
68
 
69
+ CAPABILITY QUESTIONS ARE OWNED WORK. A question about what this platform can do or how to do it — "how do I set up X", "do you have instructions for X", any configuration or how-to about a platform feature — is answered from the owning specialist or the owning plugin's tool or reference, never from your training memory. Your prior on the platform's own features is stale relative to what shipped, so memory is the wrong source here even when it feels confident. Route the question (dispatch the owning specialist, or skill-load / call the owning plugin's tool or reference) and source the answer from what that route returns. Freestyle tier 3 is not a route for these: "there is no instruction surface for this" is itself something you establish by routing and finding nothing, not by recalling.
70
+
68
71
  ROUTING LADDER (apply to the restated task, in this order):
69
72
  1) Delegate to a specialist. If an installed specialist owns this deliverable's type, dispatch it with the Agent tool and the matching subagent_type. Ownership covers origination, not just the final render: you may not author the substance here and hand the specialist only the formatting, the voice pass, or the PDF step. The brief states the outcome (what is true when the specialist returns) plus the binding constraints (account, file scope, deadline, acceptance check). It never names lines, anchors, or literal text to insert; deriving those is the specialist's job. Wrong shape: naming the file, the line range, and the exact text to drop in. Right shape: "on the realagent-code site, the footer respects iOS safe-area-inset-bottom on mobile; the site's CSS is the only file in scope." Installed specialists:
70
73
  DIRECTIVE_EOF
@@ -93,5 +96,22 @@ print(json.dumps({
93
96
 
94
97
  [ -n "$OUT" ] || exit 0
95
98
  printf '%s\n' "$OUT"
99
+
100
+ # Durable breadcrumb: capture the per-turn injection in $LOG_DIR so "directive
101
+ # fired on this turn" is greppable for compliance review, not only on stderr
102
+ # (Task 719). Fail-open: no LOG_DIR, missing dir, or unwritable -> skip the file;
103
+ # the stderr line below still emits. session_id is parsed from the captured
104
+ # stdin; absent -> "?".
105
+ if [ -n "${LOG_DIR:-}" ] && [ -d "$LOG_DIR" ]; then
106
+ SID=$(printf '%s' "$HOOK_INPUT" | python3 -c 'import json,sys
107
+ try:
108
+ print(json.load(sys.stdin).get("session_id","") or "")
109
+ except Exception:
110
+ print("")' 2>/dev/null)
111
+ TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
112
+ printf '%s [prompt-optimiser-directive] injected len=%s session=%s\n' \
113
+ "$TS" "${#DIRECTIVE}" "${SID:-?}" >> "$LOG_DIR/prompt-optimiser-directive.log" 2>/dev/null || true
114
+ fi
115
+
96
116
  echo "[prompt-optimiser-directive] injected len=${#DIRECTIVE}" >&2
97
117
  exit 0
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:137ecdb03fdbb733f6150e0d931f4e5f30bf7daafcf738398d8c2768b868936e
4
+ content-hash: sha256:5dae80dcc0e3d36e755dcf26570c4d7c8893b6198b8cd769b3e5c6c1c79169f5
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -1035,7 +1035,7 @@ Each installation has its own Cloudflare account. The **tunnel** sign-in is OAut
1035
1035
  | **Domain scope** (which zones the operator can route) | The operator picks the zone in the dashboard during OAuth or names it in chat; the agent can also enumerate zones via the API with a minted read-scoped token. |
1036
1036
  | **Local tunnel state** | `~/{configDir}/cloudflared/` — `cert.pem`, `<UUID>.json`, `config.yml`, `alias-domains.json`. |
1037
1037
 
1038
- Tunnel auth on the operator-owned path (Mode A) is the OAuth cert (`cert.pem`); API operations use a narrow token the agent mints from your master token. To switch Cloudflare accounts, the agent runs the reset flow from `plugins/cloudflare/references/reset-guide.md` (deletes the cert and every tunnel on the current account), then the manual-setup flow again — `cloudflared tunnel login` picks a fresh account when you sign in.
1038
+ Tunnel auth on the operator-owned path (Mode A) is the OAuth cert (`cert.pem`); API operations use a narrow token the agent mints from your master token. That master may be account-scoped (`cfat_…`) or user-scoped (`cfut_…`); the agent detects the type and routes API endpoints accordingly (the account vs user token endpoints) — see `plugins/cloudflare/references/api.md`. To switch Cloudflare accounts, the agent runs the reset flow from `plugins/cloudflare/references/reset-guide.md` (deletes the cert and every tunnel on the current account), then the manual-setup flow again — `cloudflared tunnel login` picks a fresh account when you sign in.
1039
1039
 
1040
1040
  ## Setup flow
1041
1041
 
@@ -1728,13 +1728,13 @@ The Telegram plugin connects Maxy to a Telegram bot. Once set up, you can:
1728
1728
 
1729
1729
  ### Step 2: Connect the plugin
1730
1730
 
1731
- Tell Maxy: "Set up Telegram" or "Configure the Telegram bot."
1731
+ Tell Maxy: "Set up Telegram" or "Configure the Telegram bot." Setup runs entirely in the admin chat — there is no settings page.
1732
1732
 
1733
- Maxy will ask for your bot token, then save it and activate the plugin. The bot is now connected.
1733
+ Maxy asks for your bot token, then asks whether this is an **admin bot** (only your numeric Telegram IDs may message it) or a **public bot** (customer-facing, with a DM policy). It verifies the token, registers the bot's webhook on your brand's edge, stores the token under your brand's config (never in a shared system directory), and for a public bot — sets the agent that will answer. The bot is now connected.
1734
1734
 
1735
1735
  ### Step 3: Start the bot
1736
1736
 
1737
- In Telegram, open your bot and send `/start`. The bot is now active and listening.
1737
+ In Telegram, open your bot and send `/start`, then a message. Maxy receives it and replies in the same chat. Admin-bot messages from your registered IDs reach the admin agent; public-bot messages reach the public agent you selected.
1738
1738
 
1739
1739
  ## Sending Messages via Maxy
1740
1740