@rubytech/create-maxy-code 0.1.17 → 0.1.19

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 (101) hide show
  1. package/dist/__tests__/cdp-port-no-silent-fallback.test.js +2 -2
  2. package/dist/__tests__/onboarding-state-readback.test.js +61 -0
  3. package/dist/index.js +42 -0
  4. package/dist/onboarding-readback.js +27 -0
  5. package/package.json +2 -2
  6. package/payload/platform/config/brand-registry.json +4 -4
  7. package/payload/platform/config/brand.json +4 -4
  8. package/payload/platform/plugins/cloudflare/scripts/reset-tunnel.sh +1 -1
  9. package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +1 -1
  10. package/payload/platform/plugins/docs/references/deployment.md +25 -2
  11. package/payload/platform/scripts/installer-device-verify.sh +2 -2
  12. package/payload/platform/scripts/redact-install-logs.sh +2 -2
  13. package/payload/platform/scripts/seed-neo4j.sh +35 -0
  14. package/payload/platform/scripts/verify-skill-tool-surface.sh +1 -1
  15. package/payload/platform/scripts/vnc.sh +2 -2
  16. package/payload/premium-plugins/real-agency/plugins/brochures/skills/brand-design/SKILL.md +1 -1
  17. package/payload/premium-plugins/real-agency/plugins/brochures/skills/make-brochure/SKILL.md +9 -9
  18. package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/SKILL.md +7 -7
  19. package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/references/build.md +5 -5
  20. package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/references/copy.md +3 -3
  21. package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/references/images.md +1 -1
  22. package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/references/{page-landing.md → index-landing.md} +14 -14
  23. package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/references/{page.html → index.html} +4 -4
  24. package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/references/placeholders.md +6 -6
  25. package/payload/server/package.json +1 -1
  26. package/payload/server/public/assets/{Checkbox-nrf4ISU0.js → Checkbox-BmlrSRF8.js} +1 -1
  27. package/payload/server/public/assets/{admin-BctPxbjB.js → admin-Drvl0Okz.js} +53 -53
  28. package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-DZI73ap7.js → architectureDiagram-Q4EWVU46-DU8MnqOj.js} +1 -1
  29. package/payload/server/public/assets/{blockDiagram-DXYQGD6D-BQQz9YNZ.js → blockDiagram-DXYQGD6D-DwBZc1na.js} +1 -1
  30. package/payload/server/public/assets/{c4Diagram-AHTNJAMY-gl8-XVLH.js → c4Diagram-AHTNJAMY-C9ZHreVX.js} +1 -1
  31. package/payload/server/public/assets/channel-Boz4UHN2.js +1 -0
  32. package/payload/server/public/assets/{chunk-336JU56O-839etdKR.js → chunk-336JU56O-DNXqHk42.js} +2 -2
  33. package/payload/server/public/assets/{chunk-426QAEUC-hyvaLE5p.js → chunk-426QAEUC-DOLKkQGn.js} +1 -1
  34. package/payload/server/public/assets/{chunk-4TB4RGXK-DEIYWdpd.js → chunk-4TB4RGXK-l8iZx-gQ.js} +1 -1
  35. package/payload/server/public/assets/{chunk-5FUZZQ4R-zWevOyjc.js → chunk-5FUZZQ4R-DQwRtUdO.js} +1 -1
  36. package/payload/server/public/assets/{chunk-5PVQY5BW-CL9KWSZH.js → chunk-5PVQY5BW-CvvoBJnn.js} +1 -1
  37. package/payload/server/public/assets/{chunk-EDXVE4YY-D91vGp64.js → chunk-EDXVE4YY-DV1J4-LR.js} +1 -1
  38. package/payload/server/public/assets/{chunk-ENJZ2VHE-DApK4iuk.js → chunk-ENJZ2VHE-Co6qpIQl.js} +1 -1
  39. package/payload/server/public/assets/{chunk-ICPOFSXX-BOjV-nTe.js → chunk-ICPOFSXX-9v9O_mwr.js} +1 -1
  40. package/payload/server/public/assets/{chunk-OYMX7WX6-D3Cv5Z_Z.js → chunk-OYMX7WX6-D2X1NuKZ.js} +1 -1
  41. package/payload/server/public/assets/{chunk-U2HBQHQK-C92E-iRU.js → chunk-U2HBQHQK-DmqWEsN2.js} +1 -1
  42. package/payload/server/public/assets/{chunk-X2U36JSP-DohG6qWK.js → chunk-X2U36JSP-C0b2a6Bq.js} +1 -1
  43. package/payload/server/public/assets/{chunk-YZCP3GAM-CyeLVSjf.js → chunk-YZCP3GAM-BdBAquWg.js} +1 -1
  44. package/payload/server/public/assets/{chunk-ZZ45TVLE-D7R-lONY.js → chunk-ZZ45TVLE-C1AFspLM.js} +1 -1
  45. package/payload/server/public/assets/classDiagram-6PBFFD2Q-CRA6E97A.js +1 -0
  46. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-VnieTvEm.js +1 -0
  47. package/payload/server/public/assets/clone-6YhZHo8b.js +1 -0
  48. package/payload/server/public/assets/{dagre-DjhovTZd.js → dagre-7eiQzAHj.js} +1 -1
  49. package/payload/server/public/assets/{dagre-KV5264BT-DFeRlQuy.js → dagre-KV5264BT-a88ylEXY.js} +1 -1
  50. package/payload/server/public/assets/data-Dq_if_B4.js +1 -0
  51. package/payload/server/public/assets/{device-url-actions-CE3A1UDw.js → device-url-actions-_s-UgHa3.js} +1 -1
  52. package/payload/server/public/assets/{diagram-5BDNPKRD-DsmRGnVL.js → diagram-5BDNPKRD-XuINnSy-.js} +1 -1
  53. package/payload/server/public/assets/{diagram-G4DWMVQ6-CVSK-mLR.js → diagram-G4DWMVQ6-B93iSQ8a.js} +1 -1
  54. package/payload/server/public/assets/{diagram-MMDJMWI5-DMN94Pe-.js → diagram-MMDJMWI5-Gcoj967k.js} +1 -1
  55. package/payload/server/public/assets/{diagram-TYMM5635-DaD4mLMc.js → diagram-TYMM5635-C2BKnsAF.js} +1 -1
  56. package/payload/server/public/assets/{erDiagram-SMLLAGMA-BUkZ2Iq1.js → erDiagram-SMLLAGMA-DwbOuflI.js} +1 -1
  57. package/payload/server/public/assets/{flowDiagram-DWJPFMVM-DW8DX_ge.js → flowDiagram-DWJPFMVM-CEExhdxC.js} +1 -1
  58. package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DWaRL6__.js → ganttDiagram-T4ZO3ILL-kYNYeUK-.js} +1 -1
  59. package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BaPTFtVx.js → gitGraphDiagram-UUTBAWPF-D3X2eJPE.js} +1 -1
  60. package/payload/server/public/assets/graph-CNFkzAKU.js +1 -0
  61. package/payload/server/public/assets/{graph-labels-BRtJE9AE.js → graph-labels-C6ZZPglH.js} +1 -1
  62. package/payload/server/public/assets/{graphlib-BUhb3hPU.js → graphlib-_S6i_Jn2.js} +1 -1
  63. package/payload/server/public/assets/{infoDiagram-42DDH7IO-Ch6CE3GO.js → infoDiagram-42DDH7IO-CQx0vDcC.js} +1 -1
  64. package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-DApFr2KO.js → ishikawaDiagram-UXIWVN3A-0eZUgqUP.js} +1 -1
  65. package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-D72xl-VA.js → journeyDiagram-VCZTEJTY-D-B2Kd_J.js} +1 -1
  66. package/payload/server/public/assets/jsx-runtime-CtqEPPN5.css +1 -0
  67. package/payload/server/public/assets/{kanban-definition-6JOO6SKY-TuAvkCJU.js → kanban-definition-6JOO6SKY-BmLn-OEz.js} +1 -1
  68. package/payload/server/public/assets/{line-Cr3lHgh8.js → line-h25nWPBw.js} +1 -1
  69. package/payload/server/public/assets/{mermaid-parser.core-BvbEd4_6.js → mermaid-parser.core-CXXeaSZi.js} +1 -1
  70. package/payload/server/public/assets/{mermaid.core-CSIEcw1L.js → mermaid.core-BjFfgEHL.js} +3 -3
  71. package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CNLvqgk-.js → mindmap-definition-QFDTVHPH-Dql4ILoK.js} +1 -1
  72. package/payload/server/public/assets/{page-B_80xGrM.js → page-CdWWweCx.js} +1 -1
  73. package/payload/server/public/assets/{page-BcnqM490.js → page-D20UnO_r.js} +1 -1
  74. package/payload/server/public/assets/{pieDiagram-DEJITSTG-CSOsdFn6.js → pieDiagram-DEJITSTG-nYaoTCKZ.js} +1 -1
  75. package/payload/server/public/assets/{public-DGrCAqZN.js → public-CXgyLdJU.js} +3 -3
  76. package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-Bsxz9S58.js → quadrantDiagram-34T5L4WZ-XG8xivm9.js} +1 -1
  77. package/payload/server/public/assets/{requirementDiagram-MS252O5E-BZRrlQlh.js → requirementDiagram-MS252O5E-C2GzfT-6.js} +1 -1
  78. package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-C-W-Az7g.js → sankeyDiagram-XADWPNL6-3aI78p2X.js} +1 -1
  79. package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-DCZPAj4-.js → sequenceDiagram-FGHM5R23-CvaNRYuS.js} +1 -1
  80. package/payload/server/public/assets/{stateDiagram-FHFEXIEX-C3wODMGb.js → stateDiagram-FHFEXIEX-DHT2Zyvj.js} +1 -1
  81. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-YmtuS9BN.js +1 -0
  82. package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-TRzSKinM.js → timeline-definition-GMOUNBTQ-DqPjDUku.js} +1 -1
  83. package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DSKZVM8N.js → vennDiagram-DHZGUBPP-lQB1LgjH.js} +1 -1
  84. package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BRXL08eb.js → wardleyDiagram-NUSXRM2D-sxSMxkwm.js} +1 -1
  85. package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CgWMOf0l.js → xychartDiagram-5P7HB3ND-4-dvvfSD.js} +1 -1
  86. package/payload/server/public/brand-constants.json +1 -1
  87. package/payload/server/public/brand-defaults.css +1 -1
  88. package/payload/server/public/data.html +5 -5
  89. package/payload/server/public/graph.html +6 -6
  90. package/payload/server/public/index.html +8 -8
  91. package/payload/server/public/public.html +5 -5
  92. package/payload/server/server.js +91 -6
  93. package/payload/server/public/assets/channel-BIMyzzFT.js +0 -1
  94. package/payload/server/public/assets/classDiagram-6PBFFD2Q-BAfXNwa9.js +0 -1
  95. package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-D5bE1GaW.js +0 -1
  96. package/payload/server/public/assets/clone-DOH_suVb.js +0 -1
  97. package/payload/server/public/assets/data-DzTaKq-r.js +0 -1
  98. package/payload/server/public/assets/graph-dgoq2zvY.js +0 -1
  99. package/payload/server/public/assets/jsx-runtime-BgXAk35j.css +0 -1
  100. package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ch6qbhpm.js +0 -1
  101. /package/payload/server/public/assets/{jsx-runtime-Bz7aoCi7.js → jsx-runtime-BSJRynxp.js} +0 -0
@@ -2,7 +2,7 @@
2
2
  // silent-fallback-masks-root-cause violation. The four runtime sites that
3
3
  // previously substituted `9222 + offset` for a missing brand.json.cdpPort
4
4
  // (paths.ts, admin/mcp/index.ts, vnc.sh, test-laptop-vnc-boot.sh) plus the
5
- // installer-side brand stamp at packages/create-maxy/src/index.ts have all
5
+ // installer-side brand stamp at packages/create-maxy-code/src/index.ts have all
6
6
  // been swept to loud-fail. This test asserts the three greps from criterion
7
7
  // 2 of the task brief return zero matches; reintroducing any silent fallback
8
8
  // fails CI immediately.
@@ -19,7 +19,7 @@ import { resolve } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
20
  const SELF_DIR = fileURLToPath(new URL(".", import.meta.url));
21
21
  // dist/__tests__/ → repo root is four levels up
22
- // (packages/create-maxy/dist/__tests__ → packages/create-maxy → packages → repo).
22
+ // (packages/create-maxy-code/dist/__tests__ → packages/create-maxy-code → packages → repo).
23
23
  const REPO_ROOT = resolve(SELF_DIR, "..", "..", "..", "..");
24
24
  function grepReturns(pattern, includes) {
25
25
  const args = ["-RnE", pattern];
@@ -0,0 +1,61 @@
1
+ // Task 033 — install-invariant for OnboardingState seed.
2
+ //
3
+ // The installer re-reads Neo4j after seed-neo4j.sh to verify the
4
+ // OnboardingState node landed. The parser turns cypher-shell's plain-
5
+ // format output into a tri-state outcome consumed by the loud-fail
6
+ // branch in src/index.ts. These tests fence the three outcomes the
7
+ // installer logs name: present / missing / unreachable.
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { classifyOnboardingReadBack } from "../onboarding-readback.js";
11
+ test("non-zero exit → unreachable, with stderr tail captured", () => {
12
+ const outcome = classifyOnboardingReadBack({
13
+ status: 1,
14
+ stdout: "",
15
+ stderr: "Connection refused: bolt://localhost:7687",
16
+ });
17
+ assert.equal(outcome.kind, "unreachable");
18
+ if (outcome.kind === "unreachable") {
19
+ assert.match(outcome.stderrTail, /Connection refused/);
20
+ }
21
+ });
22
+ test("header row only (no match) → missing", () => {
23
+ // cypher-shell --format plain emits the column header even when zero rows
24
+ // matched. The seed-or-fail contract treats header-only as the loud-fail.
25
+ const outcome = classifyOnboardingReadBack({
26
+ status: 0,
27
+ stdout: "currentStep\n",
28
+ stderr: "",
29
+ });
30
+ assert.equal(outcome.kind, "missing");
31
+ });
32
+ test("data row present → present, currentStep relayed verbatim", () => {
33
+ const outcome = classifyOnboardingReadBack({
34
+ status: 0,
35
+ stdout: "currentStep\n0\n",
36
+ stderr: "",
37
+ });
38
+ assert.equal(outcome.kind, "present");
39
+ if (outcome.kind === "present") {
40
+ assert.equal(outcome.currentStep, "0");
41
+ }
42
+ });
43
+ test("empty stdout + zero exit → missing (defensive: cypher-shell flushed nothing)", () => {
44
+ const outcome = classifyOnboardingReadBack({
45
+ status: 0,
46
+ stdout: "",
47
+ stderr: "",
48
+ });
49
+ assert.equal(outcome.kind, "missing");
50
+ });
51
+ test("trailing whitespace tolerated on present row", () => {
52
+ const outcome = classifyOnboardingReadBack({
53
+ status: 0,
54
+ stdout: "currentStep\n0 \n\n",
55
+ stderr: "",
56
+ });
57
+ assert.equal(outcome.kind, "present");
58
+ if (outcome.kind === "present") {
59
+ assert.equal(outcome.currentStep, "0");
60
+ }
61
+ });
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, rea
4
4
  import { resolve, join, dirname } from "node:path";
5
5
  import { randomBytes } from "node:crypto";
6
6
  import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerUnitFile } from "./port-resolution.js";
7
+ import { classifyOnboardingReadBack } from "./onboarding-readback.js";
7
8
  import { parseOsRelease, isUbuntuLike as isUbuntuLikePure, parseAptCacheCandidate, decideAptResolution, } from "./apt-resolve.js";
8
9
  import { findPeerBrandOnDefaultNeo4jPort } from "./peer-brand-detect.js";
9
10
  import { requireSupportedPlatform } from "./platform-detect.js";
@@ -2289,6 +2290,47 @@ function setupAccount() {
2289
2290
  logFile(` [neo4j] passing NEO4J_URI=${neo4jUri} to seed`);
2290
2291
  shell("bash", [seedScript], { cwd: INSTALL_DIR, env: neo4jEnv });
2291
2292
  }
2293
+ // Task 033 — install-invariant: OnboardingState must exist post-seed.
2294
+ // Loud-fails (exit 1) on miss because the first admin session has nothing
2295
+ // to read otherwise: loadOnboardingStep returns -1 (no node), the chat
2296
+ // tries to drive the 9-step flow against missing state, and step
2297
+ // advancements have nowhere to land. Symmetric to Task 904's
2298
+ // [install-invariant] line — log-and-continue isn't enough here because
2299
+ // the gap silently breaks every fresh install. Idempotent on re-runs:
2300
+ // the seed's MERGE leaves the node untouched.
2301
+ const accountId = resolveInstallAccountId();
2302
+ if (accountId) {
2303
+ assertOnboardingStateSeeded(accountId, neo4jUri, password);
2304
+ }
2305
+ else {
2306
+ console.log(" [install-invariant] onboarding-state-check SKIPPED reason=no-account-discovered");
2307
+ }
2308
+ }
2309
+ // Task 033 — Neo4j read-back assertion. Runs cypher-shell against the
2310
+ // brand's Neo4j and confirms an OnboardingState{accountId} node exists.
2311
+ // Loud-fail exit on miss / Neo4j unreachable so the installer logs name
2312
+ // the exact symptom the next run has to fix.
2313
+ function assertOnboardingStateSeeded(accountId, neo4jUri, password) {
2314
+ const TAG = "[install-invariant]";
2315
+ const cypher = `MATCH (o:OnboardingState {accountId: '${accountId}'}) RETURN o.currentStep AS currentStep;`;
2316
+ const result = spawnSync("cypher-shell", ["-u", "neo4j", "-p", password, "-a", neo4jUri, "--format", "plain", cypher], { stdio: "pipe", encoding: "utf-8", timeout: 10_000 });
2317
+ const outcome = classifyOnboardingReadBack({
2318
+ status: result.status,
2319
+ stdout: result.stdout ?? "",
2320
+ stderr: result.stderr ?? "",
2321
+ });
2322
+ if (outcome.kind === "unreachable") {
2323
+ console.error(` ${TAG} onboarding-state-UNREACHABLE accountId=${accountId} exit=${result.status} stderr=${JSON.stringify(outcome.stderrTail)}`);
2324
+ logFile(` ${TAG} onboarding-state-UNREACHABLE accountId=${accountId} stderr=${outcome.stderrTail}`);
2325
+ throw new Error(`OnboardingState read-back failed: cypher-shell exit=${result.status}`);
2326
+ }
2327
+ if (outcome.kind === "missing") {
2328
+ console.error(` ${TAG} onboarding-state-MISSING accountId=${accountId}`);
2329
+ logFile(` ${TAG} onboarding-state-MISSING accountId=${accountId}`);
2330
+ throw new Error(`OnboardingState missing for accountId=${accountId} after seed-neo4j.sh`);
2331
+ }
2332
+ console.log(` ${TAG} onboarding-state-present accountId=${accountId} currentStep=${outcome.currentStep}`);
2333
+ logFile(` ${TAG} onboarding-state-present accountId=${accountId} currentStep=${outcome.currentStep}`);
2292
2334
  }
2293
2335
  // ---------------------------------------------------------------------------
2294
2336
  // Tunnel script shortcuts
@@ -0,0 +1,27 @@
1
+ // Task 033 — pure parser for the OnboardingState read-back assertion.
2
+ //
3
+ // Extracted into a dedicated module (not src/index.ts) so the test
4
+ // suite can import it without triggering index's brand.json-at-load
5
+ // side effect. The installer's loud-fail branches at
6
+ // `assertOnboardingStateSeeded` consume the tri-state outcome.
7
+ /**
8
+ * Classify cypher-shell --format plain output into one of three states:
9
+ *
10
+ * - `unreachable` — non-zero exit (auth, network, syntax). Stderr tail
11
+ * (last 200 chars) is preserved so the install log carries the
12
+ * symptom verbatim.
13
+ * - `missing` — exit 0 but no data row beyond the header. cypher-shell
14
+ * prints the column name even on a zero-match query.
15
+ * - `present` — exit 0 with at least one data row. `currentStep` is the
16
+ * first field on row 2.
17
+ */
18
+ export function classifyOnboardingReadBack(input) {
19
+ if (input.status !== 0) {
20
+ return { kind: "unreachable", stderrTail: input.stderr.trim().slice(-200) };
21
+ }
22
+ const lines = input.stdout.trim().split("\n").filter((l) => l.length > 0);
23
+ if (lines.length < 2) {
24
+ return { kind: "missing" };
25
+ }
26
+ return { kind: "present", currentStep: lines[1].trim() };
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -17,7 +17,7 @@
17
17
  "payload"
18
18
  ],
19
19
  "keywords": [
20
- "maxy",
20
+ "maxy-code",
21
21
  "ai",
22
22
  "assistant",
23
23
  "raspberry-pi",
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "brands": [
3
3
  {
4
- "hostname": "maxy",
5
- "configDir": ".maxy",
4
+ "hostname": "maxy-code",
5
+ "configDir": ".maxy-code",
6
6
  "vncDisplay": 99,
7
7
  "rfbPort": 5900,
8
8
  "websockifyPort": 6080,
9
9
  "cdpPort": 9222
10
10
  },
11
11
  {
12
- "hostname": "realagent",
13
- "configDir": ".realagent",
12
+ "hostname": "realagent-code",
13
+ "configDir": ".realagent-code",
14
14
  "vncDisplay": 100,
15
15
  "rfbPort": 5901,
16
16
  "websockifyPort": 6081,
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "productName": "Maxy",
3
- "hostname": "maxy",
4
- "serviceName": "maxy.service",
5
- "installDir": "maxy",
6
- "configDir": ".maxy",
3
+ "hostname": "maxy-code",
4
+ "serviceName": "maxy-code.service",
5
+ "installDir": "maxy-code",
6
+ "configDir": ".maxy-code",
7
7
  "tagline": "AI for Productive People",
8
8
  "strapline": "Convenience as standard.",
9
9
  "domain": "getmaxy.com",
@@ -28,7 +28,7 @@ set -euo pipefail
28
28
 
29
29
  # shellcheck source=_stream-log.sh
30
30
  # Resolve symlinks before dirname — ~/reset-tunnel.sh is installed as a symlink
31
- # (see packages/create-maxy/src/index.ts:installTunnelScripts), so the raw
31
+ # (see packages/create-maxy-code/src/index.ts:installTunnelScripts), so the raw
32
32
  # BASH_SOURCE[0] points at $HOME, not the scripts directory where _stream-log.sh lives.
33
33
  source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
34
34
  require_stream_log_path reset-tunnel
@@ -33,7 +33,7 @@ set -euo pipefail
33
33
 
34
34
  # shellcheck source=_stream-log.sh
35
35
  # Resolve symlinks before dirname — ~/setup-tunnel.sh is installed as a symlink
36
- # (see packages/create-maxy/src/index.ts:installTunnelScripts), so the raw
36
+ # (see packages/create-maxy-code/src/index.ts:installTunnelScripts), so the raw
37
37
  # BASH_SOURCE[0] points at $HOME, not the scripts directory where _stream-log.sh lives.
38
38
  source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
39
39
  require_stream_log_path setup-tunnel
@@ -39,6 +39,29 @@ This installs all dependencies (Node.js, Neo4j, Cloudflare tunnel, Claude Code),
39
39
  6. Starts the {{productName}} web server on port 19200
40
40
  7. Configures systemd so everything restarts automatically if the Pi reboots
41
41
 
42
+ ## First admin session — onboarding handover
43
+
44
+ After install, the first time you open the admin URL, {{productName}} walks you through a 9-step onboarding flow: plugin selection, output style, thinking-view, timezone, persona, Cloudflare, Anthropic API key, and the business-profile capture. The flow is gated on a single piece of Neo4j state, an `OnboardingState{accountId, currentStep}` node:
45
+
46
+ - `seed-neo4j.sh` writes the node at install time with `currentStep=0`. The installer re-reads it before exiting; a missing node or an unreachable Neo4j fails the install loudly (`[install-invariant] onboarding-state-MISSING` or `onboarding-state-UNREACHABLE`).
47
+ - Every admin session-create reads `currentStep`. Anything below 9 keeps `onboarding_complete=false` on `/api/health` and `/api/admin/session`, and the chat opens on the next step's component (multi-select, dropdown, etc.).
48
+ - The first input on a freshly-spawned admin claude session is prepended with an `<onboarding-state currentStep="N">` directive that tells the agent to call `skill-load skillName=onboarding`. Resumed sessions skip this prepend so the agent doesn't re-ask the same step.
49
+ - If Neo4j is unreachable at session-create, the prepend becomes a loud-fail block (`graphUnreachable="true"`) and `onboarding_complete` is reported false — never silently skipped.
50
+
51
+ Diagnostic command on the Pi (substitute the brand's Neo4j port from `~/.maxy/.env` or `~/.realagent/.env`):
52
+
53
+ ```bash
54
+ cypher-shell -a bolt://localhost:<brand-neo4j-port> \
55
+ "MATCH (o:OnboardingState) RETURN o.accountId, o.currentStep"
56
+ ```
57
+
58
+ Failure signals to grep in `~/.maxy/logs/server.log` (or `~/.realagent/logs/server.log`):
59
+
60
+ - missing `[onboarding-seed]` line in install output
61
+ - `[install-invariant] onboarding-state-MISSING`
62
+ - `[onboarding-gate] step=null complete=true` (the pre-Task-033 bug)
63
+ - missing `[skill-load] name=onboarding` while `[onboarding-gate]` reports `complete=false`
64
+
42
65
  ## Service Management
43
66
 
44
67
  {{productName}} runs via systemd and starts automatically on boot. You don't need to start it manually. To check if it's running, ask {{productName}} "Check system status."
@@ -121,7 +144,7 @@ A separate operator-side harness at `platform/scripts/installer-device-verify.sh
121
144
 
122
145
  The installer registers Claude Code plugins on the device as the last step before the brand service starts. After registration, `claude plugin list` on the Pi shows every Maxy platform plugin shipped by the brand, every premium sub-plugin shipped by the brand, and any external plugins the brand declares (e.g. Telegram, Discord, iMessage from `claude-plugins-official`). Spawned `claude` sessions inherit those plugins from `~/.claude/` — the session manager passes no `--mcp-config` argv.
123
146
 
124
- **Where the manifests come from.** The Maxy plugin source tree uses `PLUGIN.md` (YAML frontmatter) for plugin metadata, not Claude Code's native `.claude-plugin/plugin.json`. At bundle time, `scripts/generate-plugin-manifests.mjs` walks the payload and synthesises a Claude-Code-native `plugin.json` per plugin plus a `marketplace.json` at each tree root. The generator runs in `packages/create-maxy/scripts/bundle.js` after platform + premium plugins are copied into the payload, so the deployed install directory carries:
147
+ **Where the manifests come from.** The Maxy plugin source tree uses `PLUGIN.md` (YAML frontmatter) for plugin metadata, not Claude Code's native `.claude-plugin/plugin.json`. At bundle time, `scripts/generate-plugin-manifests.mjs` walks the payload and synthesises a Claude-Code-native `plugin.json` per plugin plus a `marketplace.json` at each tree root. The generator runs in `packages/create-maxy-code/scripts/bundle.js` after platform + premium plugins are copied into the payload, so the deployed install directory carries:
125
148
 
126
149
  - `<INSTALL_DIR>/platform/plugins/<name>/.claude-plugin/plugin.json` per platform plugin
127
150
  - `<INSTALL_DIR>/platform/plugins/.claude-plugin/marketplace.json` (marketplace `maxy-platform`)
@@ -142,7 +165,7 @@ Generator schema:
142
165
 
143
166
  Skills, agents, hooks, and commands directories at the plugin root are auto-discovered by Claude Code — no explicit field needed.
144
167
 
145
- **Install flow** (`registerLocalAndExternalPlugins()` in `packages/create-maxy/src/index.ts`):
168
+ **Install flow** (`registerLocalAndExternalPlugins()` in `packages/create-maxy-code/src/index.ts`):
146
169
 
147
170
  1. Discover every `.claude-plugin/marketplace.json` under the install directory.
148
171
  2. For each one not already in `claude plugin marketplace list`, run `claude plugin marketplace add <dir>`. Pre-existing entries log `[plugin-marketplace] added <name> idempotent=true`.
@@ -12,7 +12,7 @@
12
12
  # banner — is a non-zero exit with the failing device named on stderr.
13
13
  #
14
14
  # CONTRACT
15
- # Archival of any task that touches packages/create-maxy/** is contingent
15
+ # Archival of any task that touches packages/create-maxy-code/** is contingent
16
16
  # on this script exiting zero against the published version. The exit code
17
17
  # and the per-device summary are quoted in the close commit body.
18
18
  #
@@ -191,7 +191,7 @@ for i in $(seq 0 $((DEVICE_COUNT - 1))); do
191
191
  # Step 2 — confirm a terminal-success banner in the latest install log.
192
192
  # Pick the newest install-*.log by mtime (`ls -t`) and grep for either of
193
193
  # the installer's two terminal-success markers emitted by
194
- # packages/create-maxy/src/index.ts:
194
+ # packages/create-maxy-code/src/index.ts:
195
195
  # • DISPLAY_MODE=virtual (Pi, headless VNC) →
196
196
  # "Browser automation ready (CDP connected)" (index.ts:3012)
197
197
  # • DISPLAY_MODE=native (laptop, on-demand Chromium) →
@@ -2,7 +2,7 @@
2
2
  # Existing-pi install-log redaction (Task 744).
3
3
  #
4
4
  # Idempotent one-shot remediation for Pis that completed installation BEFORE
5
- # the install-log redaction landed at packages/create-maxy/src/index.ts:152.
5
+ # the install-log redaction landed at packages/create-maxy-code/src/index.ts:152.
6
6
  # Scans every `install-*.log` in the configured logs directory and replaces
7
7
  # every literal `set-initial-password ...<secret>` payload with
8
8
  # `set-initial-password [REDACTED]`. Re-running the script is safe —
@@ -10,7 +10,7 @@
10
10
  # edits occur.
11
11
  #
12
12
  # Source patterns covered:
13
- # 1. TS installer (packages/create-maxy/src/index.ts:152) — "[ISO] > sudo
13
+ # 1. TS installer (packages/create-maxy-code/src/index.ts:152) — "[ISO] > sudo
14
14
  # neo4j-admin dbms set-initial-password -- <secret>" or any args after
15
15
  # "set-initial-password" (positional or "--" delimited).
16
16
  # 2. Legacy bash installer (removed) — "+ sudo neo4j-admin dbms
@@ -541,4 +541,39 @@ BACKFILL_EOF
541
541
  echo " [backfill] $BACKFILL_RESULT"
542
542
  fi
543
543
 
544
+ # ------------------------------------------------------------------
545
+ # 4. Seed OnboardingState (Task 033 — maxy-code)
546
+ # ------------------------------------------------------------------
547
+ # The first admin session reads OnboardingState.currentStep to decide whether
548
+ # the chat opens on step 1 of the 9-step onboarding flow or on a free-form
549
+ # greeting. Pre-Task-033 the node was never written at seed, so the post-PIN
550
+ # session-create's loadOnboardingStep returned -1 (no node) which used to
551
+ # conflate with null (Neo4j unreachable) and silently report
552
+ # onboardingComplete:true — the operator was dropped into the admin agent
553
+ # with no plugin selection, output-style, timezone, SOUL, Cloudflare,
554
+ # Anthropic-key, or business-profile setup.
555
+ #
556
+ # Keyed on accountId, not userId — fresh installs reach the seed before
557
+ # users.json exists (PIN is set later by the onboarding flow itself).
558
+ # MERGE is idempotent: re-runs leave currentStep untouched, refreshing only
559
+ # updatedAt.
560
+ SEED_NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
561
+ echo "==> Seeding OnboardingState for accountId=$ACCOUNT_ID"
562
+ ONBOARDING_SEED_RESULT=$("$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" --format plain << CYPHER_EOF
563
+ MERGE (o:OnboardingState {accountId: '$ACCOUNT_ID'})
564
+ ON CREATE SET o.currentStep = 0,
565
+ o.createdAt = '$SEED_NOW',
566
+ o.updatedAt = '$SEED_NOW'
567
+ ON MATCH SET o.updatedAt = '$SEED_NOW'
568
+ RETURN o.currentStep AS currentStep, o.createdAt = '$SEED_NOW' AS created;
569
+ CYPHER_EOF
570
+ )
571
+ ONBOARDING_STEP=$(echo "$ONBOARDING_SEED_RESULT" | awk 'NR==2 {print $1}')
572
+ ONBOARDING_CREATED=$(echo "$ONBOARDING_SEED_RESULT" | awk 'NR==2 {print $2}')
573
+ if [ "$ONBOARDING_CREATED" = "true" ]; then
574
+ echo " [onboarding-seed] accountId=$ACCOUNT_ID currentStep=$ONBOARDING_STEP idempotent=false"
575
+ else
576
+ echo " [onboarding-seed] accountId=$ACCOUNT_ID currentStep=$ONBOARDING_STEP idempotent=true"
577
+ fi
578
+
544
579
  echo " Done."
@@ -8,7 +8,7 @@
8
8
  # and where a skill prescribes a forbidden direct-execution path
9
9
  # (`cypher-shell`, `neo4j-admin` invocations, raw-Cypher DML in prose).
10
10
  #
11
- # Wired into the root `packages/create-maxy/package.json` `prepublishOnly`
11
+ # Wired into the root `packages/create-maxy-code/package.json` `prepublishOnly`
12
12
  # script so a regression cannot reach npm publish without firing.
13
13
  #
14
14
  # One stdout line per (skill, specialist) pair:
@@ -45,7 +45,7 @@ BRAND_JSON="${PLATFORM_ROOT}/config/brand.json"
45
45
  # Task 959 — brand.json is the single source of truth at runtime; missing
46
46
  # fields loud-fail rather than silently substituting a vncDisplay-derived
47
47
  # offset (silent-fallback-masks-root-cause recurrence). The install-time
48
- # offset rule in packages/create-maxy/src/index.ts stamps every brand at
48
+ # offset rule in packages/create-maxy-code/src/index.ts stamps every brand at
49
49
  # build time, so a correctly-installed device always has all five fields.
50
50
  if [ ! -f "$BRAND_JSON" ]; then
51
51
  echo "[vnc.sh] error reason=brand-config-missing path=$BRAND_JSON" >&2
@@ -89,7 +89,7 @@ mkdir -p "$LOG_DIR"
89
89
  log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }
90
90
 
91
91
  # Task 929 — resolve the absolute Chromium binary path from the install-time
92
- # config file. The installer (packages/create-maxy/src/index.ts
92
+ # config file. The installer (packages/create-maxy-code/src/index.ts
93
93
  # ensureNonSnapChromium + writeChromiumBinaryPathFile) writes this file with
94
94
  # the non-snap binary chosen for this device — `/usr/bin/chromium` on Pi
95
95
  # Bookworm (real .deb) or `/usr/bin/google-chrome-stable` on Ubuntu Noble
@@ -80,7 +80,7 @@ Two PNGs of the wordmark/lockup as the brand uses it. **The filename describes t
80
80
  | Filename | Art colour | Place it on |
81
81
  |---|---|---|
82
82
  | `<slug>-logo-light.png` | light/white/cream/pale pixels | dark surfaces (back-page hero, dark masthead) |
83
- | `<slug>-logo-dark.png` | dark/black/navy/charcoal pixels | light surfaces (TOC, page.html nav, paper backgrounds) |
83
+ | `<slug>-logo-dark.png` | dark/black/navy/charcoal pixels | light surfaces (TOC, index.html nav, paper backgrounds) |
84
84
 
85
85
  So if you open `donnavincent-logo-light.png` and see white text, that is correct — it's the light-coloured variant. If you open `donnavincent-logo-dark.png` and see white text, the file was misnamed by an earlier run; rename it.
86
86
 
@@ -23,7 +23,7 @@ The deliverable is identical to `property-brochure`'s outcome — written into `
23
23
 
24
24
  - `brochure.html` plus per-page 300 dpi PNG print snapshots (canonical, archival)
25
25
  - Two image-only PDFs: `<slug>-brochure-print.pdf` (300 dpi print master) and `<slug>-brochure-web.pdf` (192 dpi web/digital). Both built from per-page Playwright snapshots via img2pdf and linearized via qpdf — no Ghostscript anywhere in the chain. See `property-brochure → PDF deliverable` and `a4-print-documents → PDF deliverables`.
26
- - A self-contained **web bundle** at `output/web/` plus `output/<slug>-web.zip` — the smaller, ready-to-host version for property micro-sites; includes both the brochure and a companion **landing page** at `page.html` that presents the property as a continuous scrollable web page. See `property-brochure → Web bundle` and `property-brochure → Page`
26
+ - A self-contained **web bundle** at `output/web/` plus `output/<slug>-web.zip` — the smaller, ready-to-host version for property micro-sites; includes both the brochure and a companion **landing page** at `index.html` that presents the property as a continuous scrollable web page. See `property-brochure → Web bundle` and `property-brochure → Page`
27
27
 
28
28
  The orchestrator's value is in what it does **not** redo:
29
29
 
@@ -271,19 +271,19 @@ After a complete run, the on-disk layout is **exactly** this — no deeper nesti
271
271
  qr-video.png, qr-listing.png
272
272
  web/ # self-contained web bundle, generated alongside print archive
273
273
  brochure.html # same HTML; print-img refs point at .jpg snapshots; QR codes are clickable <a>
274
- page.html # companion property landing page (Modern House-style scroll)
274
+ index.html # companion property landing page (Modern House-style scroll)
275
275
  <property_slug>-brochure.pdf # digital PDF, copied for the in-page Download button
276
276
  cover-print.jpg
277
277
  page2-print.jpg … page15-print.jpg
278
278
  backpage-print.jpg
279
279
  images/ # web-tier per-slot encodings: hero 1300/q82, story 1100/q80, thumb 800/q76
280
280
  <property_slug>-NN.webp
281
- <property_slug>-hero-1-main.webp # page.html hero rotator — source-res @ q88
282
- <property_slug>-hero-2-<role>.webp # page.html hero rotator — interior moment
283
- <property_slug>-hero-3-<role>.webp # page.html hero rotator — garden / outdoor
281
+ <property_slug>-hero-1-main.webp # index.html hero rotator — source-res @ q88
282
+ <property_slug>-hero-2-<role>.webp # index.html hero rotator — interior moment
283
+ <property_slug>-hero-3-<role>.webp # index.html hero rotator — garden / outdoor
284
284
  <property_slug>-floorplan.png # line-art PNG, copied unchanged from canonical
285
- <brand_slug>-logo-light.png # light-coloured logo art — for dark surfaces (e.g. dark page.html sections)
286
- <brand_slug>-logo-dark.png # dark-coloured logo art — for light surfaces (e.g. page.html nav, brochure cover when paper)
285
+ <brand_slug>-logo-light.png # light-coloured logo art — for dark surfaces (e.g. dark index.html sections)
286
+ <brand_slug>-logo-dark.png # dark-coloured logo art — for light surfaces (e.g. index.html nav, brochure cover when paper)
287
287
  qr-video.png, qr-listing.png
288
288
  <property_slug>-web.zip # zipped form of web/, ready to upload to a static-site host
289
289
  ```
@@ -325,7 +325,7 @@ Determinism rules — a run that violates any of these is wrong, even if the bro
325
325
  | "House" instead of "home" at super-premium register | Wrong word for the buyer. "Home" is warmer, more aspirational, and matches the register. Replace globally. |
326
326
  | Text overlaid on photographs relying on a gradient + text-shadow for legibility | Insufficient against busy backgrounds (stone tile, golden-hour skies). Use a translucent dark panel directly behind the text. See `property-brochure → Text on images`. |
327
327
  | Including a detached gymnasium-summerhouse footprint in the principal home's first-floor sq ft | Misrepresents £/sq ft. List ancillary footprints separately. See `property-brochure → Floor area`. |
328
- | Shipping a web bundle without `page.html` | The web bundle is the property's hosted experience. The brochure is a downloadable folio; the landing page is the scrollable "for sale" page that links to it. Both are mandatory in the zip. See `property-brochure → Page`. |
328
+ | Shipping a web bundle without `index.html` | The web bundle is the property's hosted experience. The brochure is a downloadable folio; the landing page is the scrollable "for sale" page that links to it. Both are mandatory in the zip. See `property-brochure → Page`. |
329
329
  | Putting QR codes on the back page without a visible URL or a clickable wrapper | A QR is unscannable from the digital screen showing the brochure. The clickable URL beneath each QR is mandatory; it makes the digital reader experience equivalent to the print reader experience. See `property-brochure → Back-page QR codes`. |
330
330
  | Using the same logo variant for the back-page masthead (dark surface) and the landing-page nav (light surface) | The two surfaces need opposite logo variants — white-on-transparent for the back page, dark/coloured for the landing nav. Both must be present in `output/web/images/`. |
331
331
 
@@ -339,7 +339,7 @@ Report `DONE` only after:
339
339
  - Both PDF deliverables exist alongside `brochure.html`: the print master (`<property_slug>-brochure-print.pdf`, ~50–80 MB at 300 dpi) and the web/digital (`<property_slug>-brochure-web.pdf`, ~20–35 MB at 192 dpi). Both are image-only, built from per-page Playwright snapshots via img2pdf and linearized via qpdf; both pass `qpdf --check`, both report `Optimized: yes`, and both have zero embedded fonts (`pdffonts | wc -l` returns 2). See `a4-print-documents → Verification`.
340
340
  - All per-page print snapshots referenced by the HTML's print CSS exist at expected names — `cover-print.png`, `page2-print.png` through `page15-print.png`, and `backpage-print.png` (16 files for the 16-page folio) — at the orientation-correct 300 dpi pixel dimensions (3509×2481 landscape / 2481×3509 portrait).
341
341
  - The brochure has an **even page count** (canonical 16; multiples of 2 required, ideally multiples of 4 for booklet binding) — see `a4-print-documents` → Even-page count for duplex printing.
342
- - The **web bundle** exists at `output/web/` and as a zipped `output/<slug>-web.zip` — see `property-brochure → Web bundle` for what it contains. The bundle includes both `brochure.html` and the companion `page.html` (see `property-brochure → Page`), plus a copy of the web PDF named simply `<property_slug>-brochure.pdf` to match the in-page Download links. Smoke-test was performed: an isolated HTTP server returned 200 for every referenced asset (HTML, web-tier images, jpg snapshots, the bundled PDF), and **both** `brochure.html` and `page.html` rendered correctly. Typical zip is 30–50 MB for a 16-page folio.
342
+ - The **web bundle** exists at `output/web/` and as a zipped `output/<slug>-web.zip` — see `property-brochure → Web bundle` for what it contains. The bundle includes both `brochure.html` and the companion `index.html` (see `property-brochure → Page`), plus a copy of the web PDF named simply `<property_slug>-brochure.pdf` to match the in-page Download links. Smoke-test was performed: an isolated HTTP server returned 200 for every referenced asset (HTML, web-tier images, jpg snapshots, the bundled PDF), and **both** `brochure.html` and `index.html` rendered correctly. Typical zip is 30–50 MB for a 16-page folio.
343
343
  - **Back-page QR codes are clickable.** Each QR is wrapped in an `<a target="_blank">` tag and accompanied by a visible underlined URL beneath the label, so a digital reader can click instead of scan. See `property-brochure → Back-page QR codes`.
344
344
  - The user-facing report names which steps ran and which were reused, **and the chosen orientation**, **and the path to both the canonical print archive and the web zip**.
345
345
  - No image was read in violation of the 2000px rule (verifiable: every image read should have been preceded by a `sips`/`identify` measurement, and any over-threshold image was previewed via `/tmp/`).
@@ -9,7 +9,7 @@ Produce an A4 property brochure from raw assets. The default deliverable is a 16
9
9
 
10
10
  ## Output location
11
11
 
12
- By default the brochure is written to the **caller's current working directory**. The skill creates `./output/brochure.html`, `./output/web/page.html`, the print PNGs, and the two PDFs underneath `pwd` — no inference, no nesting under a brand workspace.
12
+ By default the brochure is written to the **caller's current working directory**. The skill creates `./output/brochure.html`, `./output/web/index.html`, the print PNGs, and the two PDFs underneath `pwd` — no inference, no nesting under a brand workspace.
13
13
 
14
14
  If you want the output elsewhere, pass `output_dir` explicitly (an absolute path or a relative path resolved against CWD). Common overrides:
15
15
 
@@ -23,12 +23,12 @@ Do not silently nest the output under a brand workspace just because one is pres
23
23
 
24
24
  ## Step 1 — copy the canonical templates (do this first)
25
25
 
26
- `references/template.html` and `references/page.html` are the **only** acceptable starting points. They are placeholder-only — every property value is a `{{ token }}`. Copy them before doing anything else. **Do not write brochure HTML or page HTML from scratch. Do not start from a sibling property's output. Do not improvise structure from prose.**
26
+ `references/template.html` and `references/index.html` are the **only** acceptable starting points. They are placeholder-only — every property value is a `{{ token }}`. Copy them before doing anything else. **Do not write brochure HTML or page HTML from scratch. Do not start from a sibling property's output. Do not improvise structure from prose.**
27
27
 
28
28
  ```bash
29
29
  mkdir -p <output_dir>/web
30
30
  cp ~/.claude/plugins/real-estate-brochure/skills/property-brochure/references/template.html <output_dir>/brochure.html
31
- cp ~/.claude/plugins/real-estate-brochure/skills/property-brochure/references/page.html <output_dir>/web/page.html
31
+ cp ~/.claude/plugins/real-estate-brochure/skills/property-brochure/references/index.html <output_dir>/web/index.html
32
32
  ```
33
33
 
34
34
  `<output_dir>` is whatever the **Output location** section above resolves to — typically `./output/` in the caller's CWD.
@@ -43,7 +43,7 @@ Both files must contain canonical sentinels (proves they came from `references/`
43
43
 
44
44
  ```bash
45
45
  grep -q '{{ property_name }}' <output_dir>/brochure.html || echo "FAIL: brochure not from template"
46
- grep -q 'hero-stage' <output_dir>/web/page.html || echo "FAIL: page.html not from references"
46
+ grep -q 'hero-stage' <output_dir>/web/index.html || echo "FAIL: index.html not from references"
47
47
  ```
48
48
 
49
49
  Both greps must succeed before populating content. If either fails, recopy.
@@ -74,14 +74,14 @@ Read the seller brief once before writing any copy. Then walk the `{{ placeholde
74
74
  | Editorial copy register, em-dash policy, "home" vs "house", text-on-images, typography, stats rows, AI hero prompt, **per-section composition guide** | [references/copy.md](references/copy.md) |
75
75
  | Orientation, 16-page layout, cover, floorplan, distinguishing features, Material Information, QR codes, location map | [references/structure.md](references/structure.md) |
76
76
  | Live editing, snapshot capture, PDF deliverable, web bundle | [references/build.md](references/build.md) |
77
- | `page.html` landing page — sections, hero rotator, mobile/drawer | [references/page-landing.md](references/page-landing.md) |
77
+ | `index.html` landing page — sections, hero rotator, mobile/drawer | [references/index-landing.md](references/index-landing.md) |
78
78
 
79
79
  ### Substitution completion gate
80
80
 
81
81
  A brochure with any `{{ x }}` still in the rendered HTML is **shipped broken**. Before any snapshot capture or PDF build, run:
82
82
 
83
83
  ```bash
84
- unsubstituted=$(grep -c '{{' "$output_dir/brochure.html" "$output_dir/web/page.html" 2>/dev/null | awk -F: '{s+=$2} END {print s}')
84
+ unsubstituted=$(grep -c '{{' "$output_dir/brochure.html" "$output_dir/web/index.html" 2>/dev/null | awk -F: '{s+=$2} END {print s}')
85
85
  [ "$unsubstituted" -eq 0 ] || { echo "DEFECT: $unsubstituted unsubstituted {{ tokens }} remain"; exit 1; }
86
86
  ```
87
87
 
@@ -106,7 +106,7 @@ unsubstituted=$(grep -c '{{' "$output_dir/brochure.html" "$output_dir/web/page.h
106
106
  - **Page count must be even** (16 default; 12 if the property is too small). 14 is forbidden.
107
107
  - **Image-only PDFs** — no fonts embedded, no Ghostscript anywhere in the chain.
108
108
  - **Self-contained `output/`** — never reference `../` paths. Every asset the HTML uses must live under `output/images/`.
109
- - **Web bundle must include `page.html`** — a bundle without it is incomplete.
109
+ - **Web bundle must include `index.html`** — a bundle without it is incomplete.
110
110
  - **Suppress screen-only chrome before snapshotting** — the template's `.download-bar` is hidden under `@media print` but Playwright's `element.screenshot()` captures the screen render, where the bar IS visible. The render script in `build.md` injects a style tag (`.download-bar { display: none !important; }`) before capture. Any custom render script must do the same.
111
111
  - **No `{{ token }}` may remain in rendered output** — `grep '{{' output/brochure.html` must return zero matches before PDFs are built.
112
112
 
@@ -164,7 +164,7 @@ The web bundle is a parallel directory with the same on-disk shape as `output/`
164
164
  ```
165
165
  output/web/
166
166
  brochure.html # identical content; only print-img references switched .png → .jpg
167
- page.html # companion landing page (mandatory in bundle)
167
+ index.html # companion landing page (mandatory in bundle)
168
168
  cover-print.jpg, page2-print.jpg … backpage-print.jpg # 96 dpi JPEG snapshots (q=88)
169
169
  <slug>-brochure.pdf # identical bytes to <slug>-brochure-web.pdf at the property level — simpler name inside the bundle since there's only one PDF here
170
170
  images/
@@ -220,7 +220,7 @@ A 16-page folio's web-bundle snapshots total ~3 MB (vs ~85 MB for the canonical
220
220
 
221
221
  ### Copy the web PDF into the bundle
222
222
 
223
- The web PDF (`<slug>-brochure-web.pdf` at the property level) is also placed inside the bundle, named simply `<slug>-brochure.pdf` to match `page.html` and `brochure.html` link conventions (where there's only one PDF in scope, no `-web` suffix is needed):
223
+ The web PDF (`<slug>-brochure-web.pdf` at the property level) is also placed inside the bundle, named simply `<slug>-brochure.pdf` to match `index.html` and `brochure.html` link conventions (where there's only one PDF in scope, no `-web` suffix is needed):
224
224
 
225
225
  ```bash
226
226
  cp output/<slug>-brochure-web.pdf output/web/<slug>-brochure.pdf
@@ -245,7 +245,7 @@ Serve the unzipped bundle from an isolated temp directory and verify every refer
245
245
  TMP=/tmp/web-test && rm -rf $TMP && mkdir -p $TMP
246
246
  cd $TMP && unzip -q /path/to/<slug>-web.zip
247
247
  python3 -m http.server 8765 &
248
- for f in brochure.html page.html cover-print.jpg images/<slug>-01.webp images/<brand>-logo-light.png <slug>-brochure.pdf; do
248
+ for f in brochure.html index.html cover-print.jpg images/<slug>-01.webp images/<brand>-logo-light.png <slug>-brochure.pdf; do
249
249
  echo "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8765/$f) $f"
250
250
  done
251
251
  ```
@@ -257,10 +257,10 @@ The brochure should render the same as the canonical preview, just lighter on th
257
257
  | File | `output/` (canonical archive) | `output/web/` (web bundle) |
258
258
  |---|---|---|
259
259
  | `brochure.html` | full-resolution images, `.png` snapshot refs | identical structure, `.jpg` snapshot refs, smaller image refs |
260
- | `page.html` | — | ✓ companion landing page; see `page-landing.md` |
260
+ | `index.html` | — | ✓ companion landing page; see `index-landing.md` |
261
261
  | `<slug>-brochure-print.pdf` | ✓ canonical print master (50–80 MB, 300 dpi) | — (too large to bundle) |
262
262
  | `<slug>-brochure-web.pdf` | ✓ web/digital deliverable (20–35 MB, 192 dpi) | — copied into bundle as `<slug>-brochure.pdf` |
263
- | `<slug>-brochure.pdf` | — | ✓ identical bytes to `-web.pdf`; matches page.html / brochure.html link |
263
+ | `<slug>-brochure.pdf` | — | ✓ identical bytes to `-web.pdf`; matches index.html / brochure.html link |
264
264
  | `cover-print.png … backpage-print.png` | ✓ 300 dpi PNG (~4–7 MB each) — canonical snapshots | — replaced by 96 dpi `.jpg` versions |
265
265
  | `cover-print.jpg … backpage-print.jpg` | — | ✓ 96 dpi JPEG (~150 KB each, derived from the 300 dpi PNGs) |
266
266
  | `images/<slug>-NN.webp` | full-quality per Render-slot table | web-tier per the web table above |
@@ -109,7 +109,7 @@ If five cells at the chosen font size do not fit the available row width, **shri
109
109
 
110
110
  ## Per-section composition guide
111
111
 
112
- This section is the writing brief for each placeholder block in `template.html` and `page.html`. The token names map 1:1 to entries in `placeholders.md`; this file says **how the copy should read** for each one.
112
+ This section is the writing brief for each placeholder block in `template.html` and `index.html`. The token names map 1:1 to entries in `placeholders.md`; this file says **how the copy should read** for each one.
113
113
 
114
114
  ### Cover & subtitle (page 1)
115
115
 
@@ -195,9 +195,9 @@ This section is the writing brief for each placeholder block in `template.html`
195
195
  - `{{ backpage_headline }}` — canonical phrasing "Arrange a viewing of <em>{{ property_name }}</em>". Do not paraphrase.
196
196
  - `{{ backpage_tagline }}` — single sentence, 25-45 words. Mirrors the cover/opener without duplicating phrases. Close with "Best understood in person." or an equivalent property-specific clincher.
197
197
 
198
- ### Landing page (page.html)
198
+ ### Landing page (index.html)
199
199
 
200
- The landing-page tokens (`{{ landing_* }}`) source the same brief but render in continuous-scroll form rather than a 16-page folio. Each `{{ landing_*_para_N }}` follows the same length/voice rules as the equivalent brochure paragraph — see placeholders.md → "Category 9 — page.html editorial copy" for the per-section breakdown.
200
+ The landing-page tokens (`{{ landing_* }}`) source the same brief but render in continuous-scroll form rather than a 16-page folio. Each `{{ landing_*_para_N }}` follows the same length/voice rules as the equivalent brochure paragraph — see placeholders.md → "Category 9 — index.html editorial copy" for the per-section breakdown.
201
201
 
202
202
  ## AI hero image prompt
203
203
 
@@ -120,7 +120,7 @@ The brochure is a **print-first deliverable**. Source images target the print-ma
120
120
  | **Map / screenshot** | screenshot with text labels | **2000 px** | source size, no resize | 88 |
121
121
  | **EPC chart** | small, already <100KB — copy through unchanged | n/a | as-is | as-is |
122
122
 
123
- **Floor plan exception — keep the source file as-is.** When the seller (or the agency's floor-plan provider) supplies a high-resolution PNG, **use it directly** — do not convert to WebP, do not downscale, do not re-encode. Floor plans are line art with thin strokes and crisp text where any quality loss is visibly worse than the same quality loss on photographic content. The same file (`<slug>-floorplan.png`) is referenced in the canonical brochure HTML, in the page.html landing page, and copied verbatim into `output/web/images/`. Because the new PDF pipeline rasterises whole pages from the rendered HTML (rather than passing the floor plan through a separate compression stage), the floor plan's quality at the PDF stage is determined by the snapshot DPI — 300 dpi for the print master, 192 dpi for the web PDF. A typical 2000 px floor plan slotted into a ~150 mm-wide brochure cell renders crisply at 300 dpi without any per-image processing. The **web bundle** also references the same `.png` (not a `.webp` substitute) — line art doesn't compress well as WebP and the file size is already modest.
123
+ **Floor plan exception — keep the source file as-is.** When the seller (or the agency's floor-plan provider) supplies a high-resolution PNG, **use it directly** — do not convert to WebP, do not downscale, do not re-encode. Floor plans are line art with thin strokes and crisp text where any quality loss is visibly worse than the same quality loss on photographic content. The same file (`<slug>-floorplan.png`) is referenced in the canonical brochure HTML, in the index.html landing page, and copied verbatim into `output/web/images/`. Because the new PDF pipeline rasterises whole pages from the rendered HTML (rather than passing the floor plan through a separate compression stage), the floor plan's quality at the PDF stage is determined by the snapshot DPI — 300 dpi for the print master, 192 dpi for the web PDF. A typical 2000 px floor plan slotted into a ~150 mm-wide brochure cell renders crisply at 300 dpi without any per-image processing. The **web bundle** also references the same `.png` (not a `.webp` substitute) — line art doesn't compress well as WebP and the file size is already modest.
124
124
 
125
125
  The **source-px floor** is the floor on the input photo's longest edge **before optimisation** — see the `Image resolution floor` section in the **a4-print-documents** SKILL for the rationale and full thresholds.
126
126