@rubytech/create-maxy-code 0.1.110 → 0.1.112
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/index.js +39 -25
- package/package.json +1 -1
- package/payload/platform/neo4j/schema.cypher +6 -3
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +3 -3
- package/payload/platform/plugins/cloudflare/references/reset-guide.md +5 -3
- package/payload/platform/plugins/memory/references/schema-estate-agent.md +51 -1
- package/payload/platform/plugins/venture-studio/PLUGIN.md +6 -1
- package/payload/platform/plugins/venture-studio/bin/scaffold.sh +1 -1
- 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 +6 -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/zero-to-prototype/SKILL.md +4 -0
- package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
- package/payload/premium-plugins/real-agent/BUNDLE.md +3 -2
- package/payload/premium-plugins/real-agent/agents/listing-curator.md +133 -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/venture-studio/PLUGIN.md +6 -1
- package/payload/premium-plugins/venture-studio/bin/scaffold.sh +1 -1
- 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 +6 -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/zero-to-prototype/SKILL.md +4 -0
|
@@ -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 + 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 |
|
|
30
31
|
|
|
31
32
|
## Sub-Plugins
|
|
32
33
|
|
|
@@ -0,0 +1,133 @@
|
|
|
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__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
|
+
## Idempotency and update semantics
|
|
104
|
+
|
|
105
|
+
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.
|
|
106
|
+
|
|
107
|
+
`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.
|
|
108
|
+
|
|
109
|
+
`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.
|
|
110
|
+
|
|
111
|
+
## Migration of legacy `KnowledgeDocument(source: "loop-crm")` records
|
|
112
|
+
|
|
113
|
+
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.
|
|
114
|
+
|
|
115
|
+
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.
|
|
116
|
+
|
|
117
|
+
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`.
|
|
118
|
+
|
|
119
|
+
## Observability
|
|
120
|
+
|
|
121
|
+
One structured log line per write, emitted by the curator before returning to the admin agent:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
[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>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`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.
|
|
128
|
+
|
|
129
|
+
## Out of scope
|
|
130
|
+
|
|
131
|
+
- 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/`).
|
|
132
|
+
- Inferring missing market-data fields (sold-price comps, demand) — that's the `property-data` MCP, not curator.
|
|
133
|
+
- 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.
|
package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-extract/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: property-extract
|
|
3
|
-
description: Use when a live property listing URL on
|
|
3
|
+
description: Use when a live property listing URL on any UK estate-agent website needs to be turned into a local property assets directory containing photos, floorplans, EPC documents, and structured metadata that the property-brochure skill can consume. Trigger phrases include "extract property from <url>", "get property details from <url>", "scrape this listing", "pull all the photos from <listing>", "stage assets for the brochure", or any request that names a live property URL and asks for the photos, floorplans, and details to be saved locally.
|
|
4
4
|
allowed-tools:
|
|
5
5
|
- Bash
|
|
6
6
|
- Read
|
|
@@ -15,7 +15,7 @@ allowed-tools:
|
|
|
15
15
|
|
|
16
16
|
Turn one publicly-accessible property listing URL into a self-contained property assets directory that `property-brochure` can consume without re-visiting the source site.
|
|
17
17
|
|
|
18
|
-
The
|
|
18
|
+
The contract is **portal-agnostic**: the output shape is the same whether the source is an agent's own site (Muvin / Loop CRM tenants, custom builds) or a portal (Rightmove, Zoopla, OnTheMarket, OpenRent). The static-HTML probe is the cheap default; when it returns no asset URLs or the data is tab-bound, fall back to a headless browser. See **Process — high level**.
|
|
19
19
|
|
|
20
20
|
## Outcome contract
|
|
21
21
|
|
|
@@ -34,7 +34,7 @@ Every photograph and floorplan referenced by the listing must be downloaded. `ep
|
|
|
34
34
|
|
|
35
35
|
Do **not** pre-create a `brochure/` slot. `property-brochure` writes its deliverable directly to `<property_dir>/output/` — see `property-brochure/SKILL.md → Reference standard`. An empty `brochure/` next to `output/` is the legacy layout and a defect.
|
|
36
36
|
|
|
37
|
-
`<property-slug>` comes from the URL path segment (e.g.
|
|
37
|
+
`<property-slug>` comes from the URL path segment (e.g. `<street-locality-postcode>` for Loop-CRM-style URLs, or whatever slug component the source portal exposes). `<listing-id>` is the numeric ID, also from the URL. Concatenating them keeps the folder both human-readable and unique even when the same street has multiple listings.
|
|
38
38
|
|
|
39
39
|
Anything else (raw HTML dumps, `/tmp` files, intermediate URL lists) must be cleaned up before reporting completion.
|
|
40
40
|
|
|
@@ -42,7 +42,7 @@ Anything else (raw HTML dumps, `/tmp` files, intermediate URL lists) must be cle
|
|
|
42
42
|
|
|
43
43
|
Required: a property listing URL, fully-formed and publicly accessible. If only a postcode or property name is given, decline and ask for the URL — listings are uniquely identified by their numeric IDs, and there is no reliable way to infer one from a description.
|
|
44
44
|
|
|
45
|
-
Optional: an output directory. The default is `<brand_dir>/properties/`, where `<brand_dir>` is the brand workspace for the agent that owns the listing.
|
|
45
|
+
Optional: an output directory. The default is `<brand_dir>/properties/`, where `<brand_dir>` is the brand workspace for the agent that owns the listing. If a brand pack already exists at `<brand_dir>/DESIGN.md`, properties nest under `<brand_dir>/properties/`. If the brand workspace cannot be located, fall back to `./properties/` in the caller's current working directory.
|
|
46
46
|
|
|
47
47
|
The property directory **must** sit inside its brand workspace at `<brand_dir>/properties/<property_slug>-<listing_id>/` — never as a sibling of the brand workspace, never inside another property's folder. This is the contract `make-brochure` and `property-brochure` read against.
|
|
48
48
|
|
|
@@ -50,14 +50,15 @@ The property directory **must** sit inside its brand workspace at `<brand_dir>/p
|
|
|
50
50
|
|
|
51
51
|
| In scope | Out of scope |
|
|
52
52
|
|---|---|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
53
|
+
| Download every CDN-hosted image, floorplan and EPC asset referenced by the listing page | Stripping AI-generated images from the photo set — keep them all and flag in metadata |
|
|
54
|
+
| Extract description copy verbatim from whichever description container the page exposes | Paraphrasing or rewriting the description — the brochure step owns voice |
|
|
55
|
+
| Capture a screenshot of any map element by heuristic (provider-agnostic — Google Maps iframe, Mapbox canvas, static-map `<img>`, custom embed) | Synthesising a map when the listing has none (no postcode-centroid renders, no AI maps) |
|
|
55
56
|
| Property description, key features, price, specs, agent contact | Walkthrough video transcription (the property-brochure skill handles that separately if the user supplies one) |
|
|
56
57
|
| EPC rating as a value, EPC certificate PDF if linked from the page | EPC certificate retrieval from the EPC Register — that's a separate lookup; if the listing does not link the certificate, leave `epc/` empty and record `"epc_rating": "TBC"` (or the band shown) in `property.json` |
|
|
57
58
|
| Council tax band as shown on the page | Council tax band verification against VOA — out of scope |
|
|
58
59
|
| Material Information visible on the listing page (tenure, council tax, EPC) | Full Part A/B/C compliance gathering — that's the brochure assembly step's job |
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
The skill is portal-agnostic. The Loop-CRM CDN pattern (`*.b-cdn.net/propertyimages/...`, `*.b-cdn.net/floorplans/...`) is one common example that the static-HTML probe handles cheaply; agency-specific platforms (Rightmove, Zoopla, OnTheMarket, OpenRent, custom builds) JS-render most data and require the Playwright fallback documented in **Process — high level** step 4b. When the static probe returns no asset URLs or critical fields are tab-bound, escalate to the headless-browser fallback as a first-class path, not a conditional one. If even that returns nothing, stop and report `STATUS: BLOCKED` — do not synthesise an asset list.
|
|
61
62
|
|
|
62
63
|
## Artifact contracts
|
|
63
64
|
|
|
@@ -67,12 +68,9 @@ The three subfolders (`images/`, `floorplans/`, `epc/`) are **mandatory and name
|
|
|
67
68
|
|
|
68
69
|
### `images/`
|
|
69
70
|
|
|
70
|
-
Every URL on the page
|
|
71
|
+
Every URL on the page pointing at an image asset is in scope. On Loop-CRM tenants the pattern is `b-cdn.net/propertyimages/<tenant-uuid>/<listing-id>/` — two example buckets in active use as of April 2026 are `loop-app.b-cdn.net` (older 2024-vintage uploads) and `loopcrm.b-cdn.net` (newer 2026 additions including drone shots, AI-staged renders, screenshots). Other portals serve from their own hosts (e.g. Rightmove's `media.rightmove.co.uk`); the rule is the same — grep every image URL the page emits, deduplicate, download all of them.
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
- `loopcrm.b-cdn.net` — newer 2026 additions (drone shots, AI-staged renders, screenshots)
|
|
74
|
-
|
|
75
|
-
Download all of them. Preserve the original filenames — they encode upload timestamps as Unix epochs (e.g. `DSC01495_1727361503898.jpg` → 26 Sept 2024) which are useful for ordering and de-duplication later.
|
|
73
|
+
Preserve the original filenames — many tenants encode upload timestamps as Unix epochs in the filename (e.g. `DSC01495_1727361503898.jpg` → 26 Sept 2024) which are useful for ordering and de-duplication later. Where filenames carry no timestamp, fall back to download order.
|
|
76
74
|
|
|
77
75
|
Image categories observable from filename prefixes:
|
|
78
76
|
|
|
@@ -88,7 +86,7 @@ Record the count per prefix in `property.json` under `media.image_breakdown` —
|
|
|
88
86
|
|
|
89
87
|
#### Curator-friendly rename — "<Property Name> N.jpg"
|
|
90
88
|
|
|
91
|
-
After downloading, **also rename the images to `<Property Name> N.jpg`** (1-indexed, ordered by upload timestamp parsed from the original filename) so a human curator can browse `images/` in Finder/Preview and intuitively pick which photo goes in which slot. The downstream `property-brochure` slot map references `images/<property-slug>-NN.webp`, where `NN` matches the curator-chosen `N` 1:1 — so renaming source images to that order at extract time makes the entire pipeline traceable: the curator sees
|
|
89
|
+
After downloading, **also rename the images to `<Property Name> N.jpg`** (1-indexed, ordered by upload timestamp parsed from the original filename) so a human curator can browse `images/` in Finder/Preview and intuitively pick which photo goes in which slot. The downstream `property-brochure` slot map references `images/<property-slug>-NN.webp`, where `NN` matches the curator-chosen `N` 1:1 — so renaming source images to that order at extract time makes the entire pipeline traceable: the curator sees `<Property Name> 22.jpg` in Finder, the brochure HTML references `images/<property-slug>-22.webp`, and a search across either resolves to the same photograph.
|
|
92
90
|
|
|
93
91
|
The rename is **not** a replacement of the original CDN-named files — those are preserved as-is for timestamp ordering and de-duplication. The renamed files are the same bytes (or a `cp`/`ln` of the originals), placed alongside, with the new `<Property Name> N.jpg` filenames.
|
|
94
92
|
|
|
@@ -105,15 +103,15 @@ for src in $(ls -1 | grep -E '^(DSC|DJI|ChatGPT-Image|Screenshot|IMG|iPhone|What
|
|
|
105
103
|
done
|
|
106
104
|
```
|
|
107
105
|
|
|
108
|
-
The `<Property Name>` is taken from `property.json -> property_name` (see *Property name vs address* below). If the property has no distinct name (uncommon at this register), use the first part of the address (
|
|
106
|
+
The `<Property Name>` is taken from `property.json -> property_name` (see *Property name vs address* below). If the property has no distinct name (uncommon at this register), use the first part of the address (the street name).
|
|
109
107
|
|
|
110
108
|
Record the original-to-curator-name mapping in `property.json -> media.image_curator_map` so the rename is reversible:
|
|
111
109
|
|
|
112
110
|
```json
|
|
113
111
|
"media": {
|
|
114
112
|
"image_curator_map": {
|
|
115
|
-
"
|
|
116
|
-
"
|
|
113
|
+
"The Old Rectory 1.jpg": "DJI_0125_1777122816019.jpg",
|
|
114
|
+
"The Old Rectory 2.jpg": "DJI_0115_1777122829462.jpg",
|
|
117
115
|
"...": "..."
|
|
118
116
|
}
|
|
119
117
|
}
|
|
@@ -121,7 +119,7 @@ Record the original-to-curator-name mapping in `property.json -> media.image_cur
|
|
|
121
119
|
|
|
122
120
|
### `floorplans/`
|
|
123
121
|
|
|
124
|
-
|
|
122
|
+
Every URL on the page pointing at a floorplan asset is in scope. On Loop-CRM tenants the pattern is `b-cdn.net/floorplans/<tenant-uuid>/<listing-id>/`; other portals serve from their own hosts. Listings typically ship one floorplan per floor (so 1–3 files for a UK home). Preserve original filenames.
|
|
125
123
|
|
|
126
124
|
### `epc/`
|
|
127
125
|
|
|
@@ -131,7 +129,7 @@ If the page links a PDF whose URL contains `epc` or whose link text mentions "EP
|
|
|
131
129
|
|
|
132
130
|
**Maps are extracted, never synthesized.** If the listing doesn't supply one, the brochure ships without a map. No fallback to OSM static maps, no postcode-geocoded re-render via postcodes.io, no AI-generated map artwork. Postcode centroids can mislead about exact location, and the brochure should reflect what the agency actually publishes — not what the pipeline could imagine.
|
|
133
131
|
|
|
134
|
-
The capture is **provider-agnostic by design.** Estate agents embed maps in many ways: Google Maps iframes (Loop
|
|
132
|
+
The capture is **provider-agnostic by design.** Estate agents embed maps in many ways: Google Maps iframes (common on Loop-CRM tenants), Mapbox or Leaflet `<div>`+canvas instantiations (custom builds), `<img>` static-map renders (used by some portal-style listings), Apple Maps embeds, custom-class wrappers — and that list will only grow. Don't hard-code a single CMS's markup. Instead: **find a map element by heuristic, screenshot it, save the bytes.** The provider's brand and tile style come along for the ride.
|
|
135
133
|
|
|
136
134
|
**Detect.** Render the listing page in a real browser (Playwright). Try these heuristics in order, taking the first non-empty hit:
|
|
137
135
|
|
|
@@ -166,21 +164,21 @@ The rule, in one line: **extract or omit**.
|
|
|
166
164
|
|
|
167
165
|
### Property name vs address — they are not the same field
|
|
168
166
|
|
|
169
|
-
At the super-premium register, many properties have a **distinct name** that is separate from the postal address: *
|
|
167
|
+
At the super-premium register, many properties have a **distinct name** that is separate from the postal address: *The Old Rectory*, *Mill House*, *Beacon House*. The brochure's cover hero is the property name; the subtitle is the address. The two must be captured separately at extraction time.
|
|
170
168
|
|
|
171
169
|
Detect the property name by:
|
|
172
170
|
|
|
173
|
-
1. Looking for "<Name>, <street>" patterns in the listing's title or H1 (e.g. "
|
|
174
|
-
2. Looking for a name that is *not* a numbered street address (numbered addresses like "12
|
|
175
|
-
3. Cross-checking with a Sprift / Land Registry report — if the title is registered as "
|
|
171
|
+
1. Looking for "<Name>, <street>" patterns in the listing's title or H1 (e.g. "The Old Rectory, Mill Lane").
|
|
172
|
+
2. Looking for a name that is *not* a numbered street address (numbered addresses like "12 Mill Lane" don't have a separate name).
|
|
173
|
+
3. Cross-checking with a Sprift / Land Registry report — if the title is registered as "<Name> at <street>", that's the property name.
|
|
176
174
|
|
|
177
175
|
Record both in `property.json`:
|
|
178
176
|
|
|
179
177
|
```json
|
|
180
|
-
"property_name": "
|
|
178
|
+
"property_name": "The Old Rectory", // or null if the property has no distinct name
|
|
181
179
|
"address": {
|
|
182
|
-
"line_1": "
|
|
183
|
-
"locality": "
|
|
180
|
+
"line_1": "Mill Lane",
|
|
181
|
+
"locality": "<Locality>",
|
|
184
182
|
"...": "..."
|
|
185
183
|
}
|
|
186
184
|
```
|
|
@@ -309,29 +307,24 @@ Capture rules:
|
|
|
309
307
|
|
|
310
308
|
## Image preview safety
|
|
311
309
|
|
|
312
|
-
**Do not `Read` any downloaded image whose longest edge exceeds 2000px** — it breaks the session by overrunning the multimodal encoding budget. Most DSLR (`DSC*`) and drone (`DJI_*`) photos from
|
|
310
|
+
**Do not `Read` any downloaded image whose longest edge exceeds 2000px** — it breaks the session by overrunning the multimodal encoding budget. Most DSLR (`DSC*`) and drone (`DJI_*`) photos from any premium listing are 6000–8000px and will trigger this. Measure with `sips -g pixelWidth -g pixelHeight <file>` before any `Read`, or skip the visual check entirely (filename, byte-size, and `sips` dimensions cover most "did the download work?" questions). See `make-brochure` for the full safety workflow including the `sips -Z 2000` downscale recipe.
|
|
313
311
|
|
|
314
312
|
## Process — high level
|
|
315
313
|
|
|
316
314
|
The skill orchestrates four capabilities, each a **what** not a **how**:
|
|
317
315
|
|
|
318
|
-
1. **Probe.** Fetch the URL once with `WebFetch` to get a summarised view (price, address, agent, image count). This is a cheap sanity check — if the summary disagrees with what you extract from raw HTML, the page is
|
|
319
|
-
2. **Extract.**
|
|
316
|
+
1. **Probe.** Fetch the URL once with `WebFetch` to get a summarised view (price, address, agent, image count). This is a cheap sanity check — if the summary disagrees with what you extract from raw HTML, the page is JS-rendered and you need a headless browser.
|
|
317
|
+
2. **Extract — tiered.** Try the static-HTML path first because it's cheap: pull the raw HTML and grep every image-asset URL (Loop-CRM tenants use `b-cdn.net/propertyimages/...` and `b-cdn.net/floorplans/...`; other portals serve from their own hosts), deduplicate, grep PDF URLs containing `epc`, extract any visible description container. **If the static probe returns zero image URLs, or the probed summary in step 1 reports content the static HTML does not contain (EPC band, floorplan thumbnail, video URL), escalate to the Playwright fallback in step 4b** — that's the first-class path for portals and tabbed UIs, not a conditional one. (Map detection is provider-agnostic and happens in step 4 below — don't try to parse map markup at this static-HTML stage.)
|
|
320
318
|
3. **Stage.** Create the four-folder structure under `<output-dir>/<slug>-<listing-id>/`. Download images and floorplans in parallel (`xargs -n1 -P8 curl -O -L`) — there are routinely 40–60 images per listing and serial downloads are slow.
|
|
321
319
|
4. **Capture map** (conditional, provider-agnostic). Drive Playwright to the listing URL and apply the heuristic detector under **Map — extract or omit**: try iframe-by-provider, then class/id-by-keyword, then static-map images, then visual fallback. On the first hit, scroll into view, wait for tiles, screenshot the **element** (not the page), convert to `map.webp` at ~1500 px wide. If no heuristic finds a map, skip — **extract or omit**, no fallback synthesis.
|
|
322
|
-
4b. **Traverse tabbed UIs (
|
|
320
|
+
4b. **Traverse tabbed UIs and JS-rendered portals (first-class for non-static hosts).** Modern listing pages from Loop CRM tenants, Rightmove, Zoopla, OnTheMarket and others render top-level data in tabs — *Details / Floorplan / EPC / Virtual Tour / Map View / Book Viewing* — and the static HTML returned by `WebFetch` or `curl` often does not include the inactive-tab content. Whenever step 2's static probe came up short, drive a Playwright session against the listing URL and walk the tab list: detect elements with `role="tablist"`, `role="tab"`, or class/id substrings `tab` containing the keywords `EPC`, `Floor plan`, `Map`, `Virtual tour`, `Brochure`. For each tab found, `page.click(<selector>)`, `page.waitForLoadState('networkidle')`, then harvest the panel content. For the EPC tab specifically: scrape the certificate PDF/PNG link and the displayed current/potential scores into `specifications.epc_rating`, `specifications.epc_current_score`, `specifications.epc_potential_score`. For other tabs: extract what each contract field needs and skip the rest. The traversal is principled — never hard-code a single agency's or portal's tab markup as the sole detector; the same listing surface ships with different tab implementations across CMS tenants and portal layouts.
|
|
323
321
|
5. **Assemble.** Write `property.json` and `description.md`. Run a final consistency pass: image count in JSON matches files on disk, EPC rating in JSON matches the description.md line, agent contact in JSON matches the description.md header. `media.map` is populated iff a map screenshot landed on disk; `address.geo` is populated iff coordinates were parseable from a recognised URL pattern (and may legitimately be `null` even when `media.map` is set — see the schema notes).
|
|
324
322
|
|
|
325
|
-
The skill does not prescribe which HTTP tool to use. `curl` is fine for
|
|
323
|
+
The skill does not prescribe which HTTP tool to use. `curl` is fine for static-HTML hosts (most Loop-CRM tenants serve listing data inline); switch to Chrome DevTools MCP or Playwright for JS-rendered listing pages and portals — the contract on the output is unchanged.
|
|
326
324
|
|
|
327
325
|
## Reference standard
|
|
328
326
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
- `/Users/neo/estate-agents/muvin/properties/henham-road-debden-green-hamperden-end-cb11-3lz-949931/property.json` — the JSON schema in practice.
|
|
332
|
-
- `/Users/neo/estate-agents/muvin/properties/henham-road-debden-green-hamperden-end-cb11-3lz-949931/description.md` — the prose voice and structure.
|
|
333
|
-
|
|
334
|
-
A delivered package that diverges substantially in shape from the reference is wrong, even if the content is accurate.
|
|
327
|
+
The format contract is defined by `property.json` (schema above) and `description.md` (structure above). If the caller has a prior conforming property pack on disk (under any `<brand_dir>/properties/<slug>-<id>/`), read its `property.json` and `description.md` to anchor the format. A delivered package that diverges substantially in shape from those is wrong, even if the content is accurate.
|
|
335
328
|
|
|
336
329
|
## Common mistakes
|
|
337
330
|
|
|
@@ -343,8 +336,8 @@ A delivered package that diverges substantially in shape from the reference is w
|
|
|
343
336
|
| Inventing values for unknown specifications | `null` means "not on the listing". A guess at tenure or EPC introduces silent error into downstream Material Information compliance. |
|
|
344
337
|
| Including the agency's marketing boilerplate ("WOULD YOU LIKE TO VIEW", "CAN WE HELP YOU TOO") in `description.md` | The brochure has its own back page. Including it would duplicate text the brand designer is already authoring. |
|
|
345
338
|
| Skipping `floorplans/` because the listing only has one | One floorplan still goes in `floorplans/`, not `images/`. The folder layout is a contract — `property-brochure` looks for floorplans in exactly one place. |
|
|
346
|
-
| Saving `/tmp/
|
|
347
|
-
| Hardcoding the tenant UUID |
|
|
339
|
+
| Saving `/tmp/listing_raw.html` or other working files inside the output directory | The directory is the deliverable. Working files get cleaned up. |
|
|
340
|
+
| Hardcoding the tenant UUID | On Loop-CRM tenants the UUID identifies the agency; different offices have different tenant IDs. Extract it from the actual URLs on the page rather than assuming a single value. |
|
|
348
341
|
| Treating "EPC: TBC" as a missing field | "TBC" is information — the agent is telling you the assessment hasn't been done. Record it verbatim. |
|
|
349
342
|
| Recording "EPC: TBC" when the listing's EPC tab actually shows a band but the static HTML did not | Modern listings tab-hide data behind a JS tab switcher; `WebFetch` / `curl` see only the active tab. If the listing visibly carries EPC content the static HTML lacks, drive Playwright per step 4b and harvest from the tab panel. "TBC" recorded from a missed-tab extract is a silent data-loss defect, not a faithful record. |
|
|
350
343
|
| Synthesizing a map when the listing has none | The rule is **extract or omit**. Postcode-centroid renders, OSM static maps, and AI-generated map artwork are all forbidden. If the heuristic detector returns nothing, write `media.map: null` and move on. |
|
|
@@ -369,6 +362,5 @@ If any of those are not true, report `DONE_WITH_CONCERNS` and list each gap.
|
|
|
369
362
|
## When NOT to use this skill
|
|
370
363
|
|
|
371
364
|
- The user wants a brochure produced from photos they already have on disk → skip extraction, go straight to `property-brochure`.
|
|
372
|
-
- The listing is on Rightmove, Zoopla, OnTheMarket, or another non-Loop-CRM platform → the URL parsing and CDN paths differ. Either extend this skill with a new branch or route to a platform-specific extractor.
|
|
373
365
|
- The user wants a quick price/specs lookup, not the full asset bundle → use `WebFetch` directly. The folder structure is overkill.
|
|
374
366
|
- The user wants market data, comparables, or transaction history → that's a different research task; this skill is asset staging, not analysis.
|
package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-market-report/SKILL.md
CHANGED
|
@@ -19,9 +19,9 @@ The report has its own typographic identity — a luxury serif stack pinned in t
|
|
|
19
19
|
|
|
20
20
|
## Inputs (never invented)
|
|
21
21
|
|
|
22
|
-
- **`design_tokens_path`** — path to a `DESIGN.md` that tokenises the agent's brand
|
|
23
|
-
- **`logo_light_path`** / **`logo_dark_path`** — PNG/SVG. Light = white-on-dark version (for the teal-toned cover); dark = full-colour version (for body-page headers). When supplied, the renderer emits an `<img>` for the logo; when omitted, it falls back to a Cormorant-Garamond text wordmark using `brand.name` + `brand.tagline`. The text-wordmark fallback is acceptable for brands whose logo is essentially a serif-typeset name
|
|
24
|
-
- **`property_urls`** — list of up to 10 source URLs (
|
|
22
|
+
- **`design_tokens_path`** — path to a `DESIGN.md` that tokenises the agent's brand. Resolve it from `brand_source` at runtime: when `brand_source` is a URL, the path is `<brand_dir>/DESIGN.md` for whatever brand directory the orchestrator created (or already had on disk); when `brand_source` is a directory path, the file is at `<brand_source>/DESIGN.md`. Read the canonical hex values directly; never substitute "close" colours. **Only colour tokens are consumed** — the report imports `primary` (e.g. teal-900), `primary_dark`, `accent` (e.g. gold-500), `paper`, `paper_banded`, and `rule`. Any typography, spacing, or radius tokens in the agent's DESIGN.md are *ignored by design*.
|
|
23
|
+
- **`logo_light_path`** / **`logo_dark_path`** — PNG/SVG. Light = white-on-dark version (for the teal-toned cover); dark = full-colour version (for body-page headers). When supplied, the renderer emits an `<img>` for the logo; when omitted, it falls back to a Cormorant-Garamond text wordmark using `brand.name` + `brand.tagline`. The text-wordmark fallback is acceptable for brands whose logo is essentially a serif-typeset name; for richer marks, supply the files.
|
|
24
|
+
- **`property_urls`** — list of up to 10 source URLs from any UK estate-agent or portal site (Loop-CRM tenants, Rightmove, Zoopla, OnTheMarket, etc.). Each must resolve to a single property listing page. Reject more than 10; the layout breaks at >10 cards.
|
|
25
25
|
- **`market_data`** — flat dict of numbers (average asking, average sold, £/sqft, YoY growth, demand rating, days-on-market, property-type distribution, sample size, period). Source from a market intelligence API (PropertyData / Land Registry / ONS) or paste from an existing report. **The period is the *generated* date, not whatever appears in the URL slug** — the Market Trak `oct-2025` slug describes a snapshot from May 2026 (Oct 2025 was the trailing window). Use the report-page's stated `Generated` value.
|
|
26
26
|
|
|
27
27
|
## How the skill renders
|
|
@@ -90,7 +90,7 @@ The disclaimer block on the final page must name:
|
|
|
90
90
|
- **Logo invisible in preview.** The IDE preview panel can't always resolve relative `img/` paths or large base64 data URIs. The deliverable PDF is the source of truth; viewing the HTML in a real browser (or via `python3 -m http.server`) is the second source of truth. Don't burn iterations chasing preview-panel artefacts.
|
|
91
91
|
- **Date drift from URL slug.** A Market Trak URL like `…-oct-2025?…` does not mean the report is October 2025. Always trust the `Generated:` field on the source page.
|
|
92
92
|
- **Page 3 / page 4 overflow.** The default Cormorant Garamond + Lora stack is slightly taller-set than a sans default. KPI tile padding and panel padding need to come down (~5 mm not 7 mm) for serif typography to fit alongside the photo strip on page 3 and two listing cards on page 4.
|
|
93
|
-
- **Property-socials skipped.** A report that drops raw `loop-app.b-cdn.net/propertyimages/...jpg`
|
|
93
|
+
- **Property-socials skipped.** A report that drops the raw listing-CDN image straight into a tile (whether `loop-app.b-cdn.net/propertyimages/...jpg`, `media.rightmove.co.uk/...`, or any other host) breaks the brand contract — the tiles have no overlay, no scrim, no address copy. Always run the OG pipeline; if it can't run, surface the gap, don't substitute.
|
|
94
94
|
- **Anchor links dead in PDF.** Chrome `--print-to-pdf` preserves `<a href>` as live PDF annotations. Verify by opening the PDF in Preview and clicking a listing — if the link is dead, the HTML wrapped a `<div>` instead of an `<a>`.
|
|
95
95
|
|
|
96
96
|
## Out of scope
|
package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-socials/SKILL.md
CHANGED
|
@@ -19,8 +19,9 @@ shots — it does not re-design anything.
|
|
|
19
19
|
that page. Do not pick new colours.
|
|
20
20
|
- **Copy** — `property.json` is canonical for address, price, qualifier,
|
|
21
21
|
bedrooms/baths/receptions, agent name + phone. The page's
|
|
22
|
-
`<meta name="description">` is the sub-headline. The
|
|
23
|
-
|
|
22
|
+
`<meta name="description">` is the sub-headline. The property's display
|
|
23
|
+
name (`property.json -> property_name`, or the first address line if the
|
|
24
|
+
property has no distinct name) is the headline.
|
|
24
25
|
- **Hero images** — `images/<slug>-hero-1-main.webp`, `hero-2-kitchen.webp`,
|
|
25
26
|
`hero-3-garden.webp` are the curated set. Use them by name; do not pick
|
|
26
27
|
numbered shots from the raw set unless the curated heroes are missing.
|
|
@@ -4,6 +4,7 @@ description: "Founding-a-business agent. Drives an operator from raw idea to fun
|
|
|
4
4
|
requires:
|
|
5
5
|
- projects
|
|
6
6
|
- work
|
|
7
|
+
- deep-research
|
|
7
8
|
metadata: {"platform":{"always":false,"embed":["admin"],"pluginKey":"venture-studio","optional":true,"recommended":true}}
|
|
8
9
|
---
|
|
9
10
|
|
|
@@ -62,8 +63,8 @@ The script's three effects:
|
|
|
62
63
|
2. **Creates the `Project` node.** Tier `full` (data-room work is multi-phase). Project name is the business's working name; description references the data-room root path.
|
|
63
64
|
3. **Enumerates the eight artefact Tasks** in section order — one transaction, atomic with the Project. The work-item list the script pre-seeds:
|
|
64
65
|
- **Stage 1 — Office-hours design doc** → produces `01-narrative/office-hours-design.md` (skill: `office-hours`)
|
|
65
|
-
- **Brand pack** → produces brand identity files into `06-product-ip/brand/` (skill: `brand-pack`)
|
|
66
66
|
- **Stage 2 — Wedge validation + landing page + PRD** → produces `01-narrative/{PMF, LANDING, PRD}.md` (skill: `zero-to-prototype`)
|
|
67
|
+
- **Brand pack** → produces brand guidelines + summary + color palette + typography system + tone-of-voice (voice/messaging/copy) + logo guidelines into `06-product-ip/brand/` (skill: `brand-pack`)
|
|
67
68
|
- **Stage 3 — Business plan** → produces `01-narrative/business-plan.md` (skill: `investor-data-room` Stage 3)
|
|
68
69
|
- **Stage 3b — Term sheet** → produces `html/prospectus/term_sheet.html` (skill: `investor-data-room` Stage 3b)
|
|
69
70
|
- **Stage 4 — Deck blueprint** → produces `01-narrative/deck-blueprint.md` (skill: `investor-data-room` Stage 4)
|
|
@@ -96,6 +97,10 @@ When the operator's intent matches a row but the scaffold does not yet exist, ru
|
|
|
96
97
|
- If a section reveals a new artefact the operator wants (e.g. a competitor-claims-rebuttal in `08-market/`), `work-create` a new Task under the same Project, then proceed.
|
|
97
98
|
- The Project's health signal (green/amber/red) is computed from Task state. Use `project-get` at the start of each turn to surface what's outstanding.
|
|
98
99
|
|
|
100
|
+
## Research routing
|
|
101
|
+
|
|
102
|
+
Every structured-research task — market sizing (TAM/SAM/SOM + CAGR), compliance research (DMCC / ICO ADM / EU AI Act), competitor scans, comparable rounds and exit comparables, named-acquirer enumeration — routes through the `research-assistant` specialist with the five `deep-research` skills (`deep-research`, `book-mirror`, `strategic-reading`, `academic-verify`, `data-research`). Dispatch the specialist; do not call `WebSearch` or `WebFetch` directly from a venture-studio skill. The specialist returns structured findings with citations the data-room artefacts can quote verbatim.
|
|
103
|
+
|
|
99
104
|
## Tone
|
|
100
105
|
|
|
101
106
|
Founder-facing. Direct, evidence-based, no flattery. You are the chief of staff for a one-person founding team — your job is to keep every artefact tracked and every deadline visible, while the operator focuses on the parts only they can do (customer conversations, founder signal, vision).
|
|
@@ -64,8 +64,8 @@ PAYLOAD=$(cat <<JSON
|
|
|
64
64
|
"tier": "full",
|
|
65
65
|
"workItems": [
|
|
66
66
|
{"name": "Stage 1 — Office-hours design doc", "description": "Produces 01-narrative/office-hours-design.md via the office-hours skill."},
|
|
67
|
-
{"name": "Brand pack", "description": "Produces brand identity (palette, typography, logo) into 06-product-ip/brand/ via the brand-pack skill."},
|
|
68
67
|
{"name": "Stage 2 — Wedge validation + landing page + PRD", "description": "Produces 01-narrative/{PMF,LANDING,PRD}.md via the zero-to-prototype skill."},
|
|
68
|
+
{"name": "Brand pack", "description": "Produces brand guidelines + summary + color palette + typography system + tone-of-voice (voice/messaging/copy) + logo guidelines into 06-product-ip/brand/ via the brand-pack skill."},
|
|
69
69
|
{"name": "Stage 3 — Business plan", "description": "Produces 01-narrative/business-plan.md via investor-data-room Stage 3."},
|
|
70
70
|
{"name": "Stage 3b — Term sheet", "description": "Produces html/prospectus/term_sheet.html via investor-data-room Stage 3b."},
|
|
71
71
|
{"name": "Stage 4 — Deck blueprint", "description": "Produces 01-narrative/deck-blueprint.md via investor-data-room Stage 4."},
|