@rubytech/create-maxy-code 0.1.116 → 0.1.118
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 +105 -0
- package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/platform/plugins/admin/PLUGIN.md +1 -0
- package/payload/platform/plugins/aeo/.claude-plugin/plugin.json +17 -0
- package/payload/platform/plugins/aeo/PLUGIN.md +81 -0
- package/payload/platform/plugins/aeo/mcp/package.json +22 -0
- package/payload/platform/plugins/aeo/mcp/vitest.config.ts +9 -0
- package/payload/platform/plugins/aeo/skills/structured-answer/SKILL.md +53 -0
- package/payload/platform/plugins/docs/PLUGIN.md +1 -0
- package/payload/platform/plugins/docs/references/admin-session.md +1 -1
- package/payload/platform/plugins/docs/references/aeo.md +87 -0
- package/payload/platform/plugins/docs/references/deployment.md +1 -1
- package/payload/platform/plugins/email/PLUGIN.md +6 -0
- package/payload/platform/plugins/email/skills/email-composition/SKILL.md +180 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +5 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.js +62 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/label-origin-gate.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js +62 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.d.ts +18 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.js +29 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/label-origin-gate.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-emit.test.js +2 -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 -1
- 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-update.d.ts +3 -0
- 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 +9 -1
- 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 +8 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +4 -0
- package/payload/platform/plugins/memory/references/schema-base.md +5 -1
- package/payload/platform/scripts/smoke-boot-services.sh +30 -2
- package/payload/platform/templates/specialists/agents/signal-detector.md +1 -1
- package/payload/server/public/assets/{admin-DloKrK2i.js → admin-D6IfAzYY.js} +1 -1
- package/payload/server/public/assets/{data-NMvGx6jm.js → data-BGbQyufe.js} +1 -1
- package/payload/server/public/assets/{graph-CN60KyTy.js → graph-D-1lRTeL.js} +1 -1
- package/payload/server/public/assets/{graph-labels-h39S93Xo.js → graph-labels-BRXJHNYE.js} +1 -1
- package/payload/server/public/assets/{page-B0iMmY6t.js → page-B4oirCvn.js} +1 -1
- package/payload/server/public/assets/{page-CFRU-BdJ.js → page-FmJ7PIYx.js} +1 -1
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/server.js +14 -0
package/package.json
CHANGED
|
@@ -1100,3 +1100,108 @@ FOR (h:CloudflareHostname) REQUIRE (h.accountId, h.hostnameValue) IS UNIQUE;
|
|
|
1100
1100
|
|
|
1101
1101
|
CREATE INDEX cloudflare_hostname_account IF NOT EXISTS
|
|
1102
1102
|
FOR (h:CloudflareHostname) ON (h.accountId);
|
|
1103
|
+
|
|
1104
|
+
// ----------------------------------------------------------
|
|
1105
|
+
<<<<<<< HEAD
|
|
1106
|
+
// Estate-agent ontology (Task 358) — Listing-centric model
|
|
1107
|
+
// documented at platform/plugins/memory/references/schema-estate-agent.md.
|
|
1108
|
+
//
|
|
1109
|
+
// These declarations exist primarily so the memory-write validator's
|
|
1110
|
+
// parseLabelsFromSchemaCypher() registers each label on a fresh deploy
|
|
1111
|
+
// where no node of the label has yet landed — without them, the first
|
|
1112
|
+
// :Listing write from the listing-curator agent is rejected as "unknown
|
|
1113
|
+
// label" against an empty db.labels() set.
|
|
1114
|
+
//
|
|
1115
|
+
// Idempotency contracts mirror schema-estate-agent.md:
|
|
1116
|
+
// :Listing / :Viewing / :Offer — (accountId, sourceSystem, sourceId)
|
|
1117
|
+
// :Property — (accountId, slug)
|
|
1118
|
+
// :ImageObject — (accountId, listingSlug, url) for the
|
|
1119
|
+
// estate-agent variant, (accountId, name,
|
|
1120
|
+
// contentUrl) for the brand-asset variant.
|
|
1121
|
+
// Because the two writers use different
|
|
1122
|
+
// natural keys, only an accountId index is
|
|
1123
|
+
// declared at the schema level — uniqueness
|
|
1124
|
+
// is enforced at write time by the curator
|
|
1125
|
+
// / brand-asset writer's MERGE clause.
|
|
1126
|
+
// ----------------------------------------------------------
|
|
1127
|
+
|
|
1128
|
+
CREATE CONSTRAINT listing_account_source_unique IF NOT EXISTS
|
|
1129
|
+
FOR (l:Listing) REQUIRE (l.accountId, l.sourceSystem, l.sourceId) IS UNIQUE;
|
|
1130
|
+
|
|
1131
|
+
CREATE INDEX listing_account IF NOT EXISTS
|
|
1132
|
+
FOR (l:Listing) ON (l.accountId);
|
|
1133
|
+
|
|
1134
|
+
CREATE INDEX listing_status IF NOT EXISTS
|
|
1135
|
+
FOR (l:Listing) ON (l.status);
|
|
1136
|
+
|
|
1137
|
+
CREATE INDEX listing_type IF NOT EXISTS
|
|
1138
|
+
FOR (l:Listing) ON (l.listingType);
|
|
1139
|
+
|
|
1140
|
+
CREATE VECTOR INDEX listing_embedding IF NOT EXISTS
|
|
1141
|
+
FOR (l:Listing) ON (l.embedding)
|
|
1142
|
+
OPTIONS {
|
|
1143
|
+
indexConfig: {
|
|
1144
|
+
`vector.dimensions`: 768,
|
|
1145
|
+
`vector.similarity_function`: 'cosine'
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
CREATE CONSTRAINT property_account_slug_unique IF NOT EXISTS
|
|
1150
|
+
FOR (p:Property) REQUIRE (p.accountId, p.slug) IS UNIQUE;
|
|
1151
|
+
|
|
1152
|
+
CREATE INDEX property_account IF NOT EXISTS
|
|
1153
|
+
FOR (p:Property) ON (p.accountId);
|
|
1154
|
+
|
|
1155
|
+
CREATE CONSTRAINT viewing_account_source_unique IF NOT EXISTS
|
|
1156
|
+
FOR (v:Viewing) REQUIRE (v.accountId, v.sourceSystem, v.sourceId) IS UNIQUE;
|
|
1157
|
+
|
|
1158
|
+
CREATE INDEX viewing_account IF NOT EXISTS
|
|
1159
|
+
FOR (v:Viewing) ON (v.accountId);
|
|
1160
|
+
|
|
1161
|
+
CREATE CONSTRAINT offer_account_source_unique IF NOT EXISTS
|
|
1162
|
+
FOR (o:Offer) REQUIRE (o.accountId, o.sourceSystem, o.sourceId) IS UNIQUE;
|
|
1163
|
+
|
|
1164
|
+
CREATE INDEX offer_account IF NOT EXISTS
|
|
1165
|
+
FOR (o:Offer) ON (o.accountId);
|
|
1166
|
+
|
|
1167
|
+
CREATE INDEX image_object_account IF NOT EXISTS
|
|
1168
|
+
FOR (i:ImageObject) ON (i.accountId);
|
|
1169
|
+
|
|
1170
|
+
// :PostalAddress (schema:PostalAddress) — shared address nodes documented in
|
|
1171
|
+
// schema-base.md. Address rows are deliberately accountId-free (one address
|
|
1172
|
+
// can be shared across customers / orders / listings); writers MERGE on the
|
|
1173
|
+
// formatted address triple. Declared here so a fresh deploy registers the
|
|
1174
|
+
// label before the first :PostalAddress write lands.
|
|
1175
|
+
CREATE INDEX postal_address_postcode IF NOT EXISTS
|
|
1176
|
+
FOR (a:PostalAddress) ON (a.postalCode);
|
|
1177
|
+
=======
|
|
1178
|
+
// AEOAudit — Answer Engine Optimisation audit history (Task 354).
|
|
1179
|
+
// Written by the aeo plugin's aeo-audit-page tool when invoked
|
|
1180
|
+
// with persist=true. Append-only audit trail; each invocation
|
|
1181
|
+
// produces a new node. Linked via
|
|
1182
|
+
// (KnowledgeDocument)-[:HAS_AEO_AUDIT]->(AEOAudit) when the audit
|
|
1183
|
+
// targets a stored document, otherwise standalone keyed on
|
|
1184
|
+
// (accountId, target, runAt).
|
|
1185
|
+
//
|
|
1186
|
+
// Properties: accountId, auditId (uuid), target (url or
|
|
1187
|
+
// '(inline html)'), score (0-100), heuristicsJson (serialized
|
|
1188
|
+
// per-heuristic result), runAt, createdByAgent, createdBySession,
|
|
1189
|
+
// createdAt.
|
|
1190
|
+
//
|
|
1191
|
+
// Monitor-related labels (:AEOPrompt, :AEOCheck, :AEOCitation) are
|
|
1192
|
+
// intentionally NOT declared in v1 — the multi-engine monitor is
|
|
1193
|
+
// filed as a follow-up task and the schema will land with it.
|
|
1194
|
+
// ----------------------------------------------------------
|
|
1195
|
+
|
|
1196
|
+
CREATE CONSTRAINT aeo_audit_id_unique IF NOT EXISTS
|
|
1197
|
+
FOR (a:AEOAudit) REQUIRE a.auditId IS UNIQUE;
|
|
1198
|
+
|
|
1199
|
+
CREATE INDEX aeo_audit_account IF NOT EXISTS
|
|
1200
|
+
FOR (a:AEOAudit) ON (a.accountId);
|
|
1201
|
+
|
|
1202
|
+
CREATE INDEX aeo_audit_target IF NOT EXISTS
|
|
1203
|
+
FOR (a:AEOAudit) ON (a.target);
|
|
1204
|
+
|
|
1205
|
+
CREATE INDEX aeo_audit_run_at IF NOT EXISTS
|
|
1206
|
+
FOR (a:AEOAudit) ON (a.runAt);
|
|
1207
|
+
>>>>>>> worktree-task-354-aeo-plugin
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: admin
|
|
3
|
+
surface: platform
|
|
3
4
|
description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, file-attach, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective) for managing the Maxy platform."
|
|
4
5
|
tools:
|
|
5
6
|
- name: system-status
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aeo",
|
|
3
|
+
"description": "Answer Engine Optimisation. Emits schema.org JSON-LD for typed graph entities, generates llms.txt + llms-full.txt for the site, and audits pages against eight AEO heuristics with one rewrite suggestion per fail. Writes :AEOAudit history nodes when audits are persisted.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "Rubytech LLC"
|
|
7
|
+
},
|
|
8
|
+
"mcpServers": {
|
|
9
|
+
"aeo": {
|
|
10
|
+
"type": "stdio",
|
|
11
|
+
"command": "node",
|
|
12
|
+
"args": [
|
|
13
|
+
"${CLAUDE_PLUGIN_ROOT}/mcp/dist/index.js"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aeo
|
|
3
|
+
description: "Answer Engine Optimisation. Emits schema.org JSON-LD for typed graph entities, generates llms.txt + llms-full.txt for the site, and audits pages against eight AEO heuristics with one rewrite suggestion per fail. Writes :AEOAudit history nodes when audits are persisted."
|
|
4
|
+
tools:
|
|
5
|
+
- name: aeo-emit-jsonld
|
|
6
|
+
publicAllowlist: false
|
|
7
|
+
adminAllowlist: true
|
|
8
|
+
- name: aeo-write-llms-txt
|
|
9
|
+
publicAllowlist: false
|
|
10
|
+
adminAllowlist: true
|
|
11
|
+
- name: aeo-audit-page
|
|
12
|
+
publicAllowlist: false
|
|
13
|
+
adminAllowlist: true
|
|
14
|
+
skills:
|
|
15
|
+
- skills/structured-answer/SKILL.md
|
|
16
|
+
always: false
|
|
17
|
+
embed: false
|
|
18
|
+
metadata: {"platform":{}}
|
|
19
|
+
mcp:
|
|
20
|
+
command: node
|
|
21
|
+
args:
|
|
22
|
+
- ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
|
|
23
|
+
- ${PLATFORM_ROOT}/plugins/aeo/mcp/dist/index.js
|
|
24
|
+
env:
|
|
25
|
+
MCP_SPAWN_TEE_NAME: aeo
|
|
26
|
+
LOG_DIR: ${LOG_DIR}
|
|
27
|
+
PLATFORM_ROOT: ${PLATFORM_ROOT}
|
|
28
|
+
ACCOUNT_ID: ${ACCOUNT_ID}
|
|
29
|
+
SESSION_ID: ${SESSION_ID}
|
|
30
|
+
AGENT_SLUG: ${AGENT_SLUG}
|
|
31
|
+
mcp-manifest: auto
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# AEO (Answer Engine Optimisation)
|
|
35
|
+
|
|
36
|
+
This plugin makes the platform's content legible to answer engines — Claude, ChatGPT, Perplexity, Google AI Overviews, Bing Copilot. Classical SEO ranks links; AEO shapes the page so the answer-engine extractor lifts the right answer.
|
|
37
|
+
|
|
38
|
+
## The three surfaces
|
|
39
|
+
|
|
40
|
+
1. **JSON-LD** (`aeo-emit-jsonld`). Every typed graph entity that backs a page can be projected to a `<script type="application/ld+json">` block. The mapper covers `Organization`, `LocalBusiness`, `Person`, `Service`, `Product`, `Article`, `FAQPage`, `RealEstateListing`, `Event`. Other labels fall through to `schema:Thing` and are flagged as a doc gap.
|
|
41
|
+
|
|
42
|
+
2. **llms.txt** (`aeo-write-llms-txt`). The site emits `/llms.txt` (index of pages, one link per line) and `/llms-full.txt` (concatenated full text). Format follows the current draft at `https://llmstxt.org/`. Source is `KnowledgeDocument` nodes with a `url` property. Pages without a `url` are skipped and counted in the response.
|
|
43
|
+
|
|
44
|
+
3. **Audit** (`aeo-audit-page`). Run the eight-heuristic scorecard against a URL or raw HTML. Each heuristic returns pass / warn / fail with one rewrite suggestion per fail. Score is `100 * (passes + 0.5 * warns) / 8`. Pass `persist: true` with a `targetKnowledgeDocumentId` to record an `:AEOAudit` node linked back to the document.
|
|
45
|
+
|
|
46
|
+
## The eight heuristics
|
|
47
|
+
|
|
48
|
+
| Heuristic | Pass | Warn | Fail |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| `h1-present` | one `<h1>` | more than one | none |
|
|
51
|
+
| `jsonld-present` | one or more JSON-LD blocks, all parse | some parse | none, or all unparseable |
|
|
52
|
+
| `structured-answer` | first `<p>` after `<h1>` is ≤280 chars | first `<p>` is over 280 chars | no `<p>` after `<h1>` |
|
|
53
|
+
| `faq-section` | `FAQPage` JSON-LD on page | FAQ text but no JSON-LD | nothing FAQ-like |
|
|
54
|
+
| `meta-description` | 80–160 chars | present but outside range | absent |
|
|
55
|
+
| `canonical-url` | `<link rel="canonical">` present | — | absent |
|
|
56
|
+
| `og-tags` | `og:title` and `og:description` both set | — | one or both missing |
|
|
57
|
+
| `heading-hierarchy` | no level skips | — | h1→h3 or similar jump |
|
|
58
|
+
|
|
59
|
+
`structured-answer` is the highest-impact: answer engines lift the structured-answer span verbatim. A page can fail every other heuristic and still get cited if the first paragraph cleanly answers the question.
|
|
60
|
+
|
|
61
|
+
## Persistence model
|
|
62
|
+
|
|
63
|
+
The plugin only writes `:AEOAudit` nodes (when `persist: true`). It does not own the entities it reads — `KnowledgeDocument`, `Organization`, `Person`, etc. belong to the memory plugin. Edges written:
|
|
64
|
+
|
|
65
|
+
- `(:KnowledgeDocument)-[:HAS_AEO_AUDIT]->(:AEOAudit)`
|
|
66
|
+
|
|
67
|
+
The audit nodes carry `accountId`, `target` (URL or `(inline html)`), `score`, `heuristicsJson` (full result), `runAt`, `createdByAgent`, `createdBySession`. They form an append-only audit history; aggregate trend queries (score-over-time) walk this set.
|
|
68
|
+
|
|
69
|
+
## What this plugin does not do
|
|
70
|
+
|
|
71
|
+
- No LLM API calls. There is no monitor in v1. The "is my brand cited when a customer asks Claude / ChatGPT / Perplexity / Gemini" question depends on multi-engine answer harvesting that runs differently under maxy-code's no-API-key architecture, and is filed as a follow-up task.
|
|
72
|
+
- No automatic emission. `aeo-emit-jsonld` returns a script block; wiring it into the platform's page render path is per-renderer work, also filed as a follow-up.
|
|
73
|
+
- No publish-hook regeneration of `llms.txt`. The tool runs on demand. Hooking it into the publish event is a follow-up.
|
|
74
|
+
- No prose authoring. AEO audits structure; it does not rewrite content. Writer-craft owns voice.
|
|
75
|
+
|
|
76
|
+
## See also
|
|
77
|
+
|
|
78
|
+
- User guide: `platform/plugins/docs/references/aeo.md`
|
|
79
|
+
- Structured-answer template: `platform/plugins/aeo/skills/structured-answer/SKILL.md`
|
|
80
|
+
- Schema declaration: `platform/neo4j/schema.cypher` (search `AEOAudit`)
|
|
81
|
+
- Schema reference: `platform/plugins/memory/schema-base.md` (search `AEOAudit`)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maxy/aeo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/index.js",
|
|
10
|
+
"test": "vitest run"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
14
|
+
"neo4j-driver": "^5.28.1",
|
|
15
|
+
"zod": "^3.24.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"typescript": "^5.7.0",
|
|
19
|
+
"@types/node": "^22.0.0",
|
|
20
|
+
"vitest": "^4.1.2"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: structured-answer
|
|
3
|
+
description: Use when authoring or auditing a page whose primary purpose is to answer a single question — service pages, FAQ entries, "what is X" pages, comparison pages. Defines the page shape that answer engines lift verbatim. Triggers include "make this AEO-friendly", "structure this for Claude / ChatGPT", "audit this page for answer-engine pickup".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Structured-answer page template
|
|
7
|
+
|
|
8
|
+
Answer engines (Claude, ChatGPT, Perplexity, Google AI Overviews, Bing Copilot) lift a single short span when they cite a page. That span is almost always the first paragraph after the H1, when it is short and direct. Every other element on the page either supports the lift or gets ignored.
|
|
9
|
+
|
|
10
|
+
## The shape
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
<head>
|
|
14
|
+
<title>{question or topic} — {site name}</title>
|
|
15
|
+
<meta name="description" content="{80–160 char summary of the answer}">
|
|
16
|
+
<link rel="canonical" href="{this page's URL}">
|
|
17
|
+
<meta property="og:title" content="{question or topic}">
|
|
18
|
+
<meta property="og:description" content="{same as meta description}">
|
|
19
|
+
<script type="application/ld+json">{ emit via aeo-emit-jsonld }</script>
|
|
20
|
+
</head>
|
|
21
|
+
|
|
22
|
+
<body>
|
|
23
|
+
<h1>{question or topic}</h1>
|
|
24
|
+
<p>{≤280 characters. The answer. One sentence preferred. Names the entity, states the answer, ends on a fact.}</p>
|
|
25
|
+
|
|
26
|
+
<h2>{follow-up section}</h2>
|
|
27
|
+
<p>{details, examples, links}</p>
|
|
28
|
+
|
|
29
|
+
<h2>Frequently asked questions</h2>
|
|
30
|
+
<!-- emit FAQPage JSON-LD with 3–5 Question / Answer pairs -->
|
|
31
|
+
</body>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Why this works
|
|
35
|
+
|
|
36
|
+
- The H1 names the question; the first `<p>` is the answer. Engines treat that pair as a structured Q/A.
|
|
37
|
+
- The `<p>` is short enough to be lifted whole. Longer paragraphs get truncated mid-sentence.
|
|
38
|
+
- The JSON-LD repeats the structured data in a parser-friendly form so engines that index pre-render don't have to scrape the DOM.
|
|
39
|
+
- The FAQ section adds a second class of lift target — engines often cite an FAQ answer when the page's main question is too broad.
|
|
40
|
+
|
|
41
|
+
## What breaks the lift
|
|
42
|
+
|
|
43
|
+
- A multi-sentence first paragraph. The engine truncates and the citation reads like a fragment.
|
|
44
|
+
- Marketing prose before the answer ("In today's fast-moving world…"). The engine lifts the marketing prose, not the answer.
|
|
45
|
+
- The H1 stating the brand instead of the question. "Acme Plumbing" is not a question; "How much does a boiler service cost?" is.
|
|
46
|
+
- Missing JSON-LD. Engines that pre-render the page still index the DOM, but they down-weight pages without structured data.
|
|
47
|
+
- Skipped heading levels. Engines use heading hierarchy to decide which content is the answer and which is supporting detail.
|
|
48
|
+
|
|
49
|
+
## How to audit
|
|
50
|
+
|
|
51
|
+
Run `aeo-audit-page` against the URL or HTML. Eight heuristics return pass / warn / fail. Treat any `fail` as a blocker — the rewrite suggestion in the response is concrete. Treat `warn` as a polish target.
|
|
52
|
+
|
|
53
|
+
To record the audit on the document for trend analysis, pass `persist: true` with the document's `elementId` as `targetKnowledgeDocumentId`. The plugin writes an `:AEOAudit` node linked via `HAS_AEO_AUDIT`.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: docs
|
|
3
|
+
surface: platform
|
|
3
4
|
description: "Comprehensive user guide and platform documentation for Maxy. Load references when users ask how-to, setup, or troubleshooting questions. Also contains platform architecture references for admin use."
|
|
4
5
|
metadata: {"platform":{"always":true,"embed":["public","admin"],"pluginKey":"docs"}}
|
|
5
6
|
---
|
|
@@ -79,7 +79,7 @@ PreToolUse hook `platform/plugins/admin/hooks/askuserquestion-investigate-gate.s
|
|
|
79
79
|
|
|
80
80
|
## Spawn-with-`initialMessage` wrapper
|
|
81
81
|
|
|
82
|
-
The Hono route `POST /api/admin/claude-sessions` at [`platform/ui/server/routes/admin/claude-sessions.ts`](../../../ui/server/routes/admin/claude-sessions.ts) is the single canonical entry point for any caller — UI click handler or programmatic — that needs to open an admin PTY session with a first user prompt. Direct calls to the session-manager's `/spawn` are not allowed; the wrapper owns the per-spawn enrichment (`aboutOwner`, `dormantPlugins`, `activePlugins`, `specialistDomains`, `tunnelUrl`) and the `senderId` resolution from the admin session cookie. The session manager binds to `127.0.0.1` only so the wrapper is the sole authorised caller; any ad-hoc spawn that bypasses it duplicates this contract and is a bug.
|
|
82
|
+
The Hono route `POST /api/admin/claude-sessions` at [`platform/ui/server/routes/admin/claude-sessions.ts`](../../../ui/server/routes/admin/claude-sessions.ts) is the single canonical entry point for any caller — UI click handler or programmatic — that needs to open an admin PTY session with a first user prompt. Direct calls to the session-manager's `/spawn` are not allowed; the wrapper owns the per-spawn enrichment (`aboutOwner`, `dormantPlugins`, `activePlugins`, `specialistDomains`, `tunnelUrl`) and the `senderId` resolution from the admin session cookie. The session manager binds to `127.0.0.1` only so the wrapper is the sole authorised caller; any ad-hoc spawn that bypasses it duplicates this contract and is a bug. `dormantPlugins` is `installed − enabledPlugins` with one exclusion: PLUGIN.md frontmatter `surface: platform` (admin, docs) marks platform-shell plugins that ship with every install and are never opt-in features, so they never surface in the `<dormant-plugins>` nudge.
|
|
83
83
|
|
|
84
84
|
**Body schema.** `{channel?, permissionMode?, initialMessage?, specialist?, model?}` for the cookie path (Sidebar — `senderId` resolved from the admin session cookie, `hidden: false`). `permissionMode` accepts one of five values matching Claude Code's CLI: `'default'`, `'acceptEdits'`, `'plan'`, `'auto'`, `'bypassPermissions'`. The Stop-hook loopback caller adds `adminSessionId` and uses three overrides: `specialist: 'database-operator'`, `model: 'haiku'`, plus `initialMessage`. The wrapper rejects nothing structurally on the cookie path — string fields are coerced or defaulted (`channel` → `'browser'`, `permissionMode` → undefined, `initialMessage` → null if empty/whitespace). The loopback path refuses with `400 admin-session-id-required` if `adminSessionId` is missing or non-UUID, and `404 admin-session-not-found` if the manager has no StoredSession for it.
|
|
85
85
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# AEO (Answer Engine Optimisation) — user guide
|
|
2
|
+
|
|
3
|
+
The AEO plugin shapes the site for answer engines (Claude, ChatGPT, Perplexity, Google AI Overviews, Bing Copilot). It does three things: emits schema.org JSON-LD from typed graph entities, generates `/llms.txt` and `/llms-full.txt`, and audits any page against eight heuristics.
|
|
4
|
+
|
|
5
|
+
## When to use it
|
|
6
|
+
|
|
7
|
+
- A new page is being authored and you want it to be cited when customers ask an answer engine about your service.
|
|
8
|
+
- An existing page is not being cited. Audit it to find the structural reasons.
|
|
9
|
+
- You're standing up a new site and want the `llms.txt` pair generated so answer engines can index your content directly.
|
|
10
|
+
|
|
11
|
+
## The tools
|
|
12
|
+
|
|
13
|
+
### `aeo-emit-jsonld`
|
|
14
|
+
|
|
15
|
+
Generates a `<script type="application/ld+json">` block for a typed entity. Two modes:
|
|
16
|
+
|
|
17
|
+
- **From the graph.** Pass `entityId` (Neo4j elementId of an `Organization`, `Person`, `Service`, `Product`, `CreativeWork`, `FAQPage`, `RealEstateListing`, or `Event`). The tool resolves the entity, maps the label to its schema.org type, and emits the block.
|
|
18
|
+
- **Inline.** Pass `label` (one of the supported page types) plus a `properties` object. Use this when the page isn't backed by a stored entity (yet) but you want the JSON-LD shape.
|
|
19
|
+
|
|
20
|
+
Returns the parsed JSON-LD object and a ready-to-inline script block string. Inline the script in your page `<head>`.
|
|
21
|
+
|
|
22
|
+
### `aeo-write-llms-txt`
|
|
23
|
+
|
|
24
|
+
Generates the `llms.txt` / `llms-full.txt` pair for the account. Source is every `KnowledgeDocument` for the account that has a `url` property. Returns both files as strings plus a count of pages skipped because they had no URL.
|
|
25
|
+
|
|
26
|
+
Wire the output to your site host. Convention: `/llms.txt` (index) and `/llms-full.txt` (concatenated content), served as `text/plain`. Format follows the current draft at `https://llmstxt.org/`.
|
|
27
|
+
|
|
28
|
+
### `aeo-audit-page`
|
|
29
|
+
|
|
30
|
+
Runs the eight-heuristic audit. Pass either `url` (the tool fetches) or `html` (you supply the rendered content). Returns:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
{
|
|
34
|
+
"score": 0–100,
|
|
35
|
+
"heuristics": [
|
|
36
|
+
{
|
|
37
|
+
"name": "structured-answer",
|
|
38
|
+
"status": "pass" | "warn" | "fail",
|
|
39
|
+
"detail": "first <p> after <h1> is 142 chars",
|
|
40
|
+
"suggestion": "Add one ≤280-character <p> immediately after the <h1>…"
|
|
41
|
+
},
|
|
42
|
+
…
|
|
43
|
+
],
|
|
44
|
+
"target": "<url or '(inline html)'>",
|
|
45
|
+
"audit": { "runAt": "<iso>", "elementId": "<set when persisted>" }
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Pass `persist: true` plus `targetKnowledgeDocumentId` to write the result as an `:AEOAudit` node linked to the document.
|
|
50
|
+
|
|
51
|
+
## The eight heuristics
|
|
52
|
+
|
|
53
|
+
| Heuristic | What it checks |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `h1-present` | exactly one `<h1>` |
|
|
56
|
+
| `jsonld-present` | at least one parseable JSON-LD block |
|
|
57
|
+
| `structured-answer` | first `<p>` after `<h1>` is ≤280 chars |
|
|
58
|
+
| `faq-section` | `FAQPage` JSON-LD on the page |
|
|
59
|
+
| `meta-description` | 80–160 char `<meta name="description">` |
|
|
60
|
+
| `canonical-url` | `<link rel="canonical">` present |
|
|
61
|
+
| `og-tags` | `og:title` and `og:description` both set |
|
|
62
|
+
| `heading-hierarchy` | no level skips (h1→h3 etc.) |
|
|
63
|
+
|
|
64
|
+
`structured-answer` is the highest-impact: that one paragraph is what gets lifted into the engine's answer. A page can fail every other heuristic and still be cited if this one passes.
|
|
65
|
+
|
|
66
|
+
## Observability
|
|
67
|
+
|
|
68
|
+
Every tool emits a single log line per invocation:
|
|
69
|
+
|
|
70
|
+
- `[aeo-emit-jsonld] entityId=… schemaType=… source=graph|inline`
|
|
71
|
+
- `[aeo-llms-txt] site=… pages=… skippedNoUrl=… indexBytes=… fullBytes=…`
|
|
72
|
+
- `[aeo-audit] target=… score=… fails=… warns=…`
|
|
73
|
+
|
|
74
|
+
Diagnostic path: `grep -E '^\[aeo-' platform-logs/*.log | grep <urlOrEntityId>`.
|
|
75
|
+
|
|
76
|
+
## What this plugin does not do (yet)
|
|
77
|
+
|
|
78
|
+
- **No monitor.** Tracking whether your brand is cited by Claude / ChatGPT / Perplexity / Gemini requires multi-engine answer harvesting that has to fit maxy-code's no-API-key architecture. Filed as a follow-up.
|
|
79
|
+
- **No auto-emission on page render.** `aeo-emit-jsonld` is callable on demand. Wiring it into the platform's page-generator render path is per-renderer work, filed as a follow-up.
|
|
80
|
+
- **No publish-hook regeneration of `llms.txt`.** The tool runs on demand. Hooking it into the publish event is a follow-up.
|
|
81
|
+
|
|
82
|
+
## See also
|
|
83
|
+
|
|
84
|
+
- Plugin manifest: `platform/plugins/aeo/PLUGIN.md`
|
|
85
|
+
- Structured-answer template: `platform/plugins/aeo/skills/structured-answer/SKILL.md`
|
|
86
|
+
- Schema declaration: `platform/neo4j/schema.cypher` (search `AEOAudit`)
|
|
87
|
+
- Spec source: `https://llmstxt.org/`
|
|
@@ -85,7 +85,7 @@ Every PTY spawn injects an `--append-system-prompt` block composed of these sent
|
|
|
85
85
|
| `<about-owner>` | upstream `loadUserProfile` + `formatProfileSummary` (UI process) | sentinel renders the prose body with `NOTHING (operator-data source unavailable: \`<reason>\`)` on line one; spawn proceeds |
|
|
86
86
|
| `<specialist-domains>` | `<accountDir>/specialists/agents/*.md` frontmatter (UI process) | sentinel omitted when set is empty |
|
|
87
87
|
| `<plugin-manifest>` | enabled plugins' PLUGIN.md frontmatter + skill SKILL.md frontmatter (UI process) | sentinel omitted when set is empty |
|
|
88
|
-
| `<dormant-plugins>` | installed-minus-enabled set from `platform/plugins/` and `account.json` | sentinel omitted when set is empty |
|
|
88
|
+
| `<dormant-plugins>` | installed-minus-enabled set from `platform/plugins/` and `account.json`, excluding plugins whose PLUGIN.md frontmatter sets `surface: platform` (platform-shell — ships with every install, never opt-in). Absent `surface` field defaults to `feature` (dormant-eligible). | sentinel omitted when set is empty |
|
|
89
89
|
|
|
90
90
|
Diagnostic line per spawn (`~/.<brand>/logs/server.log`):
|
|
91
91
|
|
|
@@ -70,6 +70,12 @@ Manages the agent's own dedicated email account — IMAP for reading, SMTP for s
|
|
|
70
70
|
- **Graph history** (`email-graph-query`) — stored Email nodes in Neo4j. Use for recall, semantic search, email history questions.
|
|
71
71
|
- **General knowledge** (`memory-search`) — cross-type knowledge graph. Use when the question spans all memory, not just emails.
|
|
72
72
|
|
|
73
|
+
## Skills
|
|
74
|
+
|
|
75
|
+
| Skill | Purpose |
|
|
76
|
+
|-------|---------|
|
|
77
|
+
| [email-composition](skills/email-composition/SKILL.md) | Conversational wrapper for drafting replies, new emails, and inbox triage. Orchestrates the five `email-*` tools with human-in-the-loop approval on every send. |
|
|
78
|
+
|
|
73
79
|
## References
|
|
74
80
|
|
|
75
81
|
| Reference | Topics |
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: email-composition
|
|
3
|
+
description: "Conversational wrapper around the email plugin tools. Activate when the operator asks to reply to an email, draft a new email, follow up on a thread, or triage outstanding inbox items (e.g. 'reply to Sarah about X', 'email Joel about Y', 'what's outstanding in my inbox'). Orchestrates `email-graph-query`, `email-search`, `email-read`, `email-send`, `email-reply`. Every send is operator-approved — never auto-sends."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Email composition
|
|
7
|
+
|
|
8
|
+
Conversational wrapper around the existing `email-*` tools. Drafting, threading, triage. **Human-in-the-loop only — every send is explicitly approved by the operator.** No drip sequences, no scheduled sends, no automatic categorisation.
|
|
9
|
+
|
|
10
|
+
## When to activate
|
|
11
|
+
|
|
12
|
+
Activate when the operator asks for any of:
|
|
13
|
+
|
|
14
|
+
- **Reply to a known thread.** "Reply to Sarah's email about the lease." "Draft a reply to that thread from Joel."
|
|
15
|
+
- **New outbound email.** "Email Joel about the viewing." "Send a note to the team about Friday."
|
|
16
|
+
- **Follow-up on a topic.** "Follow up with everyone from yesterday's viewings."
|
|
17
|
+
- **Inbox triage.** "What's outstanding in my inbox?" "Anything I need to deal with?"
|
|
18
|
+
|
|
19
|
+
Do **not** activate for:
|
|
20
|
+
|
|
21
|
+
- Listing or searching emails as the end-goal (no draft intent) — let `email-search` / `email-graph-query` answer directly.
|
|
22
|
+
- Setup, status checks, OTP extraction, auto-respond config — those are direct tool calls.
|
|
23
|
+
- Any automated send loop. If the operator asks for drip / scheduled / broadcast sends, decline and explain that this skill is human-in-the-loop only.
|
|
24
|
+
|
|
25
|
+
## Flow — reply to an existing thread
|
|
26
|
+
|
|
27
|
+
### 1. Identify the thread
|
|
28
|
+
|
|
29
|
+
Pick the right retrieval path:
|
|
30
|
+
|
|
31
|
+
- **Graph history first** (`email-graph-query`) — for "reply to Sarah's email about the lease": the operator is naming a known correspondent and topic. Fast, semantic, covers the operator's full email history.
|
|
32
|
+
- **Live IMAP** (`email-search` or `email-read`) — for "that email that just came in" or "today's reply from Joel": the operator is naming a recent message that may not yet be in the graph.
|
|
33
|
+
|
|
34
|
+
If the search returns **multiple plausible threads**, do not guess. Surface 3-5 candidates with sender, subject, date, and a one-line summary, and ask which one. Emit narration `[email-composition] ambiguous-thread candidates=<n>`.
|
|
35
|
+
|
|
36
|
+
If the search returns **zero matches**, ask the operator to refine (different correspondent? different subject phrasing? wider date range?). Do not invent a thread.
|
|
37
|
+
|
|
38
|
+
Once the thread is locked, emit narration `[email-composition] thread-identified messageId=<id> subject="<subject>"`.
|
|
39
|
+
|
|
40
|
+
### 2. Load thread context
|
|
41
|
+
|
|
42
|
+
Pull the full thread — the current message plus parent messages — via `email-read` or `email-graph-query`. Reply to the actual position of the conversation, not a stripped-down summary. Quote-stripped one-line summaries miss the question the operator is actually being asked.
|
|
43
|
+
|
|
44
|
+
If the thread has **attachments** that the draft will reference, list the attachment names in the draft presentation and ask whether to forward them. The five existing tools do not currently forward attachments inline — surface this so the operator can manually re-attach if needed.
|
|
45
|
+
|
|
46
|
+
### 3. Detect skip conditions
|
|
47
|
+
|
|
48
|
+
Before drafting, check the inbound for:
|
|
49
|
+
|
|
50
|
+
- **Out-of-office / vacation autoreply.** Skip. Flag to the operator: "this looks like an OOO autoreply — no draft." Do not propose a reply.
|
|
51
|
+
- **Unsubscribe / list-management mail** (List-Unsubscribe header, "to unsubscribe click here" body). Skip. Flag to the operator.
|
|
52
|
+
- **Hostile or grief tone.** Draft cautiously and flag the tone in the presentation: "this reads as hostile / grief-related — please review carefully before sending." Never auto-soften without surfacing the tone judgement.
|
|
53
|
+
|
|
54
|
+
### 4. Retrieve voice exemplars (conditional)
|
|
55
|
+
|
|
56
|
+
If the `voice-mirror` plugin is installed and the operator has a style-card on file, fetch it plus 3-5 prior replies in the same register (recipient, subject area, formality) and condition the draft on them. If `voice-mirror` is **not** installed, draft in a **neutral British business register** — polite, concise, no slang, no transatlantic spellings — and state explicitly in the presentation: "drafted in neutral British business voice (voice-mirror not installed)."
|
|
57
|
+
|
|
58
|
+
### 5. Draft
|
|
59
|
+
|
|
60
|
+
Compose the reply. Anchor to:
|
|
61
|
+
|
|
62
|
+
- The specific question or position in the parent message.
|
|
63
|
+
- The operator's voice (from voice-mirror) or the neutral British business register.
|
|
64
|
+
- Any decisions the operator has already stated in this conversation (e.g. "say yes to the price, no to the timeline").
|
|
65
|
+
|
|
66
|
+
Keep drafts tight. Email is not a wall-of-text medium.
|
|
67
|
+
|
|
68
|
+
### 6. Present
|
|
69
|
+
|
|
70
|
+
Show the draft inline with:
|
|
71
|
+
|
|
72
|
+
- **To** — recipient address (the original sender, unless the operator asked for `replyAll`).
|
|
73
|
+
- **Cc / Bcc** — empty unless the operator explicitly named one. Never copy people forward from the original thread without being asked.
|
|
74
|
+
- **Subject** — the threaded subject (the original with `Re:` if not already prefixed; `email-reply` handles this).
|
|
75
|
+
- **Body** — the draft.
|
|
76
|
+
- **Notes** — any flags from step 3 (tone, attachments, OOO skip-already-applied).
|
|
77
|
+
|
|
78
|
+
Emit narration `[email-composition] draft-presented recipient=<addr> subjectPrefix="<first-60-chars>"`.
|
|
79
|
+
|
|
80
|
+
**Do not call `email-reply` at this step.** The send is not yet approved.
|
|
81
|
+
|
|
82
|
+
### 7. Edit loop
|
|
83
|
+
|
|
84
|
+
The operator may:
|
|
85
|
+
|
|
86
|
+
- **Approve** ("send it", "looks good", "yes"). Proceed to step 8.
|
|
87
|
+
- **Edit verbally** ("make it shorter", "drop the second paragraph", "say yes to the price but not the timeline"). Apply the edit and re-present. Stay in the loop until approval or abandonment.
|
|
88
|
+
- **Abandon** ("never mind", "I'll do it myself"). Emit `[email-composition] send-aborted reason=operator-abandoned`. Do not send.
|
|
89
|
+
|
|
90
|
+
Every operator edit is downstream feedback for `voice-mirror` — if the plugin is installed, record the edit via its feedback channel so the next draft is closer to the operator's voice. If voice-mirror is absent, no feedback is recorded.
|
|
91
|
+
|
|
92
|
+
### 8. Send
|
|
93
|
+
|
|
94
|
+
On **explicit** approval, call `email-reply` with:
|
|
95
|
+
|
|
96
|
+
- `messageId` — the parent message ID from step 1. This is the threading anchor; `email-reply` populates `In-Reply-To` and `References` from it so the reply lands inside the existing thread, not as a new conversation.
|
|
97
|
+
- `replyAll` — only if the operator asked for it. Default is false (reply to sender only).
|
|
98
|
+
- Subject and body from the approved draft.
|
|
99
|
+
|
|
100
|
+
Emit narration `[email-composition] send-confirmed`.
|
|
101
|
+
|
|
102
|
+
Confirm to the operator that the send happened by quoting the `email-reply` tool's success line (it includes the sent `messageId` and the resolved `In-Reply-To`).
|
|
103
|
+
|
|
104
|
+
If `email-reply` errors, surface the error verbatim; do not silently retry. The operator decides whether to fix and retry or abandon.
|
|
105
|
+
|
|
106
|
+
## Flow — new outbound email
|
|
107
|
+
|
|
108
|
+
Same as the reply flow with two differences:
|
|
109
|
+
|
|
110
|
+
1. **No thread context load** (step 2 skipped). Instead, ask the operator for the recipient (if not already named), the topic, and any constraints (formal vs casual, length, things to include or exclude).
|
|
111
|
+
2. **Send via `email-send`**, not `email-reply`. There is no parent `messageId` because this is a new conversation.
|
|
112
|
+
|
|
113
|
+
Multi-recipient drafting: if the operator names a single person, use that address. If the operator names a group ("email the team", "everyone from yesterday's viewings"), ask which addresses to include — list candidates if you can resolve them from the graph, otherwise ask the operator to enumerate. Do not invent recipients.
|
|
114
|
+
|
|
115
|
+
## Flow — triage variant
|
|
116
|
+
|
|
117
|
+
When the operator asks "what's outstanding in my inbox" / "anything I need to deal with" / "what's unread":
|
|
118
|
+
|
|
119
|
+
1. Call `email-read` for the last N unread messages (default N = 10; operator can specify "last 25", "today's", etc.).
|
|
120
|
+
2. For each message, return a one-line summary with a recommended action:
|
|
121
|
+
- **Reply** — needs a response.
|
|
122
|
+
- **File** — informational, can be archived.
|
|
123
|
+
- **Ignore** — autoreply, newsletter, list mail, OOO.
|
|
124
|
+
3. Emit narration `[email-composition] triage-returned n=<count>`.
|
|
125
|
+
4. Wait for the operator to pick one. When they do ("reply to #3", "draft a response to the Joel one"), drop into the reply flow at step 1 (thread already identified — use the `messageId` from the triage list).
|
|
126
|
+
|
|
127
|
+
The triage variant **never auto-drafts**. It returns the list and waits.
|
|
128
|
+
|
|
129
|
+
## Edge cases
|
|
130
|
+
|
|
131
|
+
| Situation | Behaviour |
|
|
132
|
+
|-----------|-----------|
|
|
133
|
+
| Multiple threads match the operator's description | Surface 3-5 candidates and ask. Never guess. |
|
|
134
|
+
| CC / BCC in the original thread | Reply to sender only by default. Ask explicitly before copying others. |
|
|
135
|
+
| `replyAll` requested | Pass `replyAll: true` to `email-reply`. Otherwise omit (defaults false). |
|
|
136
|
+
| Original thread has attachments referenced in the draft | List attachment names; ask whether to forward. |
|
|
137
|
+
| Inbound is an out-of-office autoreply | Skip. Flag to operator. Do not draft. |
|
|
138
|
+
| Inbound is a list / unsubscribe mail | Skip. Flag to operator. Do not draft. |
|
|
139
|
+
| Inbound is hostile or grief-related | Draft cautiously; flag tone in the presentation. Operator must review explicitly. |
|
|
140
|
+
| `voice-mirror` plugin absent | Draft in neutral British business register; state the absence in the presentation. |
|
|
141
|
+
| `email-reply` returns an error after approval | Surface the error verbatim. Operator decides whether to fix and retry or abandon. Do not silent-retry. |
|
|
142
|
+
|
|
143
|
+
## Out of scope
|
|
144
|
+
|
|
145
|
+
Hard boundaries — never offer these in this skill, even if asked:
|
|
146
|
+
|
|
147
|
+
- **Drip sequences** — multi-step automated send chains. Decline; refer to a future task.
|
|
148
|
+
- **Scheduled / delayed sends** — there is no "send at 9am tomorrow" path. The operator triggers the send when they approve.
|
|
149
|
+
- **List broadcasts** — bulk sends to managed subscriber lists. Not this skill.
|
|
150
|
+
- **Inbox auto-categorisation / labelling** — triage is on demand only.
|
|
151
|
+
- **Calendar follow-up auto-writes** — if a reply confirms a meeting, mention it and let the operator decide whether to write a calendar entry separately.
|
|
152
|
+
- **New MCP tools** — this skill orchestrates the existing five (`email-graph-query`, `email-search`, `email-read`, `email-send`, `email-reply`). Nothing else.
|
|
153
|
+
|
|
154
|
+
## Observability
|
|
155
|
+
|
|
156
|
+
The skill emits narration at each major step so a session log makes the full draft → approval → send chain greppable:
|
|
157
|
+
|
|
158
|
+
| Step | Narration |
|
|
159
|
+
|------|-----------|
|
|
160
|
+
| Thread locked | `[email-composition] thread-identified messageId=<id> subject="<subject>"` |
|
|
161
|
+
| Multiple matches | `[email-composition] ambiguous-thread candidates=<n>` |
|
|
162
|
+
| Draft shown | `[email-composition] draft-presented recipient=<addr> subjectPrefix="<first-60-chars>"` |
|
|
163
|
+
| Send approved | `[email-composition] send-confirmed` |
|
|
164
|
+
| Send abandoned | `[email-composition] send-aborted reason=<why>` |
|
|
165
|
+
| Triage list returned | `[email-composition] triage-returned n=<count>` |
|
|
166
|
+
|
|
167
|
+
The underlying `[email-send]` / `[email-reply]` / `[email-search]` / `[email-graph-query]` / `[email-read]` tool logs confirm the actual tool calls. Absence of an expected tool tag after `draft-presented` + operator approval is the failure signal — the skill said it sent but didn't.
|
|
168
|
+
|
|
169
|
+
Diagnostic on Pi:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
grep -E '\[email-(composition|reply|send|search|graph-query|read)\]' <session-log>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
returns the full chain for any thread.
|
|
176
|
+
|
|
177
|
+
## Dependencies
|
|
178
|
+
|
|
179
|
+
- **Required:** `email` plugin (`email-graph-query`, `email-search`, `email-read`, `email-send`, `email-reply`).
|
|
180
|
+
- **Optional:** `voice-mirror` plugin. If installed, drafts use the operator's style-card and edits are captured as feedback. If absent, drafts fall back to a neutral British business register.
|