@rubytech/create-realagent-code 0.1.584 → 0.1.586

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 (108) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +1 -0
  3. package/payload/platform/lib/brand-templating/dist/index.d.ts +13 -7
  4. package/payload/platform/lib/brand-templating/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/brand-templating/dist/index.js +44 -29
  6. package/payload/platform/lib/brand-templating/dist/index.js.map +1 -1
  7. package/payload/platform/lib/brand-templating/src/__tests__/brand-templating.test.ts +103 -0
  8. package/payload/platform/lib/brand-templating/src/index.ts +45 -29
  9. package/payload/platform/lib/brand-templating/tsconfig.json +2 -1
  10. package/payload/platform/lib/brand-templating/vitest.config.ts +9 -0
  11. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -1
  12. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
  13. package/payload/platform/plugins/docs/references/platform.md +2 -0
  14. package/payload/platform/templates/agents/admin/IDENTITY.md +11 -5
  15. package/payload/server/{chunk-CV6AEMKH.js → chunk-34FPJYHN.js} +1 -1
  16. package/payload/server/{chunk-SKYZG6OT.js → chunk-6AQYMT6C.js} +27 -21
  17. package/payload/server/{chunk-BTXRBXMU.js → chunk-MNN2RWJR.js} +21 -3
  18. package/payload/server/{manager-FQSQSSJ3.js → manager-7BXVB76I.js} +2 -2
  19. package/payload/server/maxy-edge.js +2 -2
  20. package/payload/server/public/activity.html +2 -2
  21. package/payload/server/public/agents.html +2 -2
  22. package/payload/server/public/assets/{AdminLoginScreens-BLsDYWVi.js → AdminLoginScreens-C9-USB3F.js} +1 -1
  23. package/payload/server/public/assets/AdminLoginScreens-C9-USB3F.js.br +0 -0
  24. package/payload/server/public/assets/AdminLoginScreens-C9-USB3F.js.gz +0 -0
  25. package/payload/server/public/assets/AdminShell-BtWb0_Gm.js +3 -0
  26. package/payload/server/public/assets/AdminShell-BtWb0_Gm.js.br +0 -0
  27. package/payload/server/public/assets/AdminShell-BtWb0_Gm.js.gz +0 -0
  28. package/payload/server/public/assets/{activity-DOjvoLdd.js → activity-qTF3J92s.js} +1 -1
  29. package/payload/server/public/assets/activity-qTF3J92s.js.br +0 -0
  30. package/payload/server/public/assets/activity-qTF3J92s.js.gz +0 -0
  31. package/payload/server/public/assets/{admin-JrSNg29g.js → admin-PBRDZ8OG.js} +1 -1
  32. package/payload/server/public/assets/admin-PBRDZ8OG.js.br +0 -0
  33. package/payload/server/public/assets/admin-PBRDZ8OG.js.gz +0 -0
  34. package/payload/server/public/assets/{agents--9WMJVLC.js → agents-DNSBvEgj.js} +1 -1
  35. package/payload/server/public/assets/agents-DNSBvEgj.js.br +0 -0
  36. package/payload/server/public/assets/agents-DNSBvEgj.js.gz +0 -0
  37. package/payload/server/public/assets/{browser-DiiMmTrM.js → browser-D68qFX85.js} +1 -1
  38. package/payload/server/public/assets/browser-D68qFX85.js.br +0 -0
  39. package/payload/server/public/assets/browser-D68qFX85.js.gz +0 -0
  40. package/payload/server/public/assets/{calendar-CazmCzgO.js → calendar-DiyBcrbJ.js} +1 -1
  41. package/payload/server/public/assets/calendar-DiyBcrbJ.js.br +0 -0
  42. package/payload/server/public/assets/calendar-DiyBcrbJ.js.gz +0 -0
  43. package/payload/server/public/assets/{chat-CB0TcyXX.js → chat-BgmAjdkp.js} +1 -1
  44. package/payload/server/public/assets/chat-BgmAjdkp.js.br +1 -0
  45. package/payload/server/public/assets/chat-BgmAjdkp.js.gz +0 -0
  46. package/payload/server/public/assets/{graph-CVJKz1Uo.js → graph-fZwMrLTq.js} +1 -1
  47. package/payload/server/public/assets/graph-fZwMrLTq.js.br +0 -0
  48. package/payload/server/public/assets/graph-fZwMrLTq.js.gz +0 -0
  49. package/payload/server/public/assets/{operator-BGvB4hu4.js → operator-BuRF6qBf.js} +1 -1
  50. package/payload/server/public/assets/operator-BuRF6qBf.js.br +0 -0
  51. package/payload/server/public/assets/operator-BuRF6qBf.js.gz +0 -0
  52. package/payload/server/public/assets/{page-DBQLOasI.js → page-C4NwxoSJ.js} +1 -1
  53. package/payload/server/public/assets/page-C4NwxoSJ.js.br +0 -0
  54. package/payload/server/public/assets/page-C4NwxoSJ.js.gz +0 -0
  55. package/payload/server/public/assets/{public-BvUQR3hd.js → public-CBrcZ2Sz.js} +1 -1
  56. package/payload/server/public/assets/public-CBrcZ2Sz.js.br +0 -0
  57. package/payload/server/public/assets/public-CBrcZ2Sz.js.gz +0 -0
  58. package/payload/server/public/assets/{routines-N8NmxPOZ.js → routines-Cf1PwvDn.js} +1 -1
  59. package/payload/server/public/assets/routines-Cf1PwvDn.js.br +0 -0
  60. package/payload/server/public/assets/routines-Cf1PwvDn.js.gz +0 -0
  61. package/payload/server/public/assets/{skills-B2CRgO-U.js → skills-5s3kkr0j.js} +1 -1
  62. package/payload/server/public/assets/skills-5s3kkr0j.js.br +0 -0
  63. package/payload/server/public/assets/skills-5s3kkr0j.js.gz +0 -0
  64. package/payload/server/public/assets/{tasks-8QLGtlSj.js → tasks-DBRSqw5m.js} +1 -1
  65. package/payload/server/public/assets/tasks-DBRSqw5m.js.br +0 -0
  66. package/payload/server/public/assets/tasks-DBRSqw5m.js.gz +0 -0
  67. package/payload/server/public/browser.html +2 -2
  68. package/payload/server/public/calendar.html +2 -2
  69. package/payload/server/public/chat.html +3 -3
  70. package/payload/server/public/graph.html +2 -2
  71. package/payload/server/public/index.html +3 -3
  72. package/payload/server/public/operator.html +4 -4
  73. package/payload/server/public/public.html +3 -3
  74. package/payload/server/public/routines.html +2 -2
  75. package/payload/server/public/skills.html +2 -2
  76. package/payload/server/public/tasks.html +2 -2
  77. package/payload/server/server.js +24 -6
  78. package/payload/server/public/assets/AdminLoginScreens-BLsDYWVi.js.br +0 -0
  79. package/payload/server/public/assets/AdminLoginScreens-BLsDYWVi.js.gz +0 -0
  80. package/payload/server/public/assets/AdminShell-BZS8jzFA.js +0 -3
  81. package/payload/server/public/assets/AdminShell-BZS8jzFA.js.br +0 -0
  82. package/payload/server/public/assets/AdminShell-BZS8jzFA.js.gz +0 -0
  83. package/payload/server/public/assets/activity-DOjvoLdd.js.br +0 -0
  84. package/payload/server/public/assets/activity-DOjvoLdd.js.gz +0 -0
  85. package/payload/server/public/assets/admin-JrSNg29g.js.br +0 -0
  86. package/payload/server/public/assets/admin-JrSNg29g.js.gz +0 -0
  87. package/payload/server/public/assets/agents--9WMJVLC.js.br +0 -0
  88. package/payload/server/public/assets/agents--9WMJVLC.js.gz +0 -0
  89. package/payload/server/public/assets/browser-DiiMmTrM.js.br +0 -0
  90. package/payload/server/public/assets/browser-DiiMmTrM.js.gz +0 -0
  91. package/payload/server/public/assets/calendar-CazmCzgO.js.br +0 -0
  92. package/payload/server/public/assets/calendar-CazmCzgO.js.gz +0 -0
  93. package/payload/server/public/assets/chat-CB0TcyXX.js.br +0 -0
  94. package/payload/server/public/assets/chat-CB0TcyXX.js.gz +0 -0
  95. package/payload/server/public/assets/graph-CVJKz1Uo.js.br +0 -0
  96. package/payload/server/public/assets/graph-CVJKz1Uo.js.gz +0 -0
  97. package/payload/server/public/assets/operator-BGvB4hu4.js.br +0 -0
  98. package/payload/server/public/assets/operator-BGvB4hu4.js.gz +0 -0
  99. package/payload/server/public/assets/page-DBQLOasI.js.br +0 -0
  100. package/payload/server/public/assets/page-DBQLOasI.js.gz +0 -0
  101. package/payload/server/public/assets/public-BvUQR3hd.js.br +0 -0
  102. package/payload/server/public/assets/public-BvUQR3hd.js.gz +0 -0
  103. package/payload/server/public/assets/routines-N8NmxPOZ.js.br +0 -0
  104. package/payload/server/public/assets/routines-N8NmxPOZ.js.gz +0 -0
  105. package/payload/server/public/assets/skills-B2CRgO-U.js.br +0 -0
  106. package/payload/server/public/assets/skills-B2CRgO-U.js.gz +0 -0
  107. package/payload/server/public/assets/tasks-8QLGtlSj.js.br +0 -0
  108. package/payload/server/public/assets/tasks-8QLGtlSj.js.gz +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-realagent-code",
3
- "version": "0.1.584",
3
+ "version": "0.1.586",
4
4
  "description": "Install Real Agent — Built for agents. By agents.",
5
5
  "bin": {
6
6
  "create-realagent-code": "./dist/index.js"
@@ -5,6 +5,7 @@
5
5
  "installDir": "realagent-code",
6
6
  "configDir": ".realagent-code",
7
7
  "tagline": "Built for agents. By agents.",
8
+ "productMandate": "You are the back office of an estate agency. Your work is the administration behind properties, listings, viewings, offers and applicants: records kept straight, correspondence answered, progress chased, deadlines met.",
8
9
  "marketingUrl": "https://realagent.network",
9
10
  "neo4jPort": 7688,
10
11
  "ttydPort": 7682,
@@ -1,18 +1,24 @@
1
1
  /**
2
- * Brand templating — single source of truth for the operator-visible brand name.
2
+ * Brand templating — single source of truth for the operator-visible brand
3
+ * name and the admin agent's stated purpose.
3
4
  *
4
5
  * Skills, agent templates, plugin manifests, and reference content reference
5
- * the brand only via the literal `{{productName}}` placeholder. This module
6
- * substitutes at read time from `brand.json`. Same shadow-source defect class
7
- * as Tasks 787/788: there is no fallback; missing or empty `productName`
8
- * hard-fails so the wrong brand can never reach the system prompt or the
9
- * operator UI silently.
6
+ * the brand only via literal `{{field}}` placeholders, one per entry in
7
+ * `BRAND_FIELDS`. This module substitutes at read time from `brand.json`. Same
8
+ * shadow-source defect class as Tasks 787/788: there is no fallback; a missing
9
+ * or empty value hard-fails so the wrong brand can never reach the system
10
+ * prompt or the operator UI silently.
10
11
  *
11
12
  * Used by both `platform/ui` (admin/public agent system-prompt assembly) and
12
13
  * `platform/plugins/admin/mcp` (the `plugin-read` MCP tool). Both call sites
13
14
  * pass the absolute file path as `sourcePath` so the diagnostic line names
14
15
  * exactly which file was substituted.
15
16
  */
16
- export declare function getBrandProductName(): string;
17
+ /** Every brand.json field a template may reference as `{{field}}`.
18
+ * Task 2646 added `productMandate`, the admin agent's stated purpose. */
19
+ declare const BRAND_FIELDS: readonly ["productName", "productMandate"];
20
+ export type BrandField = (typeof BRAND_FIELDS)[number];
21
+ export declare function getBrandValue(field: BrandField): string;
17
22
  export declare function substituteBrandPlaceholders(content: string, sourcePath: string): string;
23
+ export {};
18
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAmBH,wBAAgB,mBAAmB,IAAI,MAAM,CAsB5C;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAkBvF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH;0EAC0E;AAC1E,QAAA,MAAM,YAAY,4CAA6C,CAAC;AAChE,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAiBvD,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAwBvD;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAyBvF"}
@@ -1,13 +1,14 @@
1
1
  "use strict";
2
2
  /**
3
- * Brand templating — single source of truth for the operator-visible brand name.
3
+ * Brand templating — single source of truth for the operator-visible brand
4
+ * name and the admin agent's stated purpose.
4
5
  *
5
6
  * Skills, agent templates, plugin manifests, and reference content reference
6
- * the brand only via the literal `{{productName}}` placeholder. This module
7
- * substitutes at read time from `brand.json`. Same shadow-source defect class
8
- * as Tasks 787/788: there is no fallback; missing or empty `productName`
9
- * hard-fails so the wrong brand can never reach the system prompt or the
10
- * operator UI silently.
7
+ * the brand only via literal `{{field}}` placeholders, one per entry in
8
+ * `BRAND_FIELDS`. This module substitutes at read time from `brand.json`. Same
9
+ * shadow-source defect class as Tasks 787/788: there is no fallback; a missing
10
+ * or empty value hard-fails so the wrong brand can never reach the system
11
+ * prompt or the operator UI silently.
11
12
  *
12
13
  * Used by both `platform/ui` (admin/public agent system-prompt assembly) and
13
14
  * `platform/plugins/admin/mcp` (the `plugin-read` MCP tool). Both call sites
@@ -15,12 +16,17 @@
15
16
  * exactly which file was substituted.
16
17
  */
17
18
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.getBrandProductName = getBrandProductName;
19
+ exports.getBrandValue = getBrandValue;
19
20
  exports.substituteBrandPlaceholders = substituteBrandPlaceholders;
20
21
  const node_path_1 = require("node:path");
21
22
  const node_fs_1 = require("node:fs");
22
- const PLACEHOLDER = "{{productName}}";
23
- let cachedProductName = null;
23
+ /** Every brand.json field a template may reference as `{{field}}`.
24
+ * Task 2646 added `productMandate`, the admin agent's stated purpose. */
25
+ const BRAND_FIELDS = ["productName", "productMandate"];
26
+ /** Keyed by manifest path and field, not by field alone: a process that
27
+ * changes MAXY_PLATFORM_ROOT (only tests do) must not read a previous root's
28
+ * value back out of the cache. */
29
+ const cache = new Map();
24
30
  function brandJsonPath() {
25
31
  const platformRoot = process.env.MAXY_PLATFORM_ROOT;
26
32
  if (!platformRoot) {
@@ -28,10 +34,12 @@ function brandJsonPath() {
28
34
  }
29
35
  return (0, node_path_1.join)(platformRoot, "config", "brand.json");
30
36
  }
31
- function getBrandProductName() {
32
- if (cachedProductName !== null)
33
- return cachedProductName;
37
+ function getBrandValue(field) {
34
38
  const path = brandJsonPath();
39
+ const key = `${path}::${field}`;
40
+ const cached = cache.get(key);
41
+ if (cached !== undefined)
42
+ return cached;
35
43
  if (!(0, node_fs_1.existsSync)(path)) {
36
44
  throw new Error(`[skill-loader] brand.json missing at ${path}`);
37
45
  }
@@ -42,28 +50,35 @@ function getBrandProductName() {
42
50
  catch (err) {
43
51
  throw new Error(`[skill-loader] brand.json unreadable at ${path}: ${err instanceof Error ? err.message : String(err)}`);
44
52
  }
45
- const value = parsed.productName;
53
+ const value = parsed[field];
46
54
  if (typeof value !== "string" || value.trim() === "") {
47
- throw new Error(`[skill-loader] brand.json at ${path} has missing or empty productName`);
55
+ throw new Error(`[skill-loader] brand.json at ${path} has missing or empty ${field}`);
48
56
  }
49
- cachedProductName = value;
57
+ cache.set(key, value);
50
58
  return value;
51
59
  }
52
60
  function substituteBrandPlaceholders(content, sourcePath) {
53
- if (!content.includes(PLACEHOLDER))
54
- return content;
55
- let productName;
56
- try {
57
- productName = getBrandProductName();
58
- }
59
- catch (err) {
60
- console.error(`[skill-loader] ERROR: brand.json missing — cannot resolve productName for skill ${sourcePath}`);
61
- throw err;
61
+ let out = content;
62
+ for (const field of BRAND_FIELDS) {
63
+ const placeholder = `{{${field}}}`;
64
+ // Per-placeholder, not per-call: a file using only one placeholder must
65
+ // never read the other field, so a brand.json predating a field keeps
66
+ // rendering every file that does not use it.
67
+ if (!out.includes(placeholder))
68
+ continue;
69
+ let value;
70
+ try {
71
+ value = getBrandValue(field);
72
+ }
73
+ catch (err) {
74
+ console.error(`[skill-loader] ERROR: brand.json missing — cannot resolve ${field} for skill ${sourcePath}`);
75
+ throw err;
76
+ }
77
+ // split-length counts non-overlapping literal matches without regex-escape risk.
78
+ const occurrences = out.split(placeholder).length - 1;
79
+ out = out.split(placeholder).join(value);
80
+ console.log(`[skill-loader] brand-substituted ${field}=${value} skill=${sourcePath} occurrences=${occurrences}`);
62
81
  }
63
- // split-length counts non-overlapping literal matches without regex-escape risk.
64
- const occurrences = content.split(PLACEHOLDER).length - 1;
65
- const substituted = content.split(PLACEHOLDER).join(productName);
66
- console.log(`[skill-loader] brand-substituted productName=${productName} skill=${sourcePath} occurrences=${occurrences}`);
67
- return substituted;
82
+ return out;
68
83
  }
69
84
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;AAmBH,kDAsBC;AAED,kEAkBC;AA3DD,yCAAiC;AACjC,qCAAmD;AAEnD,MAAM,WAAW,GAAG,iBAAiB,CAAC;AAEtC,IAAI,iBAAiB,GAAkB,IAAI,CAAC;AAE5C,SAAS,aAAa;IACpB,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IACpD,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,uEAAuE,CACxE,CAAC;IACJ,CAAC;IACD,OAAO,IAAA,gBAAI,EAAC,YAAY,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;AACpD,CAAC;AAED,SAAgB,mBAAmB;IACjC,IAAI,iBAAiB,KAAK,IAAI;QAAE,OAAO,iBAAiB,CAAC;IACzD,MAAM,IAAI,GAAG,aAAa,EAAE,CAAC;IAC7B,IAAI,CAAC,IAAA,oBAAU,EAAC,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,wCAAwC,IAAI,EAAE,CAAC,CAAC;IAClE,CAAC;IACD,IAAI,MAAiC,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAA,sBAAY,EAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,2CAA2C,IAAI,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACvG,CAAC;IACJ,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,WAAW,CAAC;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,gCAAgC,IAAI,mCAAmC,CACxE,CAAC;IACJ,CAAC;IACD,iBAAiB,GAAG,KAAK,CAAC;IAC1B,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAgB,2BAA2B,CAAC,OAAe,EAAE,UAAkB;IAC7E,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,OAAO,CAAC;IACnD,IAAI,WAAmB,CAAC;IACxB,IAAI,CAAC;QACH,WAAW,GAAG,mBAAmB,EAAE,CAAC;IACtC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CACX,mFAAmF,UAAU,EAAE,CAChG,CAAC;QACF,MAAM,GAAG,CAAC;IACZ,CAAC;IACD,iFAAiF;IACjF,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IAC1D,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,CACT,gDAAgD,WAAW,UAAU,UAAU,gBAAgB,WAAW,EAAE,CAC7G,CAAC;IACF,OAAO,WAAW,CAAC;AACrB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,103 @@
1
+ // Task 2646 — the admin identity carries a per-brand purpose, so brand.json now
2
+ // supplies two fields rather than one. These cases pin the property that makes
3
+ // that safe: each placeholder resolves independently, so a manifest predating a
4
+ // field keeps rendering every file that does not use it.
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import { getBrandValue, substituteBrandPlaceholders } from "../index";
10
+
11
+ const roots: string[] = [];
12
+ const previousRoot = process.env.MAXY_PLATFORM_ROOT;
13
+
14
+ /** Seed a fresh platform root holding one brand.json and point the env at it.
15
+ * Fresh per test, so the path-keyed cache never carries a value across. */
16
+ function seedBrand(brand: Record<string, unknown>): string {
17
+ const root = mkdtempSync(join(tmpdir(), "brand-templating-"));
18
+ roots.push(root);
19
+ mkdirSync(join(root, "config"), { recursive: true });
20
+ writeFileSync(join(root, "config", "brand.json"), JSON.stringify(brand));
21
+ process.env.MAXY_PLATFORM_ROOT = root;
22
+ return root;
23
+ }
24
+
25
+ afterEach(() => {
26
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
27
+ if (previousRoot === undefined) delete process.env.MAXY_PLATFORM_ROOT;
28
+ else process.env.MAXY_PLATFORM_ROOT = previousRoot;
29
+ vi.restoreAllMocks();
30
+ });
31
+
32
+ describe("substituteBrandPlaceholders", () => {
33
+ it("substitutes both placeholders in one pass", () => {
34
+ seedBrand({ productName: "Real Agent", productMandate: "You are the back office." });
35
+ const out = substituteBrandPlaceholders(
36
+ "{{productName}} — {{productMandate}}",
37
+ "/tmp/IDENTITY.md",
38
+ );
39
+ expect(out).toBe("Real Agent — You are the back office.");
40
+ });
41
+
42
+ it("never reads productMandate for content that only uses productName", () => {
43
+ // The regression this guards: a brand.json predating the new field must keep
44
+ // rendering every existing productName-only file.
45
+ seedBrand({ productName: "Maxy" });
46
+ expect(substituteBrandPlaceholders("Hello from {{productName}}", "/tmp/x.md")).toBe(
47
+ "Hello from Maxy",
48
+ );
49
+ });
50
+
51
+ it("returns content unchanged and logs nothing when no placeholder is present", () => {
52
+ seedBrand({ productName: "Maxy", productMandate: "m" });
53
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
54
+ const content = "no placeholders here";
55
+ expect(substituteBrandPlaceholders(content, "/tmp/x.md")).toBe(content);
56
+ expect(log).not.toHaveBeenCalled();
57
+ });
58
+
59
+ it("replaces every occurrence of the mandate placeholder", () => {
60
+ seedBrand({ productName: "Maxy", productMandate: "M" });
61
+ expect(substituteBrandPlaceholders("{{productMandate}}/{{productMandate}}", "/tmp/x.md")).toBe(
62
+ "M/M",
63
+ );
64
+ });
65
+
66
+ it("logs the substitution with field, value and occurrence count", () => {
67
+ seedBrand({ productName: "Maxy", productMandate: "M" });
68
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
69
+ substituteBrandPlaceholders("{{productMandate}} {{productMandate}}", "/tmp/IDENTITY.md");
70
+ expect(log).toHaveBeenCalledWith(
71
+ "[skill-loader] brand-substituted productMandate=M skill=/tmp/IDENTITY.md occurrences=2",
72
+ );
73
+ });
74
+
75
+ it("throws when the mandate placeholder is used and the field is missing", () => {
76
+ seedBrand({ productName: "Maxy" });
77
+ expect(() => substituteBrandPlaceholders("{{productMandate}}", "/tmp/x.md")).toThrow(
78
+ /missing or empty productMandate/,
79
+ );
80
+ });
81
+
82
+ it("throws when the mandate field is not a string", () => {
83
+ seedBrand({ productName: "Maxy", productMandate: 42 });
84
+ expect(() => substituteBrandPlaceholders("{{productMandate}}", "/tmp/x.md")).toThrow(
85
+ /missing or empty productMandate/,
86
+ );
87
+ });
88
+
89
+ it("throws when the mandate field is whitespace only", () => {
90
+ seedBrand({ productName: "Maxy", productMandate: " " });
91
+ expect(() => substituteBrandPlaceholders("{{productMandate}}", "/tmp/x.md")).toThrow(
92
+ /missing or empty productMandate/,
93
+ );
94
+ });
95
+ });
96
+
97
+ describe("getBrandValue", () => {
98
+ it("reads each field independently from the same manifest", () => {
99
+ seedBrand({ productName: "SiteDesk", productMandate: "jobs" });
100
+ expect(getBrandValue("productName")).toBe("SiteDesk");
101
+ expect(getBrandValue("productMandate")).toBe("jobs");
102
+ });
103
+ });
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Brand templating — single source of truth for the operator-visible brand name.
2
+ * Brand templating — single source of truth for the operator-visible brand
3
+ * name and the admin agent's stated purpose.
3
4
  *
4
5
  * Skills, agent templates, plugin manifests, and reference content reference
5
- * the brand only via the literal `{{productName}}` placeholder. This module
6
- * substitutes at read time from `brand.json`. Same shadow-source defect class
7
- * as Tasks 787/788: there is no fallback; missing or empty `productName`
8
- * hard-fails so the wrong brand can never reach the system prompt or the
9
- * operator UI silently.
6
+ * the brand only via literal `{{field}}` placeholders, one per entry in
7
+ * `BRAND_FIELDS`. This module substitutes at read time from `brand.json`. Same
8
+ * shadow-source defect class as Tasks 787/788: there is no fallback; a missing
9
+ * or empty value hard-fails so the wrong brand can never reach the system
10
+ * prompt or the operator UI silently.
10
11
  *
11
12
  * Used by both `platform/ui` (admin/public agent system-prompt assembly) and
12
13
  * `platform/plugins/admin/mcp` (the `plugin-read` MCP tool). Both call sites
@@ -17,9 +18,15 @@
17
18
  import { join } from "node:path";
18
19
  import { existsSync, readFileSync } from "node:fs";
19
20
 
20
- const PLACEHOLDER = "{{productName}}";
21
+ /** Every brand.json field a template may reference as `{{field}}`.
22
+ * Task 2646 added `productMandate`, the admin agent's stated purpose. */
23
+ const BRAND_FIELDS = ["productName", "productMandate"] as const;
24
+ export type BrandField = (typeof BRAND_FIELDS)[number];
21
25
 
22
- let cachedProductName: string | null = null;
26
+ /** Keyed by manifest path and field, not by field alone: a process that
27
+ * changes MAXY_PLATFORM_ROOT (only tests do) must not read a previous root's
28
+ * value back out of the cache. */
29
+ const cache = new Map<string, string>();
23
30
 
24
31
  function brandJsonPath(): string {
25
32
  const platformRoot = process.env.MAXY_PLATFORM_ROOT;
@@ -31,13 +38,15 @@ function brandJsonPath(): string {
31
38
  return join(platformRoot, "config", "brand.json");
32
39
  }
33
40
 
34
- export function getBrandProductName(): string {
35
- if (cachedProductName !== null) return cachedProductName;
41
+ export function getBrandValue(field: BrandField): string {
36
42
  const path = brandJsonPath();
43
+ const key = `${path}::${field}`;
44
+ const cached = cache.get(key);
45
+ if (cached !== undefined) return cached;
37
46
  if (!existsSync(path)) {
38
47
  throw new Error(`[skill-loader] brand.json missing at ${path}`);
39
48
  }
40
- let parsed: { productName?: unknown };
49
+ let parsed: Record<string, unknown>;
41
50
  try {
42
51
  parsed = JSON.parse(readFileSync(path, "utf-8"));
43
52
  } catch (err) {
@@ -45,32 +54,39 @@ export function getBrandProductName(): string {
45
54
  `[skill-loader] brand.json unreadable at ${path}: ${err instanceof Error ? err.message : String(err)}`,
46
55
  );
47
56
  }
48
- const value = parsed.productName;
57
+ const value = parsed[field];
49
58
  if (typeof value !== "string" || value.trim() === "") {
50
59
  throw new Error(
51
- `[skill-loader] brand.json at ${path} has missing or empty productName`,
60
+ `[skill-loader] brand.json at ${path} has missing or empty ${field}`,
52
61
  );
53
62
  }
54
- cachedProductName = value;
63
+ cache.set(key, value);
55
64
  return value;
56
65
  }
57
66
 
58
67
  export function substituteBrandPlaceholders(content: string, sourcePath: string): string {
59
- if (!content.includes(PLACEHOLDER)) return content;
60
- let productName: string;
61
- try {
62
- productName = getBrandProductName();
63
- } catch (err) {
64
- console.error(
65
- `[skill-loader] ERROR: brand.json missing — cannot resolve productName for skill ${sourcePath}`,
68
+ let out = content;
69
+ for (const field of BRAND_FIELDS) {
70
+ const placeholder = `{{${field}}}`;
71
+ // Per-placeholder, not per-call: a file using only one placeholder must
72
+ // never read the other field, so a brand.json predating a field keeps
73
+ // rendering every file that does not use it.
74
+ if (!out.includes(placeholder)) continue;
75
+ let value: string;
76
+ try {
77
+ value = getBrandValue(field);
78
+ } catch (err) {
79
+ console.error(
80
+ `[skill-loader] ERROR: brand.json missing — cannot resolve ${field} for skill ${sourcePath}`,
81
+ );
82
+ throw err;
83
+ }
84
+ // split-length counts non-overlapping literal matches without regex-escape risk.
85
+ const occurrences = out.split(placeholder).length - 1;
86
+ out = out.split(placeholder).join(value);
87
+ console.log(
88
+ `[skill-loader] brand-substituted ${field}=${value} skill=${sourcePath} occurrences=${occurrences}`,
66
89
  );
67
- throw err;
68
90
  }
69
- // split-length counts non-overlapping literal matches without regex-escape risk.
70
- const occurrences = content.split(PLACEHOLDER).length - 1;
71
- const substituted = content.split(PLACEHOLDER).join(productName);
72
- console.log(
73
- `[skill-loader] brand-substituted productName=${productName} skill=${sourcePath} occurrences=${occurrences}`,
74
- );
75
- return substituted;
91
+ return out;
76
92
  }
@@ -4,5 +4,6 @@
4
4
  "outDir": "dist",
5
5
  "rootDir": "src"
6
6
  },
7
- "include": ["src"]
7
+ "include": ["src"],
8
+ "exclude": ["src/__tests__"]
8
9
  }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "node",
6
+ globals: false,
7
+ include: ["src/__tests__/**/*.test.ts"],
8
+ },
9
+ });
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what Real Agent 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:7e2b1c49501690b9b6a3c556cde20a84f087d1d74bdafb3f2f00faf5e87c4b2f
4
+ content-hash: sha256:066a1d70ab0a529e335c2ab02d19663d3fbe51032e58073c267f05f8c8a0de97
5
5
  brand: realagent-code
6
6
  product-name: Real Agent
7
7
  ---
@@ -146,6 +146,8 @@ Real Agent runs two agents simultaneously:
146
146
 
147
147
  **Admin agent (you)** — full access to all tools and plugins. This is the agent you interact with at your local or remote URL. It can read and write contacts, send Telegram messages, manage your account, and perform any task you have plugins for. Protected by your PIN. Your admin agent runs through your own Claude Code OAuth session — it never bills the Anthropic API. Authentication and SDK details are documented in the developer doc `.docs/platform.md` admin-agent section.
148
148
 
149
+ Your admin agent has a stated purpose, set by your edition of the product. When you ask for something outside it, the agent says so first, names the nearest thing it does handle, and puts the request back to you. Ask again and it does the work; the decision is yours.
150
+
149
151
  **Public agent (visitors)** — read-only access. Handles enquiries from people who reach your public URL. It can answer questions about your business and collect prospect contact details, but it cannot access your private data or take actions.
150
152
 
151
153
  ## Plugins
@@ -9,6 +9,18 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-08-10 (0.1.586)
13
+
14
+ - The remote browser no longer freezes and stay frozen. Its connection to the device browser drops for ordinary reasons several times a day, and nothing noticed: the picture stopped updating while your clicks and typing carried on going nowhere, so it looked like the browser had hung when the browser was fine. It now spots the dropped connection and reconnects on its own, usually within a few seconds.
15
+ - The browser pane has a reload button, next to the back and forward arrows. There was no way to refresh before, so a frozen view meant reloading the whole dashboard.
16
+ - If the picture ever does stop while you are working, the assistant can now see that from the logs. Before, a frozen pane and a healthy one looked identical in every record.
17
+
18
+ ## 2026-08-09 (0.1.585)
19
+
20
+ - Your assistant now knows what it is for, and says so when you ask it to work outside that. Every edition used to boot the same anonymous "Business Assistant" with no stated purpose, so it accepted anything without comment. Ask it for something outside its remit and it names its purpose, offers the nearest thing it does handle, and puts the request back to you; ask again and it does the work.
21
+ - The estate agency and building contractor editions each describe the work they actually do, while Real Agent stays a general personal assistant and second brain, tied to no trade or industry.
22
+ - The assistant no longer assumes you run a business. Five places in its own description took that for granted, including the name it introduced itself by.
23
+
12
24
  ## 2026-08-09 (0.1.584)
13
25
 
14
26
  - The assistant can find the Telegram chats it is able to reach. It was looking for them in a different folder from the one they are saved in, so it reported having no way to message people it could in fact message.
@@ -19,6 +19,8 @@ The Pi runs the web interface, the AI agent, and all the plugin servers. When yo
19
19
 
20
20
  **Admin agent (you)** — full access to all tools and plugins. This is the agent you interact with at your local or remote URL. It can read and write contacts, send Telegram messages, manage your account, and perform any task you have plugins for. Protected by your PIN. Your admin agent runs through your own Claude Code OAuth session — it never bills the Anthropic API. Authentication and SDK details are documented in the developer doc `.docs/platform.md` admin-agent section.
21
21
 
22
+ Your admin agent has a stated purpose, set by your edition of the product. When you ask for something outside it, the agent says so first, names the nearest thing it does handle, and puts the request back to you. Ask again and it does the work; the decision is yours.
23
+
22
24
  **Public agent (visitors)** — read-only access. Handles enquiries from people who reach your public URL. It can answer questions about your business and collect prospect contact details, but it cannot access your private data or take actions.
23
25
 
24
26
  ## Plugins
@@ -1,6 +1,6 @@
1
- # Business Assistant
1
+ # {{productName}}
2
2
 
3
- Your job is to do useful work for the operator. How useful you can be depends on what you know about them and their business; both grow together, and every interaction is a chance to learn more.
3
+ Your job is to do useful work for the operator. How useful you can be depends on what you know about them and their work; both grow together, and every interaction is a chance to learn more.
4
4
 
5
5
  ## Prime directives
6
6
 
@@ -8,9 +8,15 @@ Your job is to do useful work for the operator. How useful you can be depends on
8
8
  2. Be concise. Three sentences or fewer. If you cannot answer in three, ask in five words.
9
9
  3. Show your evidence. Gather it before forming a hypothesis. Never speculate.
10
10
 
11
+ ## Your purpose
12
+
13
+ {{productMandate}}
14
+
15
+ When a request falls outside that purpose, say so before you start it. Name what you are for, name the closest thing you do own, and put the request back to the operator. If they ask again, the decision is theirs and you do it. That is one exchange, not a refusal and not a negotiation.
16
+
11
17
  ## Before you speak
12
18
 
13
- If more than one admin works on this account, identify which one you are speaking with this session, purely as a convenience so you load the right person's profile and history. This is never a gate and never blocks any tool; with a single admin, skip it. Then, before your first message, do reconnaissance: read what is known about the operator and their business, today's calendar, active projects, open work, recent conversations. Infer the domain from the plugins and skills available. Then lead with what you found, and follow with a proposal suited to where the operator is in their day. Ask only the questions you could not answer through investigation. You are expected to drive the day, not solicit instructions for it.
19
+ If more than one admin works on this account, identify which one you are speaking with this session, purely as a convenience so you load the right person's profile and history. This is never a gate and never blocks any tool; with a single admin, skip it. Then, before your first message, do reconnaissance: read what is known about the operator and their work, today's calendar, active projects, open work, recent conversations. Infer the domain from the plugins and skills available. Then lead with what you found, and follow with a proposal suited to where the operator is in their day. Ask only the questions you could not answer through investigation. You are expected to drive the day, not solicit instructions for it.
14
20
 
15
21
  When a turn needs you to ground a claim about what the product is, how the platform is built, how plugins or skills or specialists work, how install or deploy runs, or any other product-architecture fact, load the platform-architecture skill and answer from its body. Cite the source URL printed above the block you drew from. The platform-architecture skill is the install catalogue, not evidence of what is installed on this account. For questions about what is enabled here — which plugins are on, which specialists you can dispatch, which models are in use — call capabilities-here and answer from its JSON; never infer install state from the catalogue body.
16
22
 
@@ -20,13 +26,13 @@ When a request needs a skill and you do not already know its exact slug, resolve
20
26
 
21
27
  You may not act on a request until you know what is being asked, what is in scope and what is out, and what rules apply. When the operator's words are precise, all three are obvious and you act. When any is imprecise, stop and ask. Insist on the operator being precise and concise.
22
28
 
23
- How you route the work is decided per turn by the routing ladder injected with each prompt: delegate to the specialist that owns the deliverable, else load an admin-usable skill, else, when the deliverable is one the business issues repeatedly and no specialist or skill owns it, author its owning skill once via skill-builder (or a worker agent via agent-builder) and produce it through that, else author it directly. Authoring directly is the last resort for a genuine one-off, never for a repeatable document or process. That ladder, not this file, carries the live roster, the admin-usable list, and the outcome-only brief contract.
29
+ How you route the work is decided per turn by the routing ladder injected with each prompt: delegate to the specialist that owns the deliverable, else load an admin-usable skill, else, when the deliverable is one that recurs and no specialist or skill owns it, author its owning skill once via skill-builder (or a worker agent via agent-builder) and produce it through that, else author it directly. Authoring directly is the last resort for a genuine one-off, never for a repeatable document or process. That ladder, not this file, carries the live roster, the admin-usable list, and the outcome-only brief contract.
24
30
 
25
31
  ## Working from the graph
26
32
 
27
33
  The graph is your brain. The first read of any turn that needs to know something is against the graph; outside lookups happen only after the graph has confirmed it does not hold the answer, and anything new an outside lookup produces is written back the same turn. Greetings and pure interaction turns do not need this loop. When something worth remembering comes out of a conversation, hand the write off to the specialist who owns graph writes; direct writes from this seat are reserved for one-line updates against a node already in context whose classification is unambiguous.
28
34
 
29
- Every factual claim you make in any turn, about the operator, their business, the product, the platform, the day, or the world, has a source you can name. The permissible sources are four: a graph read this session, a tool output returned this session, content the operator pasted into this conversation, and the body of the platform-architecture skill when it is loaded. Pretraining recall is not a source. If a claim does not trace to one of the four, you do not have it yet; read the graph, run the tool, load the skill, or ask the operator. This rule binds every factual sentence you emit, not only the ones about the graph.
35
+ Every factual claim you make in any turn, about the operator, their work, the product, the platform, the day, or the world, has a source you can name. The permissible sources are four: a graph read this session, a tool output returned this session, content the operator pasted into this conversation, and the body of the platform-architecture skill when it is loaded. Pretraining recall is not a source. If a claim does not trace to one of the four, you do not have it yet; read the graph, run the tool, load the skill, or ask the operator. This rule binds every factual sentence you emit, not only the ones about the graph.
30
36
 
31
37
  When the operator asks for a previous workflow output by name, fetch the latest matching report directly rather than searching. Searching is for questions about the contents of reports, not for the reports themselves. When the operator asks how your understanding of a person, organisation, or concept has changed over time, walk the revision history of that entity and surface the deltas yourself.
32
38
 
@@ -8,7 +8,7 @@ import {
8
8
  getUserTimezone,
9
9
  isValidAccountId,
10
10
  registerLoop
11
- } from "./chunk-SKYZG6OT.js";
11
+ } from "./chunk-6AQYMT6C.js";
12
12
  import {
13
13
  __commonJS,
14
14
  __require,
@@ -115,8 +115,8 @@ import { readFileSync as readFileSync3, readdirSync, existsSync as existsSync3,
115
115
  // ../lib/brand-templating/src/index.ts
116
116
  import { join as join2 } from "path";
117
117
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
118
- var PLACEHOLDER = "{{productName}}";
119
- var cachedProductName = null;
118
+ var BRAND_FIELDS = ["productName", "productMandate"];
119
+ var cache = /* @__PURE__ */ new Map();
120
120
  function brandJsonPath() {
121
121
  const platformRoot2 = process.env.MAXY_PLATFORM_ROOT;
122
122
  if (!platformRoot2) {
@@ -126,9 +126,11 @@ function brandJsonPath() {
126
126
  }
127
127
  return join2(platformRoot2, "config", "brand.json");
128
128
  }
129
- function getBrandProductName() {
130
- if (cachedProductName !== null) return cachedProductName;
129
+ function getBrandValue(field) {
131
130
  const path = brandJsonPath();
131
+ const key = `${path}::${field}`;
132
+ const cached = cache.get(key);
133
+ if (cached !== void 0) return cached;
132
134
  if (!existsSync2(path)) {
133
135
  throw new Error(`[skill-loader] brand.json missing at ${path}`);
134
136
  }
@@ -140,32 +142,36 @@ function getBrandProductName() {
140
142
  `[skill-loader] brand.json unreadable at ${path}: ${err instanceof Error ? err.message : String(err)}`
141
143
  );
142
144
  }
143
- const value = parsed.productName;
145
+ const value = parsed[field];
144
146
  if (typeof value !== "string" || value.trim() === "") {
145
147
  throw new Error(
146
- `[skill-loader] brand.json at ${path} has missing or empty productName`
148
+ `[skill-loader] brand.json at ${path} has missing or empty ${field}`
147
149
  );
148
150
  }
149
- cachedProductName = value;
151
+ cache.set(key, value);
150
152
  return value;
151
153
  }
152
154
  function substituteBrandPlaceholders(content, sourcePath) {
153
- if (!content.includes(PLACEHOLDER)) return content;
154
- let productName;
155
- try {
156
- productName = getBrandProductName();
157
- } catch (err) {
158
- console.error(
159
- `[skill-loader] ERROR: brand.json missing \u2014 cannot resolve productName for skill ${sourcePath}`
155
+ let out = content;
156
+ for (const field of BRAND_FIELDS) {
157
+ const placeholder = `{{${field}}}`;
158
+ if (!out.includes(placeholder)) continue;
159
+ let value;
160
+ try {
161
+ value = getBrandValue(field);
162
+ } catch (err) {
163
+ console.error(
164
+ `[skill-loader] ERROR: brand.json missing \u2014 cannot resolve ${field} for skill ${sourcePath}`
165
+ );
166
+ throw err;
167
+ }
168
+ const occurrences = out.split(placeholder).length - 1;
169
+ out = out.split(placeholder).join(value);
170
+ console.log(
171
+ `[skill-loader] brand-substituted ${field}=${value} skill=${sourcePath} occurrences=${occurrences}`
160
172
  );
161
- throw err;
162
173
  }
163
- const occurrences = content.split(PLACEHOLDER).length - 1;
164
- const substituted = content.split(PLACEHOLDER).join(productName);
165
- console.log(
166
- `[skill-loader] brand-substituted productName=${productName} skill=${sourcePath} occurrences=${occurrences}`
167
- );
168
- return substituted;
174
+ return out;
169
175
  }
170
176
 
171
177
  // app/lib/claude-agent/account.ts