@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.
Files changed (33) hide show
  1. package/dist/index.js +39 -25
  2. package/package.json +1 -1
  3. package/payload/platform/neo4j/schema.cypher +6 -3
  4. package/payload/platform/plugins/cloudflare/references/manual-setup.md +3 -3
  5. package/payload/platform/plugins/cloudflare/references/reset-guide.md +5 -3
  6. package/payload/platform/plugins/memory/references/schema-estate-agent.md +51 -1
  7. package/payload/platform/plugins/venture-studio/PLUGIN.md +6 -1
  8. package/payload/platform/plugins/venture-studio/bin/scaffold.sh +1 -1
  9. package/payload/platform/plugins/venture-studio/skills/brand-pack/SKILL.md +1 -1
  10. package/payload/platform/plugins/venture-studio/skills/investor-data-room/SKILL.md +27 -14
  11. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/business-plan-template.md +7 -4
  12. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +6 -5
  13. package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/deck-blueprint-template.md +7 -6
  14. package/payload/platform/plugins/venture-studio/skills/office-hours/SKILL.md +8 -9
  15. package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/SKILL.md +4 -0
  16. package/payload/platform/templates/specialists/agents/personal-assistant.md +1 -1
  17. package/payload/premium-plugins/real-agent/BUNDLE.md +3 -2
  18. package/payload/premium-plugins/real-agent/agents/listing-curator.md +133 -0
  19. package/payload/premium-plugins/real-agent/plugins/brochures/skills/a4-print-documents/SKILL.md +1 -1
  20. package/payload/premium-plugins/real-agent/plugins/brochures/skills/brand-design/SKILL.md +5 -11
  21. package/payload/premium-plugins/real-agent/plugins/brochures/skills/make-brochure/SKILL.md +7 -7
  22. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-extract/SKILL.md +31 -39
  23. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-market-report/SKILL.md +4 -4
  24. package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-socials/SKILL.md +3 -2
  25. package/payload/premium-plugins/venture-studio/PLUGIN.md +6 -1
  26. package/payload/premium-plugins/venture-studio/bin/scaffold.sh +1 -1
  27. package/payload/premium-plugins/venture-studio/skills/brand-pack/SKILL.md +1 -1
  28. package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +27 -14
  29. package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/business-plan-template.md +7 -4
  30. package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +6 -5
  31. package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/deck-blueprint-template.md +7 -6
  32. package/payload/premium-plugins/venture-studio/skills/office-hours/SKILL.md +8 -9
  33. 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 specialist roles (negotiator, valuer, compliance). 34 skills + 45 MCP tools."
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 three specialist roles (negotiator, valuer, and compliance) that embed domain knowledge from the sub-plugins and operate autonomously with Loop CRM tools.
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.
@@ -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
- **Loop CRM caveat.** Many UK estate agents export listing photos at a 1600 px ceiling (Beacons, Muvin, etc.). 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.
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 reference output this skill targets is `/Users/neo/estate-agents/beacons/` read that directory once before generating to anchor the standard. (The neighbouring `/Users/neo/estate-agents/muvin/` is structurally identical; either may be inspected.)
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. `beacons`, not `beaconsrealestate.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.
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. The structure mirrors the reference at `/Users/neo/estate-agents/beacons/description.md`:
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
- Before producing output, read these files to anchor the format:
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 domain (`beaconsrealestate-logo.png`) | Slug is the brand name, not the FQDN. |
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://muvin.co.uk` → `muvin`, `https://example-agent.co.uk` → `example-agent`).
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 URL's `/property/<slug>/<id>/` path segment. The folder name is `<property_slug>-<id>` (e.g. `henham-road-debden-green-hamperden-end-cb11-3lz-949931`).
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/sparrows-farm-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.
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
- Reference instances that already conform (on the maintainer's machinepaths reflect where those runs happened, not a default for new callers):
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 `./muvin/` / `./beacons/` / etc., not under any other path.
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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: property-extract
3
- description: Use when a live property listing URL on an estate-agent website (Muvin, or any site using the Loop CRM CDN at b-cdn.net) 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.
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 reference output this skill targets is `/Users/neo/estate-agents/muvin/properties/henham-road-debden-green-hamperden-end-cb11-3lz-949931/` read that directory once before generating to anchor the standard.
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. `henham-road-debden-green-hamperden-end-cb11-3lz`). `<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.
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. For a Muvin URL with the existing brand pack at `/Users/neo/estate-agents/muvin/`, that resolves to `/Users/neo/estate-agents/muvin/properties/`. If the brand workspace cannot be located, fall back to `./properties/` in the caller's current working directory.
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
- | Muvin listings (`muvin.co.uk/property/...`) and any other agent fronting the Loop CRM CDN (`*.b-cdn.net/propertyimages/...`, `*.b-cdn.net/floorplans/...`) | Listings on agency-specific platforms (Rightmove, Zoopla, OnTheMarket, OpenRent)those have separate page structures and image hosts |
54
- | All photos, floorplans, drone shots, AI-staged renders | Stripping AI-generated images from the photo set keep them all and flag in metadata |
53
+ | Download every CDN-hosted image, floorplan and EPC asset referenced by the listing page | Stripping AI-generated images from the photo setkeep 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
- If the page is JS-rendered and `curl` returns no asset URLs, fall back to a headless-browser fetch (Chrome DevTools MCP or Playwright). If even that fails, stop and report `STATUS: BLOCKED` — do not synthesise an asset list.
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 matching `b-cdn.net/propertyimages/<tenant-uuid>/<listing-id>/` is an image asset. Two CDN buckets are in active use as of April 2026:
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
- - `loop-app.b-cdn.net` older photos (2024-vintage uploads)
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 `Sparrows Farm 22.jpg` in Finder, the brochure HTML references `images/sparrows-farm-22.webp`, and a search across either resolves to the same photograph.
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 (e.g. "Henham Road").
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
- "Sparrows Farm 1.jpg": "DJI_0125_1777122816019.jpg",
116
- "Sparrows Farm 2.jpg": "DJI_0115_1777122829462.jpg",
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
- URLs matching `b-cdn.net/floorplans/<tenant-uuid>/<listing-id>/`. Listings typically ship one floorplan per floor (so 1–3 files for a UK home). Preserve original filenames.
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 CRM / Muvin), Mapbox or Leaflet `<div>`+canvas instantiations (custom builds), `<img>` static-map renders (Rightmove-style), 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.
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: *Sparrows Farm*, *The Old Rectory*, *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.
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. "Sparrows Farm, Henham Road").
174
- 2. Looking for a name that is *not* a numbered street address (numbered addresses like "12 Henham Road" don't have a separate name).
175
- 3. Cross-checking with a Sprift / Land Registry report — if the title is registered as "Sparrows Farm at Henham Road", the name is *Sparrows Farm*.
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": "Sparrows Farm", // or null if the property has no distinct name
178
+ "property_name": "The Old Rectory", // or null if the property has no distinct name
181
179
  "address": {
182
- "line_1": "Henham Road",
183
- "locality": "Hamperden End",
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 a Muvin 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.
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 likely JS-rendered and you need a headless browser.
319
- 2. **Extract.** Pull the raw HTML and grep all `b-cdn.net/propertyimages/...` and `b-cdn.net/floorplans/...` URLs. Deduplicate. Also grep PDF URLs containing `epc`. Extract the `<div class="section__description">` block for the description text. (Map detection is provider-agnostic and happens in step 4 below — don't try to parse map markup at this static-HTML stage.)
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 (conditional, provider-agnostic).** Modern listing pages from Loop CRM tenants, Rightmove, Zoopla 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. If the probe in step 1 came back with data the listing visibly shows (EPC band, floorplan thumbnail, video URL) but the static-HTML extract in step 2 does not, the data is almost certainly tab-bound. Drive the Playwright session opened in step 4 (or open one if step 4 skipped) 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 tab markup as the sole detector; the same listing surface ships with different tab implementations across CMS tenants.
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 the static HTML pages Muvin serves. If a future agent ships a JS-rendered listing page, switch to Chrome DevTools MCP or Playwright — the contract on the output is unchanged.
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
- Before producing output, read these files to anchor the format:
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/muvin_property.html` or other working files inside the output directory | The directory is the deliverable. Working files get cleaned up. |
347
- | Hardcoding the tenant UUID | The UUID identifies the agency on Loop CRM. Different Muvin offices can have different tenant IDs. Extract it from the actual URLs on the page. |
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.
@@ -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 (`muvin.co.uk` `/estate-agents/muvin/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 (e.g. *muvin*); for richer marks, supply the files.
24
- - **`property_urls`** — list of up to 10 source URLs (Muvin, Rightmove, Zoopla, etc.). Each must resolve to a single property listing page. Reject more than 10; the layout breaks at >10 cards.
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` straight into a tile 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.
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
@@ -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 brochure folder's
23
- display name (e.g. *Sparrows Farm*) is the headline.
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."},