@rubytech/create-maxy-code 0.1.281 → 0.1.282
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +2 -1
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +154 -0
- package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-directive.test.sh +24 -0
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +174 -0
- package/payload/platform/plugins/admin/hooks/prompt-optimiser-directive.sh +22 -2
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
- package/payload/platform/plugins/business-assistant/skills/e-sign/SKILL.md +378 -56
- package/payload/platform/plugins/cloudflare/references/api.md +35 -1
- package/payload/platform/plugins/cloudflare/references/d1-data-capture.md +2 -0
- package/payload/platform/plugins/docs/references/cloudflare.md +1 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-resolve.test.js +25 -1
- package/payload/platform/plugins/email/mcp/dist/__tests__/attachment-resolve.test.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.d.ts +7 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.js +9 -0
- package/payload/platform/plugins/email/mcp/dist/lib/attachment-resolve.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js +6 -2
- package/payload/platform/plugins/email/mcp/dist/tools/email-ingest.js.map +1 -1
- package/payload/platform/plugins/prompt-optimiser/skills/prompt-optimiser/SKILL.md +1 -0
- package/payload/platform/plugins/whatsapp/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/whatsapp/PLUGIN.md +7 -11
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js +10 -56
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +26 -2
- package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +9 -14
- package/payload/platform/scripts/setup-account.sh +7 -0
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +0 -2
- package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +63 -3
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +6 -0
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +14 -0
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/server.js +5 -3
- package/payload/platform/services/whatsapp-channel/dist/server.js.map +1 -1
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/server/server.js +55 -236
package/package.json
CHANGED
|
@@ -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
|
-
#
|
|
36
|
-
|
|
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:
|
|
4
|
+
content-hash: sha256:add28451435be577d674216c86cd03078f5b25d03ce6def9fc61962971c8e9ed
|
|
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
|
|