@rubytech/create-maxy-code 0.1.348 → 0.1.350

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 (102) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/docs/superpowers/plans/2026-06-23-account-filesystem-schema.md +544 -0
  3. package/payload/platform/docs/superpowers/specs/2026-06-23-account-filesystem-schema-design.md +170 -0
  4. package/payload/platform/lib/graph-style/dist/index.d.ts +1 -1
  5. package/payload/platform/lib/graph-style/dist/index.d.ts.map +1 -1
  6. package/payload/platform/lib/graph-style/dist/index.js +20 -12
  7. package/payload/platform/lib/graph-style/dist/index.js.map +1 -1
  8. package/payload/platform/lib/graph-style/src/__tests__/parity.test.ts +9 -9
  9. package/payload/platform/lib/graph-style/src/index.ts +20 -12
  10. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  11. package/payload/platform/lib/graph-write/dist/index.js +3 -2
  12. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  13. package/payload/platform/lib/graph-write/src/index.ts +3 -2
  14. package/payload/platform/neo4j/schema.cypher +110 -31
  15. package/payload/platform/plugins/.claude-plugin/marketplace.json +5 -0
  16. package/payload/platform/plugins/admin/hooks/__tests__/fs-schema-guard.test.sh +68 -0
  17. package/payload/platform/plugins/admin/hooks/fs-schema-guard.sh +108 -0
  18. package/payload/platform/plugins/admin/hooks/lib/maxy-mcp-plugins.txt +1 -0
  19. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +36 -3
  20. package/payload/platform/plugins/docs/references/calendar-booking.md +1 -1
  21. package/payload/platform/plugins/docs/references/joblogic.md +28 -0
  22. package/payload/platform/plugins/docs/references/memory-guide.md +1 -1
  23. package/payload/platform/plugins/joblogic/.claude-plugin/plugin.json +21 -0
  24. package/payload/platform/plugins/joblogic/PLUGIN.md +182 -0
  25. package/payload/platform/plugins/joblogic/lib/mcp-spawn-tee/index.js +193 -0
  26. package/payload/platform/plugins/joblogic/lib/mcp-spawn-tee/package.json +3 -0
  27. package/payload/platform/plugins/joblogic/mcp/dist/index.d.ts +2 -0
  28. package/payload/platform/plugins/joblogic/mcp/dist/index.d.ts.map +1 -0
  29. package/payload/platform/plugins/joblogic/mcp/dist/index.js +229 -0
  30. package/payload/platform/plugins/joblogic/mcp/dist/index.js.map +1 -0
  31. package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.d.ts +31 -0
  32. package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.d.ts.map +1 -0
  33. package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.js +78 -0
  34. package/payload/platform/plugins/joblogic/mcp/dist/lib/auth.js.map +1 -0
  35. package/payload/platform/plugins/joblogic/mcp/dist/lib/client.d.ts +35 -0
  36. package/payload/platform/plugins/joblogic/mcp/dist/lib/client.d.ts.map +1 -0
  37. package/payload/platform/plugins/joblogic/mcp/dist/lib/client.js +106 -0
  38. package/payload/platform/plugins/joblogic/mcp/dist/lib/client.js.map +1 -0
  39. package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.d.ts +8 -0
  40. package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.d.ts.map +1 -0
  41. package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.js +41 -0
  42. package/payload/platform/plugins/joblogic/mcp/dist/lib/idempotency.js.map +1 -0
  43. package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.d.ts +21 -0
  44. package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.d.ts.map +1 -0
  45. package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.js +47 -0
  46. package/payload/platform/plugins/joblogic/mcp/dist/lib/secrets.js.map +1 -0
  47. package/payload/platform/plugins/joblogic/mcp/package.json +10 -0
  48. package/payload/platform/plugins/joblogic/skills/joblogic/SKILL.md +32 -0
  49. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relationship-patterns.test.js +28 -22
  50. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/relationship-patterns.test.js.map +1 -1
  51. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js +8 -4
  52. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js.map +1 -1
  53. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-write-path-task802.test.js +39 -10
  54. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-write-path-task802.test.js.map +1 -1
  55. package/payload/platform/plugins/memory/mcp/dist/lib/relationship-patterns.d.ts +1 -1
  56. package/payload/platform/plugins/memory/mcp/dist/lib/relationship-patterns.js +1 -1
  57. package/payload/platform/plugins/memory/references/schema-construction.md +152 -57
  58. package/payload/platform/plugins/memory/references/schema-trades.md +22 -24
  59. package/payload/platform/scripts/lib/provision-account-dir.sh +22 -3
  60. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  61. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +46 -0
  62. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  63. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  64. package/payload/platform/services/claude-session-manager/dist/http-server.js +45 -1
  65. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  66. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +11 -0
  67. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  68. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +18 -0
  69. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  70. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +1 -0
  71. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  72. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +16 -0
  73. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  74. package/payload/platform/templates/account-schema/SCHEMA.md +54 -0
  75. package/payload/server/public/assets/{AdminLoginScreens-B_5wYcR-.js → AdminLoginScreens-BEzoTk56.js} +1 -1
  76. package/payload/server/public/assets/{AdminShell-CMV3XY-s.js → AdminShell-BG7YD4UO.js} +1 -1
  77. package/payload/server/public/assets/{Checkbox-LplFUKpb.js → Checkbox-CvTFOczj.js} +1 -1
  78. package/payload/server/public/assets/{OperatorConversations-C4Cx814-.css → OperatorConversations-CnP9Y6g2.css} +1 -1
  79. package/payload/server/public/assets/{admin-3QQtd7j2.js → admin-Bhxa1HOy.js} +1 -1
  80. package/payload/server/public/assets/{browser-D5q26eZL.js → browser-BAQFuyma.js} +1 -1
  81. package/payload/server/public/assets/calendar-sSr6zUjW.js +1 -0
  82. package/payload/server/public/assets/chat-ZH4Fsyu0.js +1 -0
  83. package/payload/server/public/assets/data-CBrgiyPM.js +1 -0
  84. package/payload/server/public/assets/{graph-DFQbqmi9.js → graph-DbGjFYPS.js} +1 -1
  85. package/payload/server/public/assets/graph-labels-IGIEr-uc.js +1 -0
  86. package/payload/server/public/assets/{operator-BlCyq4q1.js → operator-Ba5afDyM.js} +1 -1
  87. package/payload/server/public/assets/{page-drg1V-yA.js → page-D7uwMUOy.js} +1 -1
  88. package/payload/server/public/assets/{public-BVjeYfsD.js → public-DikYWzOd.js} +1 -1
  89. package/payload/server/public/browser.html +4 -4
  90. package/payload/server/public/calendar.html +4 -4
  91. package/payload/server/public/chat.html +5 -5
  92. package/payload/server/public/data.html +4 -4
  93. package/payload/server/public/graph.html +6 -6
  94. package/payload/server/public/index.html +6 -6
  95. package/payload/server/public/operator.html +7 -7
  96. package/payload/server/public/public.html +5 -5
  97. package/payload/server/server.js +92 -31
  98. package/payload/server/public/assets/calendar-Csbdj7rp.js +0 -1
  99. package/payload/server/public/assets/chat-Ct86qtMI.js +0 -1
  100. package/payload/server/public/assets/data-HwT5wPmj.js +0 -1
  101. package/payload/server/public/assets/graph-labels-kCti2CaC.js +0 -1
  102. /package/payload/server/public/assets/{OperatorConversations-Bp-g80PM.js → OperatorConversations-DU8CqO-z.js} +0 -0
@@ -447,7 +447,8 @@ DROP INDEX entity_search_admin IF EXISTS;
447
447
  CREATE FULLTEXT INDEX entity_search_admin IF NOT EXISTS
448
448
  FOR (n:LocalBusiness|Service|PriceSpecification|OpeningHoursSpecification|Organization
449
449
  |Listing|Property|Viewing|Offer|PostalAddress
450
- |Job|LineItem|Valuation|Milestone|QuoteDocument|VariationNote|InboundInvoice|CheckIn|WhatsAppGroup
450
+ |Job|Quote|QuoteLine|Valuation|Milestone|VariationNote|Invoice|InboundInvoice|JobCost|WhatsAppGroup
451
+ |Customer|Site|Asset|Visit|Part|PurchaseOrder|PpmContract|Contact|PurchaseOrderLine|InvoiceLine|InvoicePayment|Credit
451
452
  |Person|UserProfile|Preference|AdminUser|AccessGrant
452
453
  |KnowledgeDocument|ConversationArchive|Section|Chunk|DigitalDocument|CreativeWork|Question|FAQPage|DefinedTerm|Review|ImageObject
453
454
  |Conversation|AdminConversation|PublicConversation|Message|UserMessage|AssistantMessage|ToolCall
@@ -1348,22 +1349,27 @@ CREATE INDEX postal_address_postcode IF NOT EXISTS
1348
1349
  FOR (a:PostalAddress) ON (a.postalCode);
1349
1350
 
1350
1351
  // ----------------------------------------------------------
1351
- // Construction ontology (Task 650)Job-centric model for the
1352
- // sitedesk-code brand, documented at
1352
+ // Construction / field-service ontologythe canonical SiteDesk
1353
+ // service-delivery model, authored from JobLogic's field-service domain
1354
+ // (Task 1084; supersedes the Task 650 homegrown set). Documented at
1353
1355
  // platform/plugins/memory/references/schema-construction.md.
1354
1356
  //
1355
1357
  // Active vertical: brand.json#vertical = "schema-construction". Declared
1356
1358
  // here so a fresh deploy registers each label before its first write
1357
1359
  // (same fresh-install bootstrap contract as the estate-agent block above).
1358
1360
  //
1359
- // Uniqueness constraints where a clean natural key exists; accountId-index-
1360
- // only for child nodes whose uniqueness is a write-time MERGE (LineItem,
1361
- // Milestone, CheckIn) — mirrors :ImageObject. :Worker is an
1362
- // additional label on :Person (no standalone node), so it carries no line of
1363
- // its own here, same as :Applicant in the estate-agent block.
1364
- //
1365
- // Search quality (Task 652). The four text-bearing labels :Job (client +
1366
- // address), :LineItem (trade + description), :QuoteDocument, :VariationNote
1361
+ // Top-level natural-key labels: Job, Customer, Site, Asset, Visit, Quote,
1362
+ // Invoice (customer, outbound), InboundInvoice (supplier, on a PO), Part,
1363
+ // PurchaseOrder, PpmContract, WhatsAppGroup. Child
1364
+ // write-time-MERGE labels (accountId-index only, mirrors :ImageObject):
1365
+ // QuoteLine, Valuation, Milestone, VariationNote, JobCost, Contact,
1366
+ // PurchaseOrderLine, InvoiceLine, InvoicePayment, Credit. :Engineer (on
1367
+ // :Person) and :Supplier (on :Organization) are additional labels on a base
1368
+ // node no standalone line here, same as :Applicant in the estate-agent
1369
+ // block.
1370
+ //
1371
+ // Search quality (Task 652; renamed Task 1084). The four text-bearing labels —
1372
+ // :Job, :QuoteLine (trade + description), its :Quote, :VariationNote
1367
1373
  // (description) — carry a vector index on `.embedding` (below), so the
1368
1374
  // graph-write chokepoint auto-embeds them on write and they rank in the
1369
1375
  // vector half of memory-search. The four are mirrored in
@@ -1394,11 +1400,16 @@ OPTIONS {
1394
1400
  }
1395
1401
  };
1396
1402
 
1397
- CREATE INDEX line_item_account IF NOT EXISTS
1398
- FOR (li:LineItem) ON (li.accountId);
1403
+ // :LineItem -> :QuoteLine (Task 1084). A priced row of the client :Quote
1404
+ // schedule, not an incurred cost. The old-named indexes are dropped so a
1405
+ // database seeded before the rename does not keep a dead :LineItem index.
1406
+ DROP INDEX line_item_account IF EXISTS;
1407
+ DROP INDEX line_item_embedding IF EXISTS;
1408
+ CREATE INDEX quote_line_account IF NOT EXISTS
1409
+ FOR (ql:QuoteLine) ON (ql.accountId);
1399
1410
 
1400
- CREATE VECTOR INDEX line_item_embedding IF NOT EXISTS
1401
- FOR (li:LineItem) ON (li.embedding)
1411
+ CREATE VECTOR INDEX quote_line_embedding IF NOT EXISTS
1412
+ FOR (ql:QuoteLine) ON (ql.embedding)
1402
1413
  OPTIONS {
1403
1414
  indexConfig: {
1404
1415
  `vector.dimensions`: 768,
@@ -1415,14 +1426,20 @@ FOR (v:Valuation) ON (v.accountId);
1415
1426
  CREATE INDEX milestone_account IF NOT EXISTS
1416
1427
  FOR (m:Milestone) ON (m.accountId);
1417
1428
 
1418
- CREATE CONSTRAINT quote_document_account_ref_unique IF NOT EXISTS
1419
- FOR (q:QuoteDocument) REQUIRE (q.accountId, q.ref) IS UNIQUE;
1429
+ // :QuoteDocument -> :Quote (Task 1084) — JobLogic's canonical term for the
1430
+ // priced estimate/contract. New constraint+index names (IF NOT EXISTS matches
1431
+ // by name); old names dropped so the rename takes on pre-seeded databases.
1432
+ DROP CONSTRAINT quote_document_account_ref_unique IF EXISTS;
1433
+ DROP INDEX quote_document_account IF EXISTS;
1434
+ DROP INDEX quote_document_embedding IF EXISTS;
1435
+ CREATE CONSTRAINT quote_account_ref_unique IF NOT EXISTS
1436
+ FOR (q:Quote) REQUIRE (q.accountId, q.ref) IS UNIQUE;
1420
1437
 
1421
- CREATE INDEX quote_document_account IF NOT EXISTS
1422
- FOR (q:QuoteDocument) ON (q.accountId);
1438
+ CREATE INDEX quote_account IF NOT EXISTS
1439
+ FOR (q:Quote) ON (q.accountId);
1423
1440
 
1424
- CREATE VECTOR INDEX quote_document_embedding IF NOT EXISTS
1425
- FOR (q:QuoteDocument) ON (q.embedding)
1441
+ CREATE VECTOR INDEX quote_embedding IF NOT EXISTS
1442
+ FOR (q:Quote) ON (q.embedding)
1426
1443
  OPTIONS {
1427
1444
  indexConfig: {
1428
1445
  `vector.dimensions`: 768,
@@ -1445,22 +1462,31 @@ OPTIONS {
1445
1462
  }
1446
1463
  };
1447
1464
 
1448
- // Task 802 renamed the key property invoiceNumber -> confirmationNumber
1449
- // (Schema.org canonical). The constraint gets a NEW name because
1450
- // CREATE ... IF NOT EXISTS matches by constraint name: re-using the old
1451
- // name would silently skip creation on databases seeded before the rename,
1452
- // leaving the key unprotected. The old-name DROP retires the stale
1453
- // invoiceNumber constraint on those databases; both statements are
1454
- // idempotent on fresh installs.
1465
+ // Two distinct invoice documents (Task 1084). :InboundInvoice is the received
1466
+ // supplier invoice, keyed (accountId, supplier, confirmationNumber) and now
1467
+ // linked to a :PurchaseOrder. The customer-facing outbound invoice is the base
1468
+ // :Invoice (schema-base.md), registered here so it surfaces on /graph and in
1469
+ // search alongside the construction spine; its billing chain is the child
1470
+ // :InvoiceLine / :InvoicePayment / :Credit. The two labels coexist because they
1471
+ // carry different required fields (supplier vs customer). The Task-802 DROP of
1472
+ // the old invoiceNumber-keyed constraint is retained for pre-rename databases.
1455
1473
  DROP CONSTRAINT inbound_invoice_account_supplier_num_unique IF EXISTS;
1456
1474
  CREATE CONSTRAINT inbound_invoice_account_supplier_confirmation_unique IF NOT EXISTS
1457
1475
  FOR (ii:InboundInvoice) REQUIRE (ii.accountId, ii.supplier, ii.confirmationNumber) IS UNIQUE;
1458
-
1459
1476
  CREATE INDEX inbound_invoice_account IF NOT EXISTS
1460
1477
  FOR (ii:InboundInvoice) ON (ii.accountId);
1461
1478
 
1462
- CREATE INDEX checkin_account IF NOT EXISTS
1463
- FOR (ci:CheckIn) ON (ci.accountId);
1479
+ CREATE INDEX invoice_account IF NOT EXISTS
1480
+ FOR (i:Invoice) ON (i.accountId);
1481
+
1482
+ // :CheckIn -> :JobCost (Task 1084). A logged labour day is an incurred labour
1483
+ // cost (costType:'labour'); JobCost also carries material/travel/mileage/
1484
+ // expense/subcontractor/scheduleOfRates lines. Clean natural key (costId).
1485
+ DROP INDEX checkin_account IF EXISTS;
1486
+ CREATE CONSTRAINT job_cost_account_costid_unique IF NOT EXISTS
1487
+ FOR (jc:JobCost) REQUIRE (jc.accountId, jc.costId) IS UNIQUE;
1488
+ CREATE INDEX job_cost_account IF NOT EXISTS
1489
+ FOR (jc:JobCost) ON (jc.accountId);
1464
1490
 
1465
1491
  CREATE CONSTRAINT whatsapp_group_account_groupid_unique IF NOT EXISTS
1466
1492
  FOR (wg:WhatsAppGroup) REQUIRE (wg.accountId, wg.groupId) IS UNIQUE;
@@ -1468,6 +1494,59 @@ FOR (wg:WhatsAppGroup) REQUIRE (wg.accountId, wg.groupId) IS UNIQUE;
1468
1494
  CREATE INDEX whatsapp_group_account IF NOT EXISTS
1469
1495
  FOR (wg:WhatsAppGroup) ON (wg.accountId);
1470
1496
 
1497
+ // JobLogic service-delivery spine (Task 1084). Top-level natural-key labels.
1498
+ CREATE CONSTRAINT customer_account_customerid_unique IF NOT EXISTS
1499
+ FOR (c:Customer) REQUIRE (c.accountId, c.customerId) IS UNIQUE;
1500
+ CREATE INDEX customer_account IF NOT EXISTS
1501
+ FOR (c:Customer) ON (c.accountId);
1502
+
1503
+ CREATE CONSTRAINT site_account_siteid_unique IF NOT EXISTS
1504
+ FOR (s:Site) REQUIRE (s.accountId, s.siteId) IS UNIQUE;
1505
+ CREATE INDEX site_account IF NOT EXISTS
1506
+ FOR (s:Site) ON (s.accountId);
1507
+
1508
+ CREATE CONSTRAINT asset_account_assetid_unique IF NOT EXISTS
1509
+ FOR (a:Asset) REQUIRE (a.accountId, a.assetId) IS UNIQUE;
1510
+ CREATE INDEX asset_account IF NOT EXISTS
1511
+ FOR (a:Asset) ON (a.accountId);
1512
+
1513
+ CREATE CONSTRAINT visit_account_visitid_unique IF NOT EXISTS
1514
+ FOR (v:Visit) REQUIRE (v.accountId, v.visitId) IS UNIQUE;
1515
+ CREATE INDEX visit_account IF NOT EXISTS
1516
+ FOR (v:Visit) ON (v.accountId);
1517
+
1518
+ CREATE CONSTRAINT part_account_partid_unique IF NOT EXISTS
1519
+ FOR (p:Part) REQUIRE (p.accountId, p.partId) IS UNIQUE;
1520
+ CREATE INDEX part_account IF NOT EXISTS
1521
+ FOR (p:Part) ON (p.accountId);
1522
+
1523
+ CREATE CONSTRAINT purchase_order_account_ponumber_unique IF NOT EXISTS
1524
+ FOR (po:PurchaseOrder) REQUIRE (po.accountId, po.poNumber) IS UNIQUE;
1525
+ CREATE INDEX purchase_order_account IF NOT EXISTS
1526
+ FOR (po:PurchaseOrder) ON (po.accountId);
1527
+
1528
+ CREATE CONSTRAINT ppm_contract_account_contractid_unique IF NOT EXISTS
1529
+ FOR (pc:PpmContract) REQUIRE (pc.accountId, pc.contractId) IS UNIQUE;
1530
+ CREATE INDEX ppm_contract_account IF NOT EXISTS
1531
+ FOR (pc:PpmContract) ON (pc.accountId);
1532
+
1533
+ // Child write-time-MERGE labels (accountId-index only; uniqueness is the
1534
+ // writer's MERGE key, mirrors :ImageObject / :Milestone).
1535
+ CREATE INDEX contact_account IF NOT EXISTS
1536
+ FOR (ct:Contact) ON (ct.accountId);
1537
+
1538
+ CREATE INDEX purchase_order_line_account IF NOT EXISTS
1539
+ FOR (pol:PurchaseOrderLine) ON (pol.accountId);
1540
+
1541
+ CREATE INDEX invoice_line_account IF NOT EXISTS
1542
+ FOR (il:InvoiceLine) ON (il.accountId);
1543
+
1544
+ CREATE INDEX invoice_payment_account IF NOT EXISTS
1545
+ FOR (ip:InvoicePayment) ON (ip.accountId);
1546
+
1547
+ CREATE INDEX credit_account IF NOT EXISTS
1548
+ FOR (cr:Credit) ON (cr.accountId);
1549
+
1471
1550
  // ----------------------------------------------------------
1472
1551
  // Voice Mirror (Task 356 + Task 462) — authorial-voice capture + application
1473
1552
  // ----------------------------------------------------------
@@ -59,6 +59,11 @@
59
59
  "source": "./graph-viewer",
60
60
  "version": "0.1.0"
61
61
  },
62
+ {
63
+ "name": "joblogic",
64
+ "source": "./joblogic",
65
+ "version": "0.1.0"
66
+ },
62
67
  {
63
68
  "name": "linkedin-extension",
64
69
  "source": "./linkedin-extension",
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env bash
2
+ # Regression test for fs-schema-guard.sh.
3
+ #
4
+ # Covers:
5
+ # 1. Non-Write/Edit/NotebookEdit tool -> ALLOW (early exit)
6
+ # 2. Write to projects/Acme/a.txt (flat) -> ALLOW
7
+ # 3. Write to documents/a.pdf (loose) -> ALLOW
8
+ # 4. Write to output/deep/nested/x.png (tool-owned) -> ALLOW (any depth)
9
+ # 5. Write to projects/Acme/deploy/sources/a.txt -> BLOCK over-deep
10
+ # 6. Write to projects/Acme/deploy/a.txt -> BLOCK over-deep
11
+ # 7. Write to whim/x.txt (novel top-level) -> BLOCK top-level
12
+ # 8. Write to an absolute path outside the account dir -> ALLOW (not our concern)
13
+ # 9. NotebookEdit over-deep (notebook_path) -> BLOCK over-deep
14
+ # 10. Empty stdin (cannot inspect) -> BLOCK fail-closed
15
+ # 11. Allowed-set parse matches the documented set -> assertion
16
+ set -u
17
+
18
+ HOOK="$(cd "$(dirname "$0")/.." && pwd)/fs-schema-guard.sh"
19
+ [ -x "$HOOK" ] || { echo "FAIL: $HOOK not executable" >&2; exit 1; }
20
+
21
+ # A scratch account dir with a seeded SCHEMA.md (copied from the shipped template).
22
+ REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
23
+ TEMPLATE="$REPO_ROOT/templates/account-schema/SCHEMA.md"
24
+ [ -f "$TEMPLATE" ] || { echo "FAIL: template missing at $TEMPLATE" >&2; exit 1; }
25
+ ACCT=$(mktemp -d)
26
+ trap 'rm -rf "$ACCT"' EXIT
27
+ cp "$TEMPLATE" "$ACCT/SCHEMA.md"
28
+ mkdir -p "$ACCT/projects/Acme" "$ACCT/documents" "$ACCT/output"
29
+
30
+ PASS=0; FAIL=0
31
+ # Run one case in the account dir as cwd.
32
+ # $1 name $2 stdin $3 expected_exit $4 expected_log_re ("" = none)
33
+ run_case() {
34
+ local name="$1" stdin="$2" exp="$3" re="$4" ef actual
35
+ ef=$(mktemp)
36
+ ( cd "$ACCT" && printf '%s' "$stdin" | bash "$HOOK" >/dev/null 2>"$ef" )
37
+ actual=$?
38
+ local sc; sc=$(cat "$ef"); rm -f "$ef"
39
+ local ok=1
40
+ [ "$actual" -eq "$exp" ] || ok=0
41
+ if [ -n "$re" ] && ! printf '%s' "$sc" | grep -Eq "$re"; then ok=0; fi
42
+ if [ $ok -eq 1 ]; then echo "PASS: $name (exit=$actual)"; PASS=$((PASS+1));
43
+ else echo "FAIL: $name (exit=$actual, want $exp; log want /$re/)" >&2; FAIL=$((FAIL+1)); fi
44
+ }
45
+
46
+ mkenv() { printf '{"hook_event_name":"PreToolUse","tool_name":"%s","tool_input":{"%s":"%s"}}' "$1" "$2" "$3"; }
47
+
48
+ run_case "non-write allow" "$(mkenv Bash command 'ls')" 0 ""
49
+ run_case "flat project allow" "$(mkenv Write file_path 'projects/Acme/a.txt')" 0 ""
50
+ run_case "loose document allow" "$(mkenv Write file_path 'documents/a.pdf')" 0 ""
51
+ run_case "tool-owned deep allow" "$(mkenv Write file_path 'output/deep/nested/x.png')" 0 ""
52
+ run_case "over-deep block" "$(mkenv Write file_path 'projects/Acme/deploy/sources/a.txt')" 2 "fs-guard. blocked path=projects/Acme/deploy/sources/a.txt reason=over-deep"
53
+ run_case "over-deep one block" "$(mkenv Write file_path 'projects/Acme/deploy/a.txt')" 2 "reason=over-deep"
54
+ run_case "top-level block" "$(mkenv Write file_path 'whim/x.txt')" 2 "fs-guard. blocked path=whim/x.txt reason=top-level"
55
+ run_case "outside acct allow" "$(mkenv Write file_path '/etc/passwd')" 0 ""
56
+ run_case "notebook over-deep" "$(mkenv NotebookEdit notebook_path 'projects/Acme/deploy/n.ipynb')" 2 "reason=over-deep"
57
+ run_case "empty stdin block" "" 2 "no stdin"
58
+ # JSON null file_path must coerce to empty (allow), not the literal string "None".
59
+ run_case "null path allow" '{"hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":null}}' 0 ""
60
+
61
+ # Allowed-set parse == documented set.
62
+ EXPECT="projects contacts documents url-get output generated extracted uploads agents specialists sites public cache secrets state logs tmp SCHEMA.md account.json AGENTS.md .claude .git"
63
+ GOT=$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCT/SCHEMA.md" | tr '\n' ' ' | sed 's/ *$//')
64
+ if [ "$GOT" = "$EXPECT" ]; then echo "PASS: allowed-set parse"; PASS=$((PASS+1));
65
+ else echo "FAIL: allowed-set parse: got [$GOT]" >&2; FAIL=$((FAIL+1)); fi
66
+
67
+ echo "----- $PASS passed, $FAIL failed -----"
68
+ [ $FAIL -eq 0 ]
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env bash
2
+ # fs-schema-guard — PreToolUse Write/Edit/NotebookEdit path guard.
3
+ #
4
+ # Enforces the seeded account-dir file schema (see <accountDir>/SCHEMA.md):
5
+ # - the target's first path segment must be in the allowed top-level set
6
+ # (parsed from the fenced ```allowed-top-level block of the account's
7
+ # SCHEMA.md — our own structured data, not CLI prose);
8
+ # - a target under an operator-data bucket (projects/ contacts/) may be at
9
+ # most <bucket>/<entity>/<file> deep; documents/ may be at most
10
+ # <bucket>/<folder>/<file> deep. Tool-owned dirs pass at any depth.
11
+ #
12
+ # The hook governs the account dir only. A path resolving outside the account
13
+ # dir (cwd) is allowed — platform-source writes are PLATFORM_BOUNDARY's concern.
14
+ #
15
+ # Exit codes: 0 = allow, 2 = block (stderr shown to the agent). Fail closed when
16
+ # the tool call cannot be inspected (tty or empty stdin). Every block logs
17
+ # [fs-guard] blocked path=<rel> reason=<top-level|over-deep>
18
+ # No task numbers / internal refs in any operator-visible string.
19
+
20
+ set -uo pipefail
21
+
22
+ # Fail closed if attached to a terminal (no JSON envelope coming).
23
+ if [ -t 0 ]; then
24
+ echo "Blocked: fs-schema-guard received no stdin (cannot inspect the write). Failing closed." >&2
25
+ exit 2
26
+ fi
27
+ INPUT=$(cat)
28
+ if [ -z "$INPUT" ]; then
29
+ echo "Blocked: fs-schema-guard received no stdin (cannot inspect the write). Failing closed." >&2
30
+ exit 2
31
+ fi
32
+
33
+ field() {
34
+ printf '%s' "$INPUT" | python3 -c "
35
+ import sys, json
36
+ try:
37
+ d = json.load(sys.stdin)
38
+ v = (d.get('$1', {}) or {}).get('$2', '') if '$1' else d.get('$2', '')
39
+ print(v if isinstance(v, str) else '')
40
+ except Exception:
41
+ print('')
42
+ " 2>/dev/null || echo ""
43
+ }
44
+
45
+ TOOL_NAME=$(field '' tool_name)
46
+ case "$TOOL_NAME" in
47
+ Write|Edit) FILE_PATH=$(field tool_input file_path) ;;
48
+ NotebookEdit) FILE_PATH=$(field tool_input notebook_path) ;;
49
+ *) exit 0 ;;
50
+ esac
51
+ [ -z "$FILE_PATH" ] && exit 0
52
+
53
+ ACCOUNT_DIR="$PWD"
54
+
55
+ # Resolve to an account-relative path; allow anything outside the account dir.
56
+ REL=$(FILE_PATH="$FILE_PATH" ACCOUNT_DIR="$ACCOUNT_DIR" python3 -c '
57
+ import os, sys
58
+ fp = os.environ["FILE_PATH"]; acct = os.environ["ACCOUNT_DIR"]
59
+ ab = fp if os.path.isabs(fp) else os.path.join(acct, fp)
60
+ ab = os.path.normpath(ab); acct = os.path.normpath(acct)
61
+ if ab == acct or not ab.startswith(acct + os.sep):
62
+ print("") # outside the account dir (or the dir itself)
63
+ else:
64
+ print(os.path.relpath(ab, acct))
65
+ ' 2>/dev/null || echo "")
66
+ [ -z "$REL" ] && exit 0
67
+
68
+ # Parse the allowed top-level set from the account's seeded SCHEMA.md.
69
+ ALLOWED=""
70
+ if [ -f "$ACCOUNT_DIR/SCHEMA.md" ]; then
71
+ ALLOWED=$(awk '/^```allowed-top-level$/{f=1;next} /^```$/{f=0} f' "$ACCOUNT_DIR/SCHEMA.md")
72
+ fi
73
+ # Fail open on a missing/empty schema (an un-seeded legacy account must not have
74
+ # every write blocked). A re-provision seeds SCHEMA.md, after which the guard
75
+ # applies.
76
+ [ -z "$ALLOWED" ] && exit 0
77
+
78
+ SEG0=${REL%%/*}
79
+
80
+ # Top-level check.
81
+ if ! printf '%s\n' "$ALLOWED" | grep -qxF "$SEG0"; then
82
+ echo "[fs-guard] blocked path=$REL reason=top-level" >&2
83
+ echo "Blocked: '$SEG0' is not an allowed top-level entry in this account's data directory. The layout is fixed by SCHEMA.md — place operator data under projects/, contacts/, or documents/, and route any reorganisation through the data-manager specialist." >&2
84
+ exit 2
85
+ fi
86
+
87
+ # Over-deep check for operator-data buckets. Count path segments.
88
+ depth=$(printf '%s' "$REL" | awk -F/ '{print NF}')
89
+ case "$SEG0" in
90
+ projects|contacts)
91
+ # Allowed: <bucket>/<entity>/<file> = 3 segments max; <bucket>/<file> = 2 ok.
92
+ if [ "$depth" -gt 3 ]; then
93
+ echo "[fs-guard] blocked path=$REL reason=over-deep" >&2
94
+ echo "Blocked: $SEG0/ is flat — one folder per entity, then files. '$REL' nests a subtree under an entity folder, which has no basis in the graph ontology. Keep files directly under $SEG0/<name>/, or route a reorganisation through the data-manager specialist." >&2
95
+ exit 2
96
+ fi
97
+ ;;
98
+ documents)
99
+ # Allowed: documents/<file> = 2, documents/<folder>/<file> = 3 max.
100
+ if [ "$depth" -gt 3 ]; then
101
+ echo "[fs-guard] blocked path=$REL reason=over-deep" >&2
102
+ echo "Blocked: documents/ holds files or one folder deep. '$REL' nests deeper, which has no basis in the graph ontology. Route a reorganisation through the data-manager specialist." >&2
103
+ exit 2
104
+ fi
105
+ ;;
106
+ esac
107
+
108
+ exit 0
@@ -5,6 +5,7 @@ contacts
5
5
  email
6
6
  graph
7
7
  graph-viewer
8
+ joblogic
8
9
  memory
9
10
  outlook
10
11
  quickbooks
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
4
- content-hash: sha256:0d3d43e2f324aa4f5ab8b192aecd67c30a96b4610c7226a18e7bfb515ffd5b88
4
+ content-hash: sha256:33a6a5683cd3a9f34b0fcbd1d108c7fe20aece1d7d64ecc6da321ff0a8742f08
5
5
  brand: maxy-code
6
6
  product-name: Maxy
7
7
  ---
@@ -1556,7 +1556,7 @@ Every node also carries a provenance stamp — which agent wrote it, in which se
1556
1556
 
1557
1557
  ## Vertical schemas
1558
1558
 
1559
- On top of the base graph, each brand boots one optional **vertical** — an extra set of entity types tailored to a trade. The vertical is named by `brand.json#vertical` and defined in a `schema-<name>.md` reference; the memory plugin loads it at startup and validates every write against base + the active vertical. Real Agent boots `schema-estate-agent` (Listing, Property, Viewing, Offer). SiteDesk boots `schema-construction`, which adds the building-contractor entities — `Job`, `LineItem`, `Valuation`, `Milestone`, `QuoteDocument`, `VariationNote`, `InboundInvoice`, `WhatsAppGroup` — grounded in real builder job folders so a job's quote, valuations, variations, and received supplier and subcontractor invoices all hang off one `Job` node. The default Maxy brand boots no vertical (base graph only).
1559
+ On top of the base graph, each brand boots one optional **vertical** — an extra set of entity types tailored to a trade. The vertical is named by `brand.json#vertical` and defined in a `schema-<name>.md` reference; the memory plugin loads it at startup and validates every write against base + the active vertical. Real Agent boots `schema-estate-agent` (Listing, Property, Viewing, Offer). SiteDesk boots `schema-construction`, the canonical JobLogic-derived field-service ontology, which adds the service-delivery entities — `Job`, `Customer`, `Site`, `Asset`, `Visit`, `Quote`, `QuoteLine`, `Valuation`, `VariationNote`, `JobCost`, `Part`, `PurchaseOrder`, `Invoice`, `InboundInvoice`, `PpmContract`, `WhatsAppGroup` (with `Engineer` and `Supplier` as additional labels on `Person`/`Organization`) — grounded in real builder job folders so a job's customer, site, quote, valuations, variations, costs, and received supplier invoices all hang off one `Job` node. The default Maxy brand boots no vertical (base graph only).
1560
1560
 
1561
1561
  ## Public-facing summaries for customer-readable subjects
1562
1562
 
@@ -1880,6 +1880,39 @@ A QuickBooks connection goes stale if it sits unused for about 100 days, or if y
1880
1880
 
1881
1881
  Switching your Intuit app from sandbox to production requires passing Intuit's own production security review. That is between you and Intuit; it is not something the agent does for you.
1882
1882
 
1883
+ ---
1884
+ # JobLogic
1885
+ Source: https://docs.getmaxy.com/joblogic.md
1886
+
1887
+ # JobLogic
1888
+
1889
+ Connect JobLogic, the field-service management system, so the agent can read and update your jobs end to end — customers, sites, jobs, engineers, visits, quotes, invoices, assets, and timesheets — over JobLogic's REST API.
1890
+
1891
+ This connector is built for one team that already runs JobLogic as its system of record. Your field staff send a voice note or message to Maxy from the field; Maxy works out what they mean and makes the JobLogic update for them. Your staff never touch the JobLogic API themselves, so only Maxy's own connection talks to JobLogic.
1892
+
1893
+ ## What you do once
1894
+
1895
+ **1. Ask JobLogic for API access.** Raise a ticket with JobLogic Support for API credentials. Ask for a test (UAT) account first, then production. They give you a client ID, a client secret, and your company tenant id.
1896
+
1897
+ **2. Whitelist your connection.** JobLogic only accepts API calls from network addresses you have registered with them. Maxy makes its calls from the machine it runs on, so that machine's public address is the one to register. If the machine moves between networks its address can change, so either run it from one place with a stable address, send its traffic through a fixed address, or ask JobLogic whether production needs this at all. If the address is not registered, calls are refused and the agent will tell you the connection was blocked.
1898
+
1899
+ **3. Hand the keys to the agent.** Tell the agent your client ID, secret, tenant id, and whether they are test or production. The agent stores them on your install only and never repeats them back.
1900
+
1901
+ ## What the agent does after that
1902
+
1903
+ Everything else is automatic. The agent keeps the connection alive on its own, refreshing access behind the scenes, and can:
1904
+
1905
+ - **Read** anything — "what jobs are open for this site", "show this engineer's visits this week", "list unpaid invoices".
1906
+ - **Write** from a field instruction — create or reschedule a visit, log labour and materials against a job, set a job's status, approve a quote, raise an invoice, add a note or a photo to a job.
1907
+
1908
+ Because JobLogic's credential belongs to the company rather than to a person, the agent stamps each update with the engineer who reported it. You give the agent a list that maps each staff member's phone number to their JobLogic engineer record once, and it does the rest.
1909
+
1910
+ Each create carries a safety key, so if a message is retried or a reply goes missing the same instruction never creates a duplicate record.
1911
+
1912
+ ## Testing
1913
+
1914
+ Do all your testing against the JobLogic test (UAT) environment first. Switch to production only once the test environment behaves the way you expect and your production keys and whitelisting are in place.
1915
+
1883
1916
  ---
1884
1917
  # Calendar and Booking
1885
1918
  Source: https://docs.getmaxy.com/calendar-booking.md
@@ -1898,7 +1931,7 @@ Open `/calendar` on your Maxy address (the same sign-in as the dashboard). It sh
1898
1931
 
1899
1932
  Use **Today** and the arrows to move around, and the view buttons to switch. The calendar reads everything time-bound in your graph: meetings imported from a calendar export or created by a booking, appointments Maxy schedules for you in chat, and tasks that have a due date (shown on their day).
1900
1933
 
1901
- Click any entry to open its details: the full title, time, location, and who is attending, with each attendee on its own line (name, and email beneath it). For a booked meeting you can also edit its title, time, or location, or delete it, and the grid updates straight away. You can **Download** a booked meeting as a calendar file (`.ics`) to add it to Apple, Google, or Outlook calendars; an **Include attendees** toggle next to the button decides whether the file carries the attendee list (off by default). The private note is never written into the downloaded file, since the file can travel outside your admin. In the edit view you can add an attendee (give a name, an email, or both) and remove one; adding someone who is already a contact reuses that contact rather than making a duplicate, and removing them takes them off the meeting but keeps the contact. The edit view also has a private note for that meeting, a place for your own reminders that only you and your team ever see; it never appears on the public booking page or anywhere outside your admin. Appointments Maxy schedules and due-dated tasks are shown for detail only and are changed where they are created, not here. New meetings are still made by a booking or by asking Maxy in chat, not from the calendar.
1934
+ Click any entry to open its details: the full title, time, location, and who is attending, with each attendee on its own line (name, and email beneath it). For a booked meeting you can also edit its title, time, or location, or delete it, and the grid updates straight away. You can **Download** a booked meeting as a calendar file (`.ics`) to add it to Apple, Google, or Outlook calendars; when the meeting has attendees, Download asks whether to include them in the file before it saves, and a meeting with no attendees downloads straight away. The private note is never written into the downloaded file, since the file can travel outside your admin. In the edit view you can add an attendee (give a name, an email, or both) and remove one; adding someone who is already a contact reuses that contact rather than making a duplicate, and removing them takes them off the meeting but keeps the contact. The edit view also has a private note for that meeting, a place for your own reminders that only you and your team ever see; it never appears on the public booking page or anywhere outside your admin. Appointments Maxy schedules and due-dated tasks are shown for detail only and are changed where they are created, not here. New meetings are still made by a booking or by asking Maxy in chat, not from the calendar.
1902
1935
 
1903
1936
  Everything on the calendar comes from records the rest of Maxy already uses, scoped to your account.
1904
1937
 
@@ -12,7 +12,7 @@ Open `/calendar` on your Maxy address (the same sign-in as the dashboard). It sh
12
12
 
13
13
  Use **Today** and the arrows to move around, and the view buttons to switch. The calendar reads everything time-bound in your graph: meetings imported from a calendar export or created by a booking, appointments Maxy schedules for you in chat, and tasks that have a due date (shown on their day).
14
14
 
15
- Click any entry to open its details: the full title, time, location, and who is attending, with each attendee on its own line (name, and email beneath it). For a booked meeting you can also edit its title, time, or location, or delete it, and the grid updates straight away. You can **Download** a booked meeting as a calendar file (`.ics`) to add it to Apple, Google, or Outlook calendars; an **Include attendees** toggle next to the button decides whether the file carries the attendee list (off by default). The private note is never written into the downloaded file, since the file can travel outside your admin. In the edit view you can add an attendee (give a name, an email, or both) and remove one; adding someone who is already a contact reuses that contact rather than making a duplicate, and removing them takes them off the meeting but keeps the contact. The edit view also has a private note for that meeting, a place for your own reminders that only you and your team ever see; it never appears on the public booking page or anywhere outside your admin. Appointments Maxy schedules and due-dated tasks are shown for detail only and are changed where they are created, not here. New meetings are still made by a booking or by asking Maxy in chat, not from the calendar.
15
+ Click any entry to open its details: the full title, time, location, and who is attending, with each attendee on its own line (name, and email beneath it). For a booked meeting you can also edit its title, time, or location, or delete it, and the grid updates straight away. You can **Download** a booked meeting as a calendar file (`.ics`) to add it to Apple, Google, or Outlook calendars; when the meeting has attendees, Download asks whether to include them in the file before it saves, and a meeting with no attendees downloads straight away. The private note is never written into the downloaded file, since the file can travel outside your admin. In the edit view you can add an attendee (give a name, an email, or both) and remove one; adding someone who is already a contact reuses that contact rather than making a duplicate, and removing them takes them off the meeting but keeps the contact. The edit view also has a private note for that meeting, a place for your own reminders that only you and your team ever see; it never appears on the public booking page or anywhere outside your admin. Appointments Maxy schedules and due-dated tasks are shown for detail only and are changed where they are created, not here. New meetings are still made by a booking or by asking Maxy in chat, not from the calendar.
16
16
 
17
17
  Everything on the calendar comes from records the rest of Maxy already uses, scoped to your account.
18
18
 
@@ -0,0 +1,28 @@
1
+ # JobLogic
2
+
3
+ Connect JobLogic, the field-service management system, so the agent can read and update your jobs end to end — customers, sites, jobs, engineers, visits, quotes, invoices, assets, and timesheets — over JobLogic's REST API.
4
+
5
+ This connector is built for one team that already runs JobLogic as its system of record. Your field staff send a voice note or message to {{productName}} from the field; {{productName}} works out what they mean and makes the JobLogic update for them. Your staff never touch the JobLogic API themselves, so only {{productName}}'s own connection talks to JobLogic.
6
+
7
+ ## What you do once
8
+
9
+ **1. Ask JobLogic for API access.** Raise a ticket with JobLogic Support for API credentials. Ask for a test (UAT) account first, then production. They give you a client ID, a client secret, and your company tenant id.
10
+
11
+ **2. Whitelist your connection.** JobLogic only accepts API calls from network addresses you have registered with them. {{productName}} makes its calls from the machine it runs on, so that machine's public address is the one to register. If the machine moves between networks its address can change, so either run it from one place with a stable address, send its traffic through a fixed address, or ask JobLogic whether production needs this at all. If the address is not registered, calls are refused and the agent will tell you the connection was blocked.
12
+
13
+ **3. Hand the keys to the agent.** Tell the agent your client ID, secret, tenant id, and whether they are test or production. The agent stores them on your install only and never repeats them back.
14
+
15
+ ## What the agent does after that
16
+
17
+ Everything else is automatic. The agent keeps the connection alive on its own, refreshing access behind the scenes, and can:
18
+
19
+ - **Read** anything — "what jobs are open for this site", "show this engineer's visits this week", "list unpaid invoices".
20
+ - **Write** from a field instruction — create or reschedule a visit, log labour and materials against a job, set a job's status, approve a quote, raise an invoice, add a note or a photo to a job.
21
+
22
+ Because JobLogic's credential belongs to the company rather than to a person, the agent stamps each update with the engineer who reported it. You give the agent a list that maps each staff member's phone number to their JobLogic engineer record once, and it does the rest.
23
+
24
+ Each create carries a safety key, so if a message is retried or a reply goes missing the same instruction never creates a duplicate record.
25
+
26
+ ## Testing
27
+
28
+ Do all your testing against the JobLogic test (UAT) environment first. Switch to production only once the test environment behaves the way you expect and your production keys and whitelisting are in place.
@@ -144,7 +144,7 @@ Every node also carries a provenance stamp — which agent wrote it, in which se
144
144
 
145
145
  ## Vertical schemas
146
146
 
147
- On top of the base graph, each brand boots one optional **vertical** — an extra set of entity types tailored to a trade. The vertical is named by `brand.json#vertical` and defined in a `schema-<name>.md` reference; the memory plugin loads it at startup and validates every write against base + the active vertical. Real Agent boots `schema-estate-agent` (Listing, Property, Viewing, Offer). SiteDesk boots `schema-construction`, which adds the building-contractor entities — `Job`, `LineItem`, `Valuation`, `Milestone`, `QuoteDocument`, `VariationNote`, `InboundInvoice`, `WhatsAppGroup` — grounded in real builder job folders so a job's quote, valuations, variations, and received supplier and subcontractor invoices all hang off one `Job` node. The default Maxy brand boots no vertical (base graph only).
147
+ On top of the base graph, each brand boots one optional **vertical** — an extra set of entity types tailored to a trade. The vertical is named by `brand.json#vertical` and defined in a `schema-<name>.md` reference; the memory plugin loads it at startup and validates every write against base + the active vertical. Real Agent boots `schema-estate-agent` (Listing, Property, Viewing, Offer). SiteDesk boots `schema-construction`, the canonical JobLogic-derived field-service ontology, which adds the service-delivery entities — `Job`, `Customer`, `Site`, `Asset`, `Visit`, `Quote`, `QuoteLine`, `Valuation`, `VariationNote`, `JobCost`, `Part`, `PurchaseOrder`, `Invoice`, `InboundInvoice`, `PpmContract`, `WhatsAppGroup` (with `Engineer` and `Supplier` as additional labels on `Person`/`Organization`) — grounded in real builder job folders so a job's customer, site, quote, valuations, variations, costs, and received supplier invoices all hang off one `Job` node. The default Maxy brand boots no vertical (base graph only).
148
148
 
149
149
  ## Public-facing summaries for customer-readable subjects
150
150
 
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "joblogic",
3
+ "description": "JobLogic field-service connector (bespoke, one account) — OAuth client_credentials, tenant-scoped read/write across the job lifecycle.",
4
+ "version": "0.1.0",
5
+ "author": {
6
+ "name": "Rubytech LLC"
7
+ },
8
+ "mcpServers": {
9
+ "joblogic": {
10
+ "type": "stdio",
11
+ "command": "node",
12
+ "args": [
13
+ "${CLAUDE_PLUGIN_ROOT}/lib/mcp-spawn-tee/index.js",
14
+ "${CLAUDE_PLUGIN_ROOT}/mcp/dist/index.js"
15
+ ],
16
+ "env": {
17
+ "MCP_SPAWN_TEE_NAME": "joblogic"
18
+ }
19
+ }
20
+ }
21
+ }