@rubytech/create-maxy-code 0.1.111 → 0.1.113
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/dist/__tests__/brew-install.test.js +10 -0
- package/dist/__tests__/brew-resolve.test.js +35 -0
- package/dist/__tests__/launchd-plist.test.js +46 -0
- package/dist/__tests__/macos-darwin-branch.test.js +85 -0
- package/dist/brew-install.js +5 -0
- package/dist/index.js +26 -6
- package/package.json +1 -1
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +105 -0
- package/payload/platform/plugins/docs/references/deployment.md +22 -4
- package/payload/platform/plugins/memory/mcp/dist/index.js +35 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.d.ts +15 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.js +64 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.js.map +1 -0
- package/payload/platform/plugins/memory/references/schema-estate-agent.md +76 -1
- package/payload/platform/plugins/venture-studio/PLUGIN.md +13 -4
- package/payload/platform/plugins/venture-studio/bin/scaffold.sh +15 -3
- package/payload/platform/plugins/venture-studio/skills/brand-pack/SKILL.md +1 -1
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/SKILL.md +27 -14
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/business-plan-template.md +7 -4
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +16 -5
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/deck-blueprint-template.md +7 -6
- package/payload/platform/plugins/venture-studio/skills/office-hours/SKILL.md +8 -9
- package/payload/platform/plugins/venture-studio/skills/prototype-host/SKILL.md +179 -0
- package/payload/platform/plugins/venture-studio/skills/prototype-host/references/cloudflared-ingress-edit.md +81 -0
- package/payload/platform/plugins/venture-studio/skills/prototype-host/references/scaffold-frameworks.md +60 -0
- package/payload/platform/plugins/venture-studio/skills/prototype-host/references/systemd-user-service.md +104 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/SKILL.md +4 -0
- package/payload/platform/templates/agents/public/IDENTITY.md +11 -1
- package/payload/premium-plugins/real-agent/BUNDLE.md +3 -2
- package/payload/premium-plugins/real-agent/agents/listing-curator.md +152 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/a4-print-documents/SKILL.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/brand-design/SKILL.md +5 -11
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/make-brochure/SKILL.md +7 -7
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-extract/SKILL.md +31 -39
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-market-report/SKILL.md +4 -4
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-socials/SKILL.md +3 -2
- package/payload/premium-plugins/real-agent/plugins/buyers/.claude-plugin/plugin.json +1 -1
- package/payload/premium-plugins/real-agent/plugins/buyers/PLUGIN.md +3 -2
- package/payload/premium-plugins/real-agent/plugins/buyers/skills/property-recommender/SKILL.md +96 -0
- package/payload/premium-plugins/venture-studio/PLUGIN.md +13 -4
- package/payload/premium-plugins/venture-studio/bin/scaffold.sh +15 -3
- package/payload/premium-plugins/venture-studio/skills/brand-pack/SKILL.md +1 -1
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +27 -14
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/business-plan-template.md +7 -4
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +16 -5
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/deck-blueprint-template.md +7 -6
- package/payload/premium-plugins/venture-studio/skills/office-hours/SKILL.md +8 -9
- package/payload/premium-plugins/venture-studio/skills/prototype-host/SKILL.md +179 -0
- package/payload/premium-plugins/venture-studio/skills/prototype-host/references/cloudflared-ingress-edit.md +81 -0
- package/payload/premium-plugins/venture-studio/skills/prototype-host/references/scaffold-frameworks.md +60 -0
- package/payload/premium-plugins/venture-studio/skills/prototype-host/references/systemd-user-service.md +104 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/SKILL.md +4 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Framework scaffolders, dev-server commands, and port overrides
|
|
2
|
+
|
|
3
|
+
Five frameworks supported. Each row gives the exact command set the prototype-host skill runs in phases 2 and 5.
|
|
4
|
+
|
|
5
|
+
## Selection defaults
|
|
6
|
+
|
|
7
|
+
| Surface intent | Default framework | Reason |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `landing` | `static` | One HTML file rendered from `01-narrative/LANDING.md` is faster to ship and faster to load than a Vite bundle. Lighthouse-clean by construction. |
|
|
10
|
+
| `prototype` | `vite` | Vue/React/Svelte all supported; HMR is the founder's iteration loop. |
|
|
11
|
+
|
|
12
|
+
If the operator names a framework explicitly, use that. Otherwise these defaults stand.
|
|
13
|
+
|
|
14
|
+
## `static`
|
|
15
|
+
|
|
16
|
+
No scaffolder. The skill writes a single `index.html` directly into `<surface-dir>/` from `01-narrative/LANDING.md`, with brand tokens inlined into `<style>` in the `<head>`.
|
|
17
|
+
|
|
18
|
+
- **Build:** none (raw HTML); for a `dist/` build (if the operator adds Tailwind etc later) the skill respects `<surface-dir>/dist/` if present.
|
|
19
|
+
- **Serve:** `cloudflared file-server --port <port> <surface-dir>` (or `<surface-dir>/dist` if a build directory exists).
|
|
20
|
+
- **Port override:** `--port <port>` on `cloudflared file-server`.
|
|
21
|
+
|
|
22
|
+
## `vite`
|
|
23
|
+
|
|
24
|
+
- **Scaffold:** `cd <surface-dir-parent> && npm create vite@latest <surface-name> -- --template vanilla-ts -- --yes`. The agent prompts the operator before this if they want a non-vanilla template (`react-ts`, `svelte-ts`, `vue-ts`); otherwise `vanilla-ts` is the default — the founder can rewrite `index.html` + `src/main.ts` to anything from there.
|
|
25
|
+
- **Install:** `cd <surface-dir> && npm install`
|
|
26
|
+
- **Build:** `npm run build` (outputs to `<surface-dir>/dist/`)
|
|
27
|
+
- **Dev server:** `npm run dev -- --port <port> --host 127.0.0.1`
|
|
28
|
+
- **Port override:** `--port <port>` (Vite respects the flag; `PORT=` is ignored).
|
|
29
|
+
|
|
30
|
+
## `next`
|
|
31
|
+
|
|
32
|
+
- **Scaffold:** `cd <surface-dir-parent> && npx -y create-next-app@latest <surface-name> --typescript --app --tailwind --eslint --src-dir --import-alias "@/*" --use-npm --yes`
|
|
33
|
+
- **Install:** scaffolder runs `npm install` automatically.
|
|
34
|
+
- **Build:** `npm run build`
|
|
35
|
+
- **Dev server:** `npm run dev -- -p <port> -H 127.0.0.1`
|
|
36
|
+
- **Port override:** `-p <port>` (Next.js dev/start respect the flag; `PORT=` also works but `-p` is explicit).
|
|
37
|
+
|
|
38
|
+
## `sveltekit`
|
|
39
|
+
|
|
40
|
+
- **Scaffold:** `cd <surface-dir-parent> && npx -y sv create <surface-name> --template minimal --types ts --no-add-ons --install npm`
|
|
41
|
+
- **Install:** scaffolder runs `npm install` automatically when `--install npm` is passed.
|
|
42
|
+
- **Build:** `npm run build`
|
|
43
|
+
- **Dev server:** `npm run dev -- --port <port> --host 127.0.0.1`
|
|
44
|
+
- **Port override:** `--port <port>`.
|
|
45
|
+
|
|
46
|
+
## `astro`
|
|
47
|
+
|
|
48
|
+
- **Scaffold:** `cd <surface-dir-parent> && npm create astro@latest <surface-name> -- --template minimal --typescript strict --install --no-git --yes`
|
|
49
|
+
- **Install:** scaffolder runs `npm install` when `--install` is passed.
|
|
50
|
+
- **Build:** `npm run build` (outputs to `<surface-dir>/dist/`)
|
|
51
|
+
- **Dev server:** `npm run dev -- --port <port> --host 127.0.0.1`
|
|
52
|
+
- **Port override:** `--port <port>`.
|
|
53
|
+
|
|
54
|
+
## Why `--host 127.0.0.1`
|
|
55
|
+
|
|
56
|
+
Every dev server above binds to `127.0.0.1`, not `0.0.0.0`. Cloudflared connects to the loopback address; binding to `0.0.0.0` would expose the dev server on the install device's LAN address, which is unwanted (LAN clients should reach the prototype through the tunnel, not directly).
|
|
57
|
+
|
|
58
|
+
## Why no `--yes` on `create-next-app` interactive prompts in older versions
|
|
59
|
+
|
|
60
|
+
`create-next-app@latest` (≥14.2) treats all listed flags as the answer set and runs non-interactively. If a flag is omitted it falls back to a TTY prompt — fatal under the agent's non-TTY Bash invocation. Always pass every flag the table lists.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Systemd user unit template for a hosted prototype surface
|
|
2
|
+
|
|
3
|
+
One unit per hosted surface: `${HOME}/.config/systemd/user/${BRAND}-prototype-<surface>.service`.
|
|
4
|
+
|
|
5
|
+
Two flavours: dev-server mode (frameworks with hot-reload) and static file-server mode (raw HTML or built `dist/`).
|
|
6
|
+
|
|
7
|
+
## Dev-server mode (vite, next, sveltekit, astro)
|
|
8
|
+
|
|
9
|
+
```ini
|
|
10
|
+
[Unit]
|
|
11
|
+
Description=${BRAND} prototype surface <surface> — <framework> dev server
|
|
12
|
+
After=network.target
|
|
13
|
+
|
|
14
|
+
[Service]
|
|
15
|
+
Type=simple
|
|
16
|
+
WorkingDirectory=<surface-dir>
|
|
17
|
+
Environment="PORT=<port>"
|
|
18
|
+
Environment="NODE_ENV=development"
|
|
19
|
+
ExecStart=/usr/bin/env npm run dev -- <framework-port-flag> <port> --host 127.0.0.1
|
|
20
|
+
Restart=on-failure
|
|
21
|
+
RestartSec=3
|
|
22
|
+
StandardOutput=journal
|
|
23
|
+
StandardError=journal
|
|
24
|
+
|
|
25
|
+
[Install]
|
|
26
|
+
WantedBy=default.target
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`<framework-port-flag>` is the per-framework override from [`scaffold-frameworks.md`](scaffold-frameworks.md): `--port` for Vite/SvelteKit/Astro, `-p` for Next.
|
|
30
|
+
|
|
31
|
+
## Static file-server mode
|
|
32
|
+
|
|
33
|
+
For the `static` framework (raw HTML rendered from `LANDING.md`):
|
|
34
|
+
|
|
35
|
+
```ini
|
|
36
|
+
[Unit]
|
|
37
|
+
Description=${BRAND} prototype surface <surface> — static file server
|
|
38
|
+
After=network.target
|
|
39
|
+
|
|
40
|
+
[Service]
|
|
41
|
+
Type=simple
|
|
42
|
+
WorkingDirectory=<surface-dir>
|
|
43
|
+
ExecStart=/usr/local/bin/cloudflared file-server --port <port> --address 127.0.0.1 <surface-dir>
|
|
44
|
+
Restart=on-failure
|
|
45
|
+
RestartSec=3
|
|
46
|
+
StandardOutput=journal
|
|
47
|
+
StandardError=journal
|
|
48
|
+
|
|
49
|
+
[Install]
|
|
50
|
+
WantedBy=default.target
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If `<surface-dir>/dist/` exists (the founder added a build step later), point `ExecStart`'s last arg at `<surface-dir>/dist` instead of `<surface-dir>`. The skill detects this automatically with `[ -d <surface-dir>/dist ] && SERVE_DIR=<surface-dir>/dist || SERVE_DIR=<surface-dir>`.
|
|
54
|
+
|
|
55
|
+
## Why a user unit, not a root unit
|
|
56
|
+
|
|
57
|
+
The brand cloudflared connector already runs as a `systemctl --user` unit ([`platform/plugins/cloudflare/references/reset-guide.md`](../../../../../platform/plugins/cloudflare/references/reset-guide.md)). Keeping prototype units in the same scope means:
|
|
58
|
+
|
|
59
|
+
- Same `journalctl --user` namespace — the operator reads one log namespace, not two.
|
|
60
|
+
- No `sudo` required to install, enable, or restart.
|
|
61
|
+
- `loginctl enable-linger <user>` (already set by the installer) survives reboot — units come back without an SSH session.
|
|
62
|
+
|
|
63
|
+
Root units would also work, but split the operator's mental model and require `sudo` for every restart. Not worth the tradeoff for a per-prototype surface.
|
|
64
|
+
|
|
65
|
+
## Enabling and starting
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
systemctl --user daemon-reload
|
|
69
|
+
systemctl --user enable --now "${BRAND}-prototype-<surface>.service"
|
|
70
|
+
|
|
71
|
+
# Verify it stayed up.
|
|
72
|
+
systemctl --user is-active "${BRAND}-prototype-<surface>.service"
|
|
73
|
+
|
|
74
|
+
# If not active, dump the journal and surface verbatim.
|
|
75
|
+
if [ "$(systemctl --user is-active "${BRAND}-prototype-<surface>.service")" != "active" ]; then
|
|
76
|
+
journalctl --user -u "${BRAND}-prototype-<surface>.service" -n 50 --no-pager >&2
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Re-run after edit
|
|
82
|
+
|
|
83
|
+
If the skill regenerates the unit file (e.g. the port changed, or the static surface gained a `dist/` directory), the sequence is:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
systemctl --user daemon-reload
|
|
87
|
+
systemctl --user restart "${BRAND}-prototype-<surface>.service"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`daemon-reload` is required after any unit-file edit; `restart` picks up the new `ExecStart`.
|
|
91
|
+
|
|
92
|
+
## Why `Restart=on-failure`, not `always`
|
|
93
|
+
|
|
94
|
+
Dev servers exit 0 in legitimate cases (the operator killed `npm run dev` from another shell to apply a config edit). `on-failure` respects those exits; `always` would fight them.
|
|
95
|
+
|
|
96
|
+
## Logging
|
|
97
|
+
|
|
98
|
+
All output (stdout + stderr from the dev server / file server) goes to the user journal. Read with:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
journalctl --user -u "${BRAND}-prototype-<surface>.service" -f
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
No file-based log rotation needed — systemd handles journal rotation.
|
|
@@ -284,6 +284,10 @@ For go-to-market planning:
|
|
|
284
284
|
4. **Progress over perfection** — Ship, measure, iterate
|
|
285
285
|
5. **Synthesis over summary** — Connect dots, don't just list facts
|
|
286
286
|
|
|
287
|
+
## Research routing
|
|
288
|
+
|
|
289
|
+
Customer-discovery prior-art lookups, prospect-finding (`references/boolean-search.md` invocations, Earlyvangelist identification), citation-backed claims (PMF benchmarks, market signal precedents), and academic-grade evidence all route through the `research-assistant` specialist with the `deep-research`, `data-research`, and `academic-verify` skills. Dispatch the specialist; do not call `WebSearch` or `WebFetch` directly from this skill. The specialist returns structured findings with citations the PMF, Landing, and PRD artefacts can quote.
|
|
290
|
+
|
|
287
291
|
## Framework Reference Files
|
|
288
292
|
|
|
289
293
|
All frameworks are in `references/` directory:
|
|
@@ -31,7 +31,17 @@ A landfill graph defeats EVIDENCE-BASED: search returns noise, the agent re-writ
|
|
|
31
31
|
|
|
32
32
|
## Visual content
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Two sources of inline images are permitted, and only these two:
|
|
35
|
+
|
|
36
|
+
1. **Brand images at `/brand/`** — visual assets the business has prepared. When your knowledge lists an image and a visitor's question is relevant to it, embed it inline using standard markdown: ``. Only reference images explicitly listed in your knowledge — never guess or fabricate filenames.
|
|
37
|
+
|
|
38
|
+
2. **`:ImageObject` URLs returned by `memory-search`** — when a memory-search result includes `:ImageObject` neighbours on its `related` set (e.g. a `:Listing` returned with its `:DEPICTS`-linked images), the `url` field of those neighbours is permitted as an inline image source: ``. Use the ImageObject's own `description` as the alt text; never substitute placeholder alt text. Only embed URLs returned by memory-search in this turn — do not cache, reuse across turns from memory, or compose URLs from slugs.
|
|
39
|
+
|
|
40
|
+
No other external image embedding is permitted. The visitor sees the image directly in the conversation. When a relevant image exists, show it rather than describing what you could show.
|
|
41
|
+
|
|
42
|
+
### No fabrication on graph-backed content
|
|
43
|
+
|
|
44
|
+
When you relay graph-backed entities to a visitor — listings, prices, addresses, image content, anything sourced from `memory-search` — every field in the reply must trace to a property on a node the search just returned. If memory-search returns zero matches for the visitor's intent, say so plainly in one sentence. Never invent a listing, a price, a room photo, or a feature to fill the gap. Run the same intent three times and the no-match reply must stay consistent in meaning — the visitor's experience is the audit.
|
|
35
45
|
|
|
36
46
|
## Boundaries
|
|
37
47
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: real-agent
|
|
3
|
-
description: "UK estate agency skills + Loop CRM + PropertyData market analytics + single-address preval — 12 sub-plugins covering sales, listings, vendor management, buyer management, lead generation, coaching, business operations, teaching, Loop CRM, PropertyData, property brochures, and preval.
|
|
3
|
+
description: "UK estate agency skills + Loop CRM + PropertyData market analytics + single-address preval — 12 sub-plugins covering sales, listings, vendor management, buyer management, lead generation, coaching, business operations, teaching, Loop CRM, PropertyData, property brochures, and preval. 4 specialist roles (negotiator, valuer, compliance, listing-curator). 34 skills + 45 MCP tools."
|
|
4
4
|
plugins:
|
|
5
5
|
- estate-sales
|
|
6
6
|
- listings
|
|
@@ -18,7 +18,7 @@ plugins:
|
|
|
18
18
|
|
|
19
19
|
# Real Agent
|
|
20
20
|
|
|
21
|
-
Premium plugin bundle for UK estate agency professionals. Shipped on every non-Maxy brand install (today: Real Agent); Maxy installs do not receive it. All 12 sub-plugins ship as a set, each independently activatable via `enabledPlugins`. Every admin boot walks every bundle under `premium-plugins/` (brand-gated — Maxy short-circuits) and reconciles `account.json` `enabledPlugins` against on-disk presence: any sub-plugin from the `plugins:` list above whose `platform/plugins/<sub>/PLUGIN.md` exists gets stamped via an idempotent set-union, so `mcp__loop__*` tools and the other sub-plugin surfaces are visible to the admin agent from the first turn after install — and on first boot post-deploy for accounts that already had the bundle on disk from a prior install. Disable individual sub-plugins with `plugin-toggle-enabled`. The bundle also delivers
|
|
21
|
+
Premium plugin bundle for UK estate agency professionals. Shipped on every non-Maxy brand install (today: Real Agent); Maxy installs do not receive it. All 12 sub-plugins ship as a set, each independently activatable via `enabledPlugins`. Every admin boot walks every bundle under `premium-plugins/` (brand-gated — Maxy short-circuits) and reconciles `account.json` `enabledPlugins` against on-disk presence: any sub-plugin from the `plugins:` list above whose `platform/plugins/<sub>/PLUGIN.md` exists gets stamped via an idempotent set-union, so `mcp__loop__*` tools and the other sub-plugin surfaces are visible to the admin agent from the first turn after install — and on first boot post-deploy for accounts that already had the bundle on disk from a prior install. Disable individual sub-plugins with `plugin-toggle-enabled`. The bundle also delivers four specialist roles (negotiator, valuer, compliance, listing-curator) that embed domain knowledge from the sub-plugins and operate autonomously with Loop CRM and memory tools.
|
|
22
22
|
|
|
23
23
|
## Specialist Roles
|
|
24
24
|
|
|
@@ -27,6 +27,7 @@ Premium plugin bundle for UK estate agency professionals. Shipped on every non-M
|
|
|
27
27
|
| Negotiator | `agents/negotiator.md` | All Loop CRM tools + memory-search | Buyer pipeline, viewings, feedback, offers, deal progression |
|
|
28
28
|
| Valuer | `agents/valuer.md` | Loop property/people tools + memory-search | Market appraisal preparation, comparable evidence analysis |
|
|
29
29
|
| Compliance | `agents/compliance.md` | All Loop CRM tools + memory-search + perplexity-search | UK estate agency law, listing audit, AML/KYC, regulatory compliance |
|
|
30
|
+
| Listing Curator | `agents/listing-curator.md` | memory-search + memory-write + memory-update + memory-image-fetch + Loop property/team tools | Ingestion of property listings into typed `:Listing` nodes — maps Loop CRM records, published zip-site directories, and manual operator dumps to renderable graph entities so the public agent can surface them as property cards. Also writes one `:ImageObject` per Listing image (vision-LLM description + closed-set tags) so the public-side recommender can pick contextually rather than always emitting the hero. |
|
|
30
31
|
|
|
31
32
|
## Sub-Plugins
|
|
32
33
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: listing-curator
|
|
3
|
+
description: "Property listing ingestion. Maps any property-shaped input — a Loop CRM record, a published zip-site directory, or a manual operator dump — to one typed :Listing node that the public agent's memory-search can return as a renderable property card. Delegate when a property listing needs to enter the graph, when an existing Listing's status drifts (for-sale → under-offer → sold), or when a hosted property site has been published and the graph has no record of it yet."
|
|
4
|
+
summary: "Your listing curator — maps property inputs to typed :Listing nodes that visitor-facing chat can render."
|
|
5
|
+
model: claude-sonnet-4-6
|
|
6
|
+
tools: mcp__memory__memory-search, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__image-fetch, mcp__loop__loop-property-search, mcp__loop__loop-property-detail, mcp__loop__loop-property-listed, mcp__loop__loop-property-sold, mcp__loop__loop-key-list, mcp__loop__loop-team-info
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Listing Curator
|
|
10
|
+
|
|
11
|
+
You are the listing curator. You own one outcome: any property-shaped input that crosses the realagent boundary leaves the graph as a `:Listing` node carrying the renderable contract from `schema-estate-agent.md`. Generic ingestion specialists (`database-operator`) and hosting flows are not allowed to mint property data — that work routes here so the doctrine is enforced in one place.
|
|
12
|
+
|
|
13
|
+
You receive a brief from the admin agent. You read the source, map it field-by-field, and write the Listing. You return the new `:Listing`'s `slug` and the action you took (`create` / `update` / `no-op`).
|
|
14
|
+
|
|
15
|
+
## Outcome contract
|
|
16
|
+
|
|
17
|
+
For every property-shaped input the curator returns one `:Listing` node satisfying `schema-estate-agent.md`'s required-property set, idempotent on `(accountId, sourceSystem, sourceId)`. The Listing always carries the parent edge `(:Listing)-[:LISTED_BY]->(:LocalBusiness)` naming the estate agency; when a structural `:Property` node is known, additionally `(:Listing)-[:FOR_PROPERTY]->(:Property)`.
|
|
18
|
+
|
|
19
|
+
Three legal action outcomes:
|
|
20
|
+
|
|
21
|
+
- **`create`** — no Listing existed at `(accountId, sourceSystem, sourceId)`; one was written.
|
|
22
|
+
- **`update`** — a Listing existed; its mutable fields were refreshed (status, price, blurb, images, lastVerifiedAt). The `slug`, `sourceSystem`, and `sourceId` are immutable on update.
|
|
23
|
+
- **`no-op`** — the source record could not be mapped to a complete Listing (see "Source rejection" below). No write occurred. The brief is returned to the admin agent with an explanation.
|
|
24
|
+
|
|
25
|
+
Anything else — emitting a `:KnowledgeDocument` instead of a `:Listing`, leaving renderable fields null on a `for-sale` listing, creating a second Listing for a re-ingested source record — is a defect.
|
|
26
|
+
|
|
27
|
+
## Source mapping
|
|
28
|
+
|
|
29
|
+
### Loop CRM record → Listing
|
|
30
|
+
|
|
31
|
+
The Loop CRM record format you receive (either piped in by the brief or fetched via `mcp__loop__loop-property-detail`) carries a flat `body` text plus structured `loopPropertyId`. Parse with these rules:
|
|
32
|
+
|
|
33
|
+
| Listing field | Source |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `accountId` | inherited from the curator's session env |
|
|
36
|
+
| `slug` | derived from address: lowercase, ASCII-only, dash-separated; collapse repeated dashes; strip leading/trailing dashes. Example: `griffin-house-upper-church-street-chepstow` |
|
|
37
|
+
| `displayName` | the `name` field on the source record (already human-formatted), e.g. `Listing Particulars — Griffin House, Upper Church Street, Chepstow NP16 5EX` — strip the `Listing Particulars — ` prefix if present |
|
|
38
|
+
| `sourceSystem` | literal `"loop-crm"` |
|
|
39
|
+
| `sourceId` | the `loopPropertyId` field as string |
|
|
40
|
+
| `listingType` | from the FACTS block `Type of listing:` (`sale` or `let`); default `sale` when absent |
|
|
41
|
+
| `status` | from FACTS `Status:` value normalised — `Available` → `for-sale`, `Under Offer` → `under-offer`, `Sold STC` / `Sold` → `sold`, `Withdrawn` → `withdrawn`. When absent default `for-sale` |
|
|
42
|
+
| `price` | from FACTS `Price:` — strip `£` and commas, parse integer (pounds, not pence; convert if necessary) |
|
|
43
|
+
| `priceQualifier` | from any `Guide Price` / `Offers Over` / `OIEO` / `POA` keyword preceding the price |
|
|
44
|
+
| `bedrooms`, `bathrooms`, `receptionRooms` | integers from FACTS `Bedrooms:`, `Bathrooms:`, `Receptions:` |
|
|
45
|
+
| `floorAreaSqft` | from FACTS `Square Feet:` or `Floor Area:` — parse integer |
|
|
46
|
+
| `propertyType` | from FACTS `Type:` — lowercase the value (`cottage`, `detached`, `flat`, etc.) |
|
|
47
|
+
| `tenure` | from FACTS `Tenure:` — normalise to `freehold` / `leasehold` / `share-of-freehold` / `commonhold` |
|
|
48
|
+
| `postcode` | extracted from `displayName` by the trailing UK postcode regex `[A-Z]{1,2}[0-9][A-Z0-9]? ?[0-9][A-Z]{2}` — normalised to upper-case with a single space before the inward code (`NP16 5EX`) |
|
|
49
|
+
| `addressLine`, `town` | parsed from the `displayName` minus the postcode tail |
|
|
50
|
+
| `description` | the full source `body` text, with the FACTS block stripped (keep the narrative paragraphs only) |
|
|
51
|
+
| `imageUrls`, `heroImageUrl`, `floorplanUrls`, `epcUrl` | from the Loop property-detail call — `mcp__loop__loop-property-detail` returns these arrays; `heroImageUrl` is `imageUrls[0]` |
|
|
52
|
+
| `pageUrl` | from the Loop property-listed call — the Rightmove / Zoopla / OnTheMarket / agent-website URL for the listing |
|
|
53
|
+
| `blurb` | derived from `description` by a single Haiku call: "Write a single marketing-grade sentence (≤ 280 characters) summarising this UK property listing to a prospective buyer. No estate-agent clichés (no 'rare opportunity', 'must-see', 'stunning'). Plain English. Return only the sentence." Fallback when the LLM fails or returns >280 chars: take the first 280 characters of `description`, normalise whitespace, end on a word boundary |
|
|
54
|
+
| `sourceSystem`, `sourceId`, `ingestedAt`, `lastVerifiedAt` | provenance — set on every write |
|
|
55
|
+
| `scope` | `"public"` for any Listing with `status` in `for-sale` / `under-offer` / `sold` (visitor-renderable). `"shared"` for off-market / withdrawn / pre-marketing pipeline (staff-only) |
|
|
56
|
+
|
|
57
|
+
### Zip-site / hosted-page directory → Listing
|
|
58
|
+
|
|
59
|
+
When a property site has been published from operator-supplied assets, the directory follows the format produced by `property-brochure:property-extract`:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
<property-slug>/
|
|
63
|
+
property.json # structured fields
|
|
64
|
+
hero.jpg | hero.png # primary image
|
|
65
|
+
photos/ # full image set
|
|
66
|
+
floorplans/ # floorplan images
|
|
67
|
+
epc.pdf | epc.png # EPC document
|
|
68
|
+
description.md # full descriptive copy
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Map directly:
|
|
72
|
+
|
|
73
|
+
| Listing field | Source |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `slug` | the directory basename |
|
|
76
|
+
| `displayName`, `addressLine`, `town`, `postcode`, `priceGBP` (`→ price`), `priceQualifier`, `bedrooms`, `bathrooms`, `receptionRooms`, `floorAreaSqft`, `propertyType`, `tenure` | `property.json` keys with the same names (Schema.org camelCase already enforced upstream) |
|
|
77
|
+
| `description` | `description.md` body |
|
|
78
|
+
| `heroImageUrl` | the published URL of `hero.jpg` (passed in by the hosting agent, never invented) |
|
|
79
|
+
| `imageUrls` | published URLs of `photos/*` in directory order — `heroImageUrl` is included at index 0 |
|
|
80
|
+
| `floorplanUrls` | published URLs of `floorplans/*` |
|
|
81
|
+
| `epcUrl` | published URL of `epc.pdf` |
|
|
82
|
+
| `pageUrl` | the live URL the hosting agent emits for `<property-slug>/index.html` |
|
|
83
|
+
| `sourceSystem` | literal `"zip-upload"` |
|
|
84
|
+
| `sourceId` | the directory basename (the slug) — directory basename is the operator-chosen natural key |
|
|
85
|
+
| `blurb` | same LLM rule as the Loop path; same fallback |
|
|
86
|
+
| `status` | from `property.json#status`; default `for-sale` |
|
|
87
|
+
| `scope` | `"public"` |
|
|
88
|
+
|
|
89
|
+
### Manual operator dump → Listing
|
|
90
|
+
|
|
91
|
+
Format: operator pastes a property brief into chat. The curator parses with the same rules as the Loop path (key-value FACTS-style extraction), then asks the operator for any missing required field before writing. `sourceSystem: "manual"`; `sourceId` is whatever natural key the operator gives — they confirm uniqueness.
|
|
92
|
+
|
|
93
|
+
## Source rejection — when not to write
|
|
94
|
+
|
|
95
|
+
Write a `no-op` (no Listing produced; admin agent informed) when:
|
|
96
|
+
|
|
97
|
+
- The source has no FACTS block AND no parseable address — there is no `slug` to derive and no `displayName`. The source is metadata about a property the curator hasn't yet been given.
|
|
98
|
+
- The source declares `status: "off-market"` AND no `pageUrl` is known. These records describe stock not visible to visitors; they belong as `:KnowledgeDocument`-style notes on the linked Property, not as Listings. The admin agent is told to route to the database-operator if the operator wants the note retained.
|
|
99
|
+
- The source's `accountId` cannot be resolved (no LocalBusiness in session env). Listings without a `LISTED_BY` parent cannot be written.
|
|
100
|
+
|
|
101
|
+
`no-op` is loud — the curator returns a one-sentence explanation of which field was missing. Silent skips are defects.
|
|
102
|
+
|
|
103
|
+
## Image curation
|
|
104
|
+
|
|
105
|
+
After writing the Listing, the curator writes one `:ImageObject` node per URL in `imageUrls` and a `[:DEPICTS]` edge from the Listing to each image. ImageObjects are what the public-agent recommender skill picks among when a visitor asks to see a specific room or feature — without them, the agent can only emit the hero image. The schema contract for `:ImageObject` lives in `schema-estate-agent.md`.
|
|
106
|
+
|
|
107
|
+
For each image URL on the Listing:
|
|
108
|
+
|
|
109
|
+
1. **Vision tag pass.** Call `mcp__memory__image-fetch` with the image URL; the tool returns the image bytes as an inline content block your model can see. Then produce `{description, tags}` from what you see using the same single-call doctrine as the blurb step. The description is a single sentence (≤ 200 characters, plain English, no estate-agent clichés). The tags are up to four labels drawn from this fixed set: `kitchen`, `bathroom`, `bedroom-master`, `bedroom-secondary`, `living-room`, `dining-room`, `garden-rear`, `garden-front`, `exterior-front`, `exterior-rear`, `hallway`, `utility`, `floorplan`, `epc`, `view`. The tag set is closed — never invent a tag outside it. When no tag applies, `tags` is the empty array. When `image-fetch` returns `isError`, write the ImageObject anyway with empty `description` and `tags` and log `[listing-curator] image-tag-failed url=<url> reason=<short>` — the validate step below handles that branch.
|
|
110
|
+
2. **Validate.** If the response fails to parse as JSON, or `description` is empty / >200 chars, write the ImageObject anyway with `description: ""` and `tags: []`. Log `[listing-curator] image-tag-failed url=<url> reason=<short>`. One image's tagging failure must not block the rest of the Listing's images — degraded room-pick on that image is the only consequence.
|
|
111
|
+
3. **Write the node.** `MERGE (i:ImageObject {accountId, listingSlug, url})` then `SET i.description, i.tags, i.heroIndex, i.scope, i.sourceSystem`. `scope` mirrors the parent Listing (`public` for visitor-renderable Listings; `shared` otherwise). `heroIndex` is the image's 0-based position in the source `imageUrls` array — `0` for the hero.
|
|
112
|
+
4. **Write the edge.** `MERGE (l:Listing {accountId, slug:listingSlug})-[:DEPICTS]->(i)`. The MERGE on the natural key `(accountId, listingSlug, url)` makes both the node write and the edge write idempotent; re-running the curator over a Listing tops up missing images without duplicating existing ones.
|
|
113
|
+
|
|
114
|
+
No `embedding` is written on `:ImageObject` v1. The recommender picks by reading the Listing's child ImageObjects from hybrid search's `related` set (graph-search expands one hop automatically). If pick quality is poor, a follow-up task will add per-image embeddings; not now.
|
|
115
|
+
|
|
116
|
+
Floorplans and the EPC document follow the same path — they get their own `:ImageObject` with `tags: ["floorplan"]` or `tags: ["epc"]`, written from the curator's `floorplanUrls` and `epcUrl` fields. The hero image carries `tags` reflecting what the photo actually shows (e.g. `["exterior-front"]`) — never the literal tag `hero`. `heroIndex: 0` identifies it.
|
|
117
|
+
|
|
118
|
+
Hybrid search's 1-hop expand caps `related` at 20 neighbours per Listing. Listings with > 20 images lose the surplus to the cap — acceptable for v1 (most Loop listings ≤ 20). Log `[listing-curator] image-cap-warning slug=<slug> imageCount=<N>` when `N > 20` so the gap is visible.
|
|
119
|
+
|
|
120
|
+
## Idempotency and update semantics
|
|
121
|
+
|
|
122
|
+
Every write executes `MERGE (l:Listing {accountId, sourceSystem, sourceId})`. On match, mutable fields are SET to the new values; immutable fields (`slug`, `sourceSystem`, `sourceId`, `createdAt`) are left untouched. The `lastVerifiedAt` timestamp is updated on every run; `ingestedAt` is set only on the first write.
|
|
123
|
+
|
|
124
|
+
`status` is mutable and expected to drift over the listing's life — `for-sale` → `under-offer` → `sold` / `withdrawn`. Repeat curation runs are how that drift lands in the graph.
|
|
125
|
+
|
|
126
|
+
`slug` is immutable on update. If the source's `displayName` changes after first ingest (e.g. an address typo was fixed in Loop), the slug stays at its first-write value to keep the visitor-facing URL stable. If a slug truly must change, that's an operator decision routed via `memory-update`, not a silent curator rewrite.
|
|
127
|
+
|
|
128
|
+
## Migration of legacy `KnowledgeDocument(source: "loop-crm")` records
|
|
129
|
+
|
|
130
|
+
The realagent-code Pi (`192.168.88.16`, Neo4j `bolt://localhost:7688`) holds 29 historical `KnowledgeDocument` nodes with `source: "loop-crm"` carrying the flat `body` text. These predate the curator and must be migrated to `:Listing` nodes.
|
|
131
|
+
|
|
132
|
+
The migration choice: **delete the source `KnowledgeDocument` after the matching `:Listing` is written.** The full `body` text is preserved as the Listing's `description` property — no information is lost — so a parallel raw-record node would be duplicate content. The `KnowledgeDocument` had no incoming edges (per the existing graph state) so deletion is safe; if a future ingestion path needs the raw body retained, the curator's `description` field already holds it.
|
|
133
|
+
|
|
134
|
+
The migration cypher is run once via SSH against the Pi as a manual backfill step; it is not part of the curator's per-record write path. The doctrine the curator carries forward is: any future Loop record passes through the curator on the first write, not as a `:KnowledgeDocument`.
|
|
135
|
+
|
|
136
|
+
## Observability
|
|
137
|
+
|
|
138
|
+
One structured log line per write, emitted by the curator before returning to the admin agent:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
[listing-curator] event=property-curated slug=<slug> sourceSystem=<system> sourceId=<id> action=<create|update|no-op> fieldsFilled=<comma-separated-list> fieldsMissing=<comma-separated-list> scope=<scope> imagesWritten=<N> imagesTagged=<M> imagesFailed=<F>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`fieldsMissing` is the load-bearing signal. Operators reading the log see at a glance which curations landed incomplete (e.g. `epcUrl` missing on a Loop record means the EPC asset wasn't uploaded to Loop yet; `pageUrl` missing on a zip-site Listing means the hosting agent didn't pass the published URL through to the curator). Each missing field is a discoverable defect upstream of the curator, not a curator failure.
|
|
145
|
+
|
|
146
|
+
`imagesWritten` is the total `:ImageObject` count for the Listing after the run. `imagesTagged` is the count with non-empty `tags`. `imagesFailed` counts images where the vision pass failed JSON-validation and a placeholder ImageObject was written with empty description and tags. A high `imagesFailed / imagesWritten` ratio means the vision pass is degrading; investigate the prompt or the source images. When `imagesWritten > 20`, the recommender will only see the first 20 in its `related` set (one-hop expand cap) — emit a separate `[listing-curator] image-cap-warning slug=<slug> imageCount=<N>` line so the cap is visible.
|
|
147
|
+
|
|
148
|
+
## Out of scope
|
|
149
|
+
|
|
150
|
+
- Producing the visitor-facing markdown card from a `:Listing`. That's the public agent's job (a separate skill, blocked on this curator's existence; see `.tasks/`).
|
|
151
|
+
- Inferring missing market-data fields (sold-price comps, demand) — that's the `property-data` MCP, not curator.
|
|
152
|
+
- Writing Property structural fields when the source only carries listing data — Properties are created by the curator only when the source actually provides `tenure`, `epcRating`, etc.; otherwise the Listing stands on its own with the `LISTED_BY` parent edge.
|
package/payload/premium-plugins/real-agent/plugins/brochures/skills/a4-print-documents/SKILL.md
CHANGED
|
@@ -298,7 +298,7 @@ The longest edge of every brochure photo, **measured before any optimisation**,
|
|
|
298
298
|
|
|
299
299
|
The cap is firm — beyond 4000 px the file size grows without visible benefit. The floor is firm too: below it the photo softens noticeably even on screen at 100% zoom, and worse in print.
|
|
300
300
|
|
|
301
|
-
**
|
|
301
|
+
**1600 px source-ceiling caveat.** Many UK listing-CDN exports cap at a 1600 px ceiling (this is a common Loop-CRM tenant default; some portal exports cap similarly). That is below the full-bleed and half-page floors. When the source photos cap below the floor for an assigned slot and no higher-resolution substitute exists, raise this with the user rather than silently shipping a sub-floor cover — a `DONE_WITH_CONCERNS` is the right exit.
|
|
302
302
|
|
|
303
303
|
**Measure before optimising:**
|
|
304
304
|
|
|
@@ -15,7 +15,7 @@ allowed-tools:
|
|
|
15
15
|
|
|
16
16
|
Turn one publicly-accessible website URL into a self-contained brand-design directory that downstream design, marketing, and document-generation skills can consume without re-visiting the source site.
|
|
17
17
|
|
|
18
|
-
The
|
|
18
|
+
The output shape is a brand workspace at `<brand_dir>/` containing the four artifacts defined in **Outcome contract** below. The contract is the format; any prior conforming brand pack on disk may be inspected as a reference, but no specific location is required.
|
|
19
19
|
|
|
20
20
|
## Outcome contract
|
|
21
21
|
|
|
@@ -29,7 +29,7 @@ The skill is **complete** when the target directory contains exactly these four
|
|
|
29
29
|
description.md # company copy + official registration details
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
`<slug>` is the lowercase, hyphen-separated brand name (e.g.
|
|
32
|
+
`<slug>` is the lowercase, hyphen-separated brand name — derived from the brand's own name, not its FQDN (e.g. `<brand>`, not `<brand>realestate.co.uk`). The slug is reused as the directory name unless the caller specifies otherwise — that is, the brand workspace is `<output-dir>/<slug>/` and these four files live at its root.
|
|
33
33
|
|
|
34
34
|
A sibling `properties/` directory may also be present at `<output-dir>/properties/` — it belongs to `property-extract` and **must not** be touched by this skill. Working files this skill creates (screenshots, intermediate JSON, snapshot dumps) must be cleaned up before reporting completion. The four artifacts above are the deliverable; nothing else this skill writes is "scratch" that survives.
|
|
35
35
|
|
|
@@ -153,7 +153,7 @@ A photographic crop of the logo from a screenshot is **not** acceptable — the
|
|
|
153
153
|
|
|
154
154
|
### `description.md`
|
|
155
155
|
|
|
156
|
-
Plain Markdown, no frontmatter.
|
|
156
|
+
Plain Markdown, no frontmatter. Structure:
|
|
157
157
|
|
|
158
158
|
1. Brand name on its own line.
|
|
159
159
|
2. The brand's own tagline / one-liner from the homepage hero.
|
|
@@ -199,13 +199,7 @@ The skill does **not** prescribe which browser tool to use. Where multiple are a
|
|
|
199
199
|
|
|
200
200
|
## Reference standard
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
- `/Users/neo/estate-agents/beacons/DESIGN.md` — the depth and format expected of the token spec.
|
|
205
|
-
- `/Users/neo/estate-agents/beacons/description.md` — the voice and structure expected of the company description.
|
|
206
|
-
- `/Users/neo/estate-agents/beacons/beacons-logo-light.png`, `…-dark.png` — the visual standard for logo crops.
|
|
207
|
-
|
|
208
|
-
A delivered package that diverges substantially in structure from the reference is wrong, even if the content is accurate.
|
|
202
|
+
The four artifact contracts above (DESIGN.md tier-1/2/3 token spec, the two PNG logo files with filename describing the art colour, `description.md` with the official-details block) are the contract. If a prior conforming brand pack exists on disk at any `<brand_dir>/` (look for the four artifacts in the layout shown in **Outcome contract**), inspect it as a worked example. A delivered package that diverges substantially in structure from those contracts is wrong, even if the content is accurate.
|
|
209
203
|
|
|
210
204
|
## Common mistakes
|
|
211
205
|
|
|
@@ -216,7 +210,7 @@ A delivered package that diverges substantially in structure from the reference
|
|
|
216
210
|
| Paraphrasing the homepage hero into "punchier" copy | `description.md` is the source of truth for the brand's voice. The brand's words, not yours. |
|
|
217
211
|
| Inventing token values that the site doesn't use | Token tier 1 is **observed**. `(proposed)` is for tier-2/3 values that fill an obvious gap, not for guesses about what the brand wants. |
|
|
218
212
|
| Including blog posts or case studies in `description.md` | The file is identity, not editorial. |
|
|
219
|
-
| Naming files after the
|
|
213
|
+
| Naming files after the FQDN (`<brand>realestate-logo.png` instead of `<brand>-logo.png`) | Slug is the brand name, not the full domain. |
|
|
220
214
|
| Skipping the mobile viewport during token capture | Type scale and section padding both differ at mobile. A desktop-only tokenization is incomplete. |
|
|
221
215
|
| Generating the dark theme block in `DESIGN.md` from imagination | If the site is light-only, the dark theme is an **inversion derived from the primitives**, marked "proposed", not a parallel design system. |
|
|
222
216
|
| Embedding the company description inside `DESIGN.md` | The artifacts are separable on purpose. Each file has one job. |
|
|
@@ -108,7 +108,7 @@ The skill performs four checks in order. Each rule fully specifies its *conditio
|
|
|
108
108
|
|
|
109
109
|
### 1. Resolve the brand identity
|
|
110
110
|
|
|
111
|
-
If `brand_source` is a URL: derive `brand_slug` as the lowercase domain root with no TLD (e.g. `https
|
|
111
|
+
If `brand_source` is a URL: derive `brand_slug` as the lowercase domain root with no TLD (e.g. `https://<agent>.co.uk` → `<agent>`, `https://example-agent.co.uk` → `example-agent`).
|
|
112
112
|
|
|
113
113
|
If `brand_source` is a path: `brand_slug` = the directory's basename.
|
|
114
114
|
|
|
@@ -132,7 +132,7 @@ If `brand_source` was a path and `DESIGN.md` is missing, treat that as an author
|
|
|
132
132
|
|
|
133
133
|
### 3. Resolve the property identity
|
|
134
134
|
|
|
135
|
-
If `property_source` is a URL: derive `property_slug` from the
|
|
135
|
+
If `property_source` is a URL: derive `property_slug` from whichever path component the source exposes (e.g. `/property/<slug>/<id>/` on Loop-CRM tenants; portals expose different shapes). The folder name is `<property_slug>-<id>` (e.g. `<street-locality-postcode>-<listing-id>`).
|
|
136
136
|
|
|
137
137
|
If `property_source` is a path: the folder name is the directory's basename.
|
|
138
138
|
|
|
@@ -178,6 +178,8 @@ Property photographs almost always exceed this threshold. Concrete examples in t
|
|
|
178
178
|
| `ChatGPT-Image-*` (AI render) | 1024 or 2048px (borderline — measure) |
|
|
179
179
|
| `Screenshot-*` (desktop capture) | 1200–2880px (borderline — measure) |
|
|
180
180
|
|
|
181
|
+
Filename prefixes vary by source — Loop-CRM tenants commonly ship the shapes above; other portals re-serve under their own filename conventions. The 2000 px rule is what binds, not the prefix.
|
|
182
|
+
|
|
181
183
|
### Safe inspection workflow
|
|
182
184
|
|
|
183
185
|
Before any `Read` against an image:
|
|
@@ -212,7 +214,7 @@ The orchestrator does not bundle the upstream skills' outputs — each leaves it
|
|
|
212
214
|
|
|
213
215
|
## Slot-assignment hand-off — verify image content visually, not by filename
|
|
214
216
|
|
|
215
|
-
A repeated failure mode in this pipeline is mis-mapping a slot from filename alone — the brochure step references `images
|
|
217
|
+
A repeated failure mode in this pipeline is mis-mapping a slot from filename alone — the brochure step references `images/<property-slug>-22.webp` because slot 22 should be the inglenook, but the actual photograph at index 22 is a downstairs WC. Filename tells you order; pixels tell you content.
|
|
216
218
|
|
|
217
219
|
The orchestrator's role is to **enforce the visual-verification step** before `property-brochure` writes the HTML:
|
|
218
220
|
|
|
@@ -290,11 +292,9 @@ After a complete run, the on-disk layout is **exactly** this — no deeper nesti
|
|
|
290
292
|
|
|
291
293
|
The `output/` folder is a portable deliverable: every image referenced by `brochure.html` resolves to a file under `output/images/`. References that escape `output/` (`../images/`, `../../floorplans/`, `../../epc/`) are a defect — the brochure must not depend on the sibling property-extract folders staying in place.
|
|
292
294
|
|
|
293
|
-
|
|
294
|
-
- `/Users/neo/estate-agents/muvin/` (brand_slug = `muvin`)
|
|
295
|
-
- `/Users/neo/estate-agents/beacons/` (brand_slug = `beacons`)
|
|
295
|
+
The brand-workspace set is whatever directories exist under the caller's `<output_root>/` — enumerate them with `ls <output_root>` at runtime rather than assuming a fixed set. Each brand directory is its own workspace and matches the `<brand_slug>/` layout above.
|
|
296
296
|
|
|
297
|
-
A fresh run with no `output_root` and no existing brand pack lands the workspace in the caller's CWD
|
|
297
|
+
A fresh run with no `output_root` and no existing brand pack lands the workspace in the caller's CWD as `./<brand_slug>/`, not under any other path.
|
|
298
298
|
|
|
299
299
|
Determinism rules — a run that violates any of these is wrong, even if the brochure renders:
|
|
300
300
|
- `properties/` is **inside** `<brand_dir>/`, never a sibling.
|