@rubytech/create-maxy-code 0.1.62 → 0.1.63

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 (146) hide show
  1. package/dist/__tests__/installer-specialist-registration.test.js +112 -0
  2. package/dist/index.js +22 -0
  3. package/dist/specialist-registration.js +78 -0
  4. package/package.json +1 -1
  5. package/payload/platform/plugins/admin/PLUGIN.md +22 -0
  6. package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +50 -13
  7. package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +22 -21
  8. package/payload/platform/plugins/contacts/PLUGIN.md +9 -0
  9. package/payload/platform/plugins/docs/references/admin-session.md +55 -7
  10. package/payload/platform/plugins/email/PLUGIN.md +9 -0
  11. package/payload/platform/plugins/epc/PLUGIN.md +5 -0
  12. package/payload/platform/plugins/loop/PLUGIN.md +27 -0
  13. package/payload/platform/plugins/memory/PLUGIN.md +37 -0
  14. package/payload/platform/plugins/memory/mcp/dist/index.js +3 -3
  15. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  16. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/live-schema-source.test.js +137 -75
  17. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/live-schema-source.test.js.map +1 -1
  18. package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.d.ts +0 -17
  19. package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.d.ts.map +1 -1
  20. package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.js +32 -4
  21. package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.js.map +1 -1
  22. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts +65 -0
  23. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts.map +1 -0
  24. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +182 -0
  25. package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -0
  26. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js +8 -8
  27. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js.map +1 -1
  28. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/log-ingest.test.d.ts +2 -0
  29. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/log-ingest.test.d.ts.map +1 -0
  30. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/log-ingest.test.js +241 -0
  31. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/log-ingest.test.js.map +1 -0
  32. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts.map +1 -1
  33. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +9 -0
  34. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
  35. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-enrich-rejection.d.ts +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-enrich-rejection.d.ts.map +1 -1
  37. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-enrich-rejection.js +9 -1
  38. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-enrich-rejection.js.map +1 -1
  39. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.d.ts.map +1 -1
  40. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.js +10 -2
  41. package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.js.map +1 -1
  42. package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-add.d.ts +1 -1
  43. package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-add.d.ts.map +1 -1
  44. package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-add.js +5 -1
  45. package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-add.js.map +1 -1
  46. package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-remove.d.ts +1 -1
  47. package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-remove.d.ts.map +1 -1
  48. package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-remove.js +5 -1
  49. package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-remove.js.map +1 -1
  50. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
  51. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +4 -0
  52. package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
  53. package/payload/platform/plugins/memory/mcp/dist/tools/memory-classify.d.ts.map +1 -1
  54. package/payload/platform/plugins/memory/mcp/dist/tools/memory-classify.js +4 -0
  55. package/payload/platform/plugins/memory/mcp/dist/tools/memory-classify.js.map +1 -1
  56. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts.map +1 -1
  57. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +4 -0
  58. package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
  59. package/payload/platform/plugins/memory/mcp/dist/tools/memory-edit-attachment.d.ts.map +1 -1
  60. package/payload/platform/plugins/memory/mcp/dist/tools/memory-edit-attachment.js +4 -0
  61. package/payload/platform/plugins/memory/mcp/dist/tools/memory-edit-attachment.js.map +1 -1
  62. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
  63. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js +4 -0
  64. package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
  65. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest-extract.d.ts.map +1 -1
  66. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest-extract.js +4 -0
  67. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest-extract.js.map +1 -1
  68. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest-web.d.ts.map +1 -1
  69. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest-web.js +4 -0
  70. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest-web.js.map +1 -1
  71. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  72. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +5 -0
  73. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  74. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts.map +1 -1
  75. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js +4 -0
  76. package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js.map +1 -1
  77. package/payload/platform/plugins/memory/mcp/dist/tools/memory-rename-attachment.d.ts.map +1 -1
  78. package/payload/platform/plugins/memory/mcp/dist/tools/memory-rename-attachment.js +4 -0
  79. package/payload/platform/plugins/memory/mcp/dist/tools/memory-rename-attachment.js.map +1 -1
  80. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.d.ts.map +1 -1
  81. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js +4 -0
  82. package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js.map +1 -1
  83. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
  84. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +5 -0
  85. package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
  86. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  87. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +6 -0
  88. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  89. package/payload/platform/plugins/memory/mcp/dist/tools/profile-delete.d.ts.map +1 -1
  90. package/payload/platform/plugins/memory/mcp/dist/tools/profile-delete.js +4 -0
  91. package/payload/platform/plugins/memory/mcp/dist/tools/profile-delete.js.map +1 -1
  92. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
  93. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +4 -0
  94. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
  95. package/payload/platform/plugins/memory/mcp/vitest.config.ts +10 -3
  96. package/payload/platform/plugins/outlook/PLUGIN.md +7 -0
  97. package/payload/platform/plugins/preval/.claude-plugin/plugin.json +1 -1
  98. package/payload/platform/plugins/preval/PLUGIN.md +16 -8
  99. package/payload/platform/plugins/preval/hooks/preval-prompt-required.sh +140 -0
  100. package/payload/platform/plugins/preval/skills/property-preval/SKILL.md +44 -14
  101. package/payload/platform/plugins/preval/skills/property-preval/references/__tests__/adherence.test.py +373 -0
  102. package/payload/platform/plugins/preval/skills/property-preval/references/render.py +247 -62
  103. package/payload/platform/plugins/preval/skills/property-preval/references/template-inputs.schema.json +29 -19
  104. package/payload/platform/plugins/property-data/PLUGIN.md +22 -0
  105. package/payload/platform/plugins/replicate/PLUGIN.md +3 -0
  106. package/payload/platform/plugins/scheduling/PLUGIN.md +8 -0
  107. package/payload/platform/plugins/tasks/PLUGIN.md +14 -0
  108. package/payload/platform/plugins/telegram/PLUGIN.md +3 -0
  109. package/payload/platform/plugins/waitlist/PLUGIN.md +7 -0
  110. package/payload/platform/plugins/whatsapp/PLUGIN.md +12 -0
  111. package/payload/platform/plugins/workflows/PLUGIN.md +8 -0
  112. package/payload/platform/services/claude-session-manager/dist/index.js +39 -1
  113. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  114. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +21 -0
  115. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  116. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +148 -41
  117. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  118. package/payload/platform/services/claude-session-manager/dist/specialist-drift.d.ts +23 -0
  119. package/payload/platform/services/claude-session-manager/dist/specialist-drift.d.ts.map +1 -0
  120. package/payload/platform/services/claude-session-manager/dist/specialist-drift.js +141 -0
  121. package/payload/platform/services/claude-session-manager/dist/specialist-drift.js.map +1 -0
  122. package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts +13 -2
  123. package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts.map +1 -1
  124. package/payload/platform/services/claude-session-manager/dist/tool-surface.js +135 -37
  125. package/payload/platform/services/claude-session-manager/dist/tool-surface.js.map +1 -1
  126. package/payload/premium-plugins/real-agent/agents/compliance.md +1 -1
  127. package/payload/premium-plugins/real-agent/plugins/epc/PLUGIN.md +5 -0
  128. package/payload/premium-plugins/real-agent/plugins/loop/PLUGIN.md +27 -0
  129. package/payload/premium-plugins/real-agent/plugins/preval/.claude-plugin/plugin.json +1 -1
  130. package/payload/premium-plugins/real-agent/plugins/preval/PLUGIN.md +16 -8
  131. package/payload/premium-plugins/real-agent/plugins/preval/hooks/preval-prompt-required.sh +140 -0
  132. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/SKILL.md +44 -14
  133. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/__tests__/adherence.test.py +373 -0
  134. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/render.py +247 -62
  135. package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template-inputs.schema.json +29 -19
  136. package/payload/premium-plugins/real-agent/plugins/property-data/PLUGIN.md +22 -0
  137. package/payload/server/public/assets/{admin-DhN3G8W7.js → admin-ymReS1mN.js} +1 -1
  138. package/payload/server/public/assets/{data-B2ZVXOcE.js → data-D6U2e85B.js} +1 -1
  139. package/payload/server/public/assets/{graph-BAMGPHrK.js → graph-C274PN27.js} +1 -1
  140. package/payload/server/public/assets/{graph-labels-D9eBbvxo.js → graph-labels-DQFzd1FY.js} +1 -1
  141. package/payload/server/public/assets/{page-DjdVMWCz.js → page-D-OLjort.js} +1 -1
  142. package/payload/server/public/assets/{page-CV27Al6Z.js → page-DFFt9_Q0.js} +1 -1
  143. package/payload/server/public/data.html +3 -3
  144. package/payload/server/public/graph.html +3 -3
  145. package/payload/server/public/index.html +4 -4
  146. package/payload/server/server.js +1 -1
@@ -0,0 +1,112 @@
1
+ // Task 165 — installer must register every bundled specialist subagent
2
+ // at $CLAUDE_CONFIG_DIR/agents/<name>.md so Claude Code's `--agent <name>`
3
+ // discovery resolves. Pre-165 the installer left those names unreachable;
4
+ // every recorder spawn silently fell back to the admin agent.
5
+ import test from "node:test";
6
+ import assert from "node:assert/strict";
7
+ import { mkdtempSync, mkdirSync, writeFileSync, readlinkSync, lstatSync, rmSync, symlinkSync, readdirSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join, resolve } from "node:path";
10
+ import { registerSpecialistAgentsAt, SpecialistSymlinkCollision } from "../specialist-registration.js";
11
+ function makeTemplates(dir, names) {
12
+ mkdirSync(dir, { recursive: true });
13
+ for (const n of names)
14
+ writeFileSync(join(dir, `${n}.md`), `---\nname: ${n}\ndescription: "x"\ntools: mcp__x__y\n---\n# ${n}\n`);
15
+ }
16
+ test("registers every bundled specialist as a symlink under agentsDir", () => {
17
+ const templatesDir = mkdtempSync(join(tmpdir(), "t165-tpl-"));
18
+ const agentsDir = mkdtempSync(join(tmpdir(), "t165-agents-"));
19
+ try {
20
+ const names = ["content-producer", "database-operator", "librarian", "personal-assistant", "project-manager", "research-assistant"];
21
+ makeTemplates(templatesDir, names);
22
+ const result = registerSpecialistAgentsAt(templatesDir, agentsDir);
23
+ assert.equal(result.skipped, null);
24
+ assert.deepEqual(result.registered.sort(), [...names].sort());
25
+ for (const n of names) {
26
+ const linkPath = join(agentsDir, `${n}.md`);
27
+ const stat = lstatSync(linkPath);
28
+ assert.ok(stat.isSymbolicLink(), `${n}.md must be a symlink`);
29
+ const target = resolve(agentsDir, readlinkSync(linkPath));
30
+ assert.equal(target, resolve(templatesDir, `${n}.md`));
31
+ }
32
+ }
33
+ finally {
34
+ rmSync(templatesDir, { recursive: true, force: true });
35
+ rmSync(agentsDir, { recursive: true, force: true });
36
+ }
37
+ });
38
+ test("re-run refreshes a stale symlink pointing at the wrong target", () => {
39
+ const templatesDir = mkdtempSync(join(tmpdir(), "t165-tpl-"));
40
+ const agentsDir = mkdtempSync(join(tmpdir(), "t165-agents-"));
41
+ try {
42
+ makeTemplates(templatesDir, ["database-operator"]);
43
+ // Seed a stale symlink at the destination pointing somewhere else.
44
+ const stalePath = join(templatesDir, "_stale.md");
45
+ writeFileSync(stalePath, "stale\n");
46
+ const linkPath = join(agentsDir, "database-operator.md");
47
+ symlinkSync(stalePath, linkPath);
48
+ const beforeTarget = resolve(agentsDir, readlinkSync(linkPath));
49
+ assert.equal(beforeTarget, resolve(stalePath));
50
+ registerSpecialistAgentsAt(templatesDir, agentsDir);
51
+ const afterTarget = resolve(agentsDir, readlinkSync(linkPath));
52
+ assert.equal(afterTarget, resolve(templatesDir, "database-operator.md"));
53
+ }
54
+ finally {
55
+ rmSync(templatesDir, { recursive: true, force: true });
56
+ rmSync(agentsDir, { recursive: true, force: true });
57
+ }
58
+ });
59
+ test("idempotent — second run leaves the same symlink", () => {
60
+ const templatesDir = mkdtempSync(join(tmpdir(), "t165-tpl-"));
61
+ const agentsDir = mkdtempSync(join(tmpdir(), "t165-agents-"));
62
+ try {
63
+ makeTemplates(templatesDir, ["database-operator"]);
64
+ registerSpecialistAgentsAt(templatesDir, agentsDir);
65
+ const linkPath = join(agentsDir, "database-operator.md");
66
+ const firstStat = lstatSync(linkPath);
67
+ const firstTarget = readlinkSync(linkPath);
68
+ registerSpecialistAgentsAt(templatesDir, agentsDir);
69
+ const secondStat = lstatSync(linkPath);
70
+ const secondTarget = readlinkSync(linkPath);
71
+ assert.equal(firstTarget, secondTarget);
72
+ assert.ok(secondStat.isSymbolicLink());
73
+ // The number of entries in agentsDir hasn't grown.
74
+ assert.deepEqual(readdirSync(agentsDir).sort(), ["database-operator.md"]);
75
+ }
76
+ finally {
77
+ rmSync(templatesDir, { recursive: true, force: true });
78
+ rmSync(agentsDir, { recursive: true, force: true });
79
+ }
80
+ });
81
+ test("skips with reason=no-templates-dir when templates dir is absent", () => {
82
+ const agentsDir = mkdtempSync(join(tmpdir(), "t165-agents-"));
83
+ const missingDir = join(agentsDir, "_definitely-not-here");
84
+ try {
85
+ const r = registerSpecialistAgentsAt(missingDir, agentsDir);
86
+ assert.equal(r.skipped, "no-templates-dir");
87
+ assert.deepEqual(r.registered, []);
88
+ }
89
+ finally {
90
+ rmSync(agentsDir, { recursive: true, force: true });
91
+ }
92
+ });
93
+ test("collides loudly on a regular file at the destination (no silent overwrite)", () => {
94
+ const templatesDir = mkdtempSync(join(tmpdir(), "t165-tpl-"));
95
+ const agentsDir = mkdtempSync(join(tmpdir(), "t165-agents-"));
96
+ try {
97
+ makeTemplates(templatesDir, ["database-operator"]);
98
+ // Drop a regular file at the destination — the installer must
99
+ // never clobber it. The function under test throws
100
+ // SpecialistSymlinkCollision; index.ts maps that to process.exit(1).
101
+ const linkPath = join(agentsDir, "database-operator.md");
102
+ writeFileSync(linkPath, "operator-owned\n");
103
+ assert.throws(() => registerSpecialistAgentsAt(templatesDir, agentsDir), (err) => err instanceof SpecialistSymlinkCollision && err.linkPath === linkPath);
104
+ // The regular file was not unlinked.
105
+ const stat = lstatSync(linkPath);
106
+ assert.ok(!stat.isSymbolicLink(), "regular file must remain untouched after collision");
107
+ }
108
+ finally {
109
+ rmSync(templatesDir, { recursive: true, force: true });
110
+ rmSync(agentsDir, { recursive: true, force: true });
111
+ }
112
+ });
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync, spawn, spawnSync } from "node:child_process";
3
3
  import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, readdirSync, appendFileSync, openSync, closeSync, chmodSync, symlinkSync, unlinkSync, lstatSync, statSync, readlinkSync, realpathSync, accessSync, constants as fsConstants } from "node:fs";
4
+ import { registerSpecialistAgentsAt, SpecialistSymlinkCollision } from "./specialist-registration.js";
4
5
  import { resolve, join, dirname } from "node:path";
5
6
  import { randomBytes } from "node:crypto";
6
7
  import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerUnitFile } from "./port-resolution.js";
@@ -2308,6 +2309,14 @@ function setupAccount() {
2308
2309
  logFile(` [neo4j] passing NEO4J_URI=${neo4jUri} to seed`);
2309
2310
  shell("bash", [seedScript], { cwd: INSTALL_DIR, env: neo4jEnv });
2310
2311
  }
2312
+ // Task 165 — register every bundled specialist subagent at
2313
+ // $CLAUDE_CONFIG_DIR/agents/<name>.md. Claude Code's `--agent <name>`
2314
+ // discovery only resolves under this path; without these symlinks,
2315
+ // every `--agent database-operator` etc. silently runs as the admin
2316
+ // agent on admin IDENTITY/SOUL scaffolding. Symlinks (not copies) so
2317
+ // a `npx -y @rubytech/create-maxy-code` upgrade picks up template
2318
+ // changes without a second seed run.
2319
+ registerSpecialistAgents();
2311
2320
  // install-time defaults. Stamp `account.json` with the
2312
2321
  // brand's default plugin set + default outputStyle / thinkingView, and
2313
2322
  // write a minimal admin SOUL.md. The agent boots into a working state
@@ -2409,6 +2418,19 @@ function writeInstallDefaults(accountId) {
2409
2418
  // correct brand regardless of who owns
2410
2419
  // the shortcut symlink.
2411
2420
  // ---------------------------------------------------------------------------
2421
+ function registerSpecialistAgents() {
2422
+ try {
2423
+ registerSpecialistAgentsAt(join(INSTALL_DIR, "platform/templates/specialists/agents"), join(CLAUDE_CONFIG_DIR, "agents"), (line) => { console.log(line); logFile(line); });
2424
+ }
2425
+ catch (err) {
2426
+ if (err instanceof SpecialistSymlinkCollision) {
2427
+ console.error(`Setup failed: ${err.message}`);
2428
+ console.error(` Remove the file and re-run.`);
2429
+ process.exit(1);
2430
+ }
2431
+ throw err;
2432
+ }
2433
+ }
2412
2434
  function createTunnelSymlink(linkPath, target) {
2413
2435
  const targetAbs = resolve(target);
2414
2436
  let lstat = null;
@@ -0,0 +1,78 @@
1
+ // Task 165 — bundled-specialist registration.
2
+ //
3
+ // Claude Code resolves `--agent <name>` by reading
4
+ // `$CLAUDE_CONFIG_DIR/agents/<name>.md`. The installer ships every
5
+ // bundled specialist as a file under
6
+ // `<INSTALL_DIR>/platform/templates/specialists/agents/*.md`; this
7
+ // module symlinks each into the per-brand `$CLAUDE_CONFIG_DIR/agents/`
8
+ // so `claude --agent database-operator` resolves on first call.
9
+ //
10
+ // Idempotent. Re-running the installer after a template change
11
+ // refreshes stale symlinks. A regular file at the destination is a
12
+ // collision — never silently overwritten; the operator must remove it
13
+ // explicitly (we throw and the caller decides whether to exit).
14
+ //
15
+ // Lives outside index.ts so unit tests can import the function without
16
+ // triggering index.ts top-level installer code (which fatals on missing
17
+ // brand.json in a test environment).
18
+ import { existsSync, mkdirSync, readdirSync, lstatSync, readlinkSync, symlinkSync, unlinkSync } from "node:fs";
19
+ import { dirname, join, resolve } from "node:path";
20
+ export class SpecialistSymlinkCollision extends Error {
21
+ linkPath;
22
+ constructor(linkPath) {
23
+ super(`${linkPath} collision (regular file) — remove and re-run`);
24
+ this.linkPath = linkPath;
25
+ this.name = "SpecialistSymlinkCollision";
26
+ }
27
+ }
28
+ export function registerSpecialistAgentsAt(templatesDir, agentsDir, log = () => { }) {
29
+ if (!existsSync(templatesDir)) {
30
+ log(` [specialist-registration] SKIPPED reason=templates-dir-missing path=${templatesDir}`);
31
+ return { registered: [], skipped: 'no-templates-dir' };
32
+ }
33
+ mkdirSync(agentsDir, { recursive: true });
34
+ const entries = readdirSync(templatesDir).filter((n) => n.endsWith(".md"));
35
+ if (entries.length === 0) {
36
+ log(` [specialist-registration] SKIPPED reason=no-bundled-specialists path=${templatesDir}`);
37
+ return { registered: [], skipped: 'no-bundled-specialists' };
38
+ }
39
+ const registered = [];
40
+ for (const file of entries.sort()) {
41
+ const targetAbs = join(templatesDir, file);
42
+ const linkPath = join(agentsDir, file);
43
+ const name = file.replace(/\.md$/, "");
44
+ replaceSpecialistSymlink(linkPath, targetAbs);
45
+ registered.push(name);
46
+ log(` [specialist-registration] installer-specialist-registered name=${name} link=${targetAbs}`);
47
+ }
48
+ return { registered, skipped: null };
49
+ }
50
+ export function replaceSpecialistSymlink(linkPath, targetAbs) {
51
+ let lstat = null;
52
+ try {
53
+ lstat = lstatSync(linkPath);
54
+ }
55
+ catch (err) {
56
+ const code = err.code;
57
+ if (code !== "ENOENT")
58
+ throw err;
59
+ }
60
+ if (lstat === null) {
61
+ symlinkSync(targetAbs, linkPath);
62
+ return;
63
+ }
64
+ if (!lstat.isSymbolicLink()) {
65
+ throw new SpecialistSymlinkCollision(linkPath);
66
+ }
67
+ let resolvedTarget;
68
+ try {
69
+ resolvedTarget = resolve(dirname(linkPath), readlinkSync(linkPath));
70
+ }
71
+ catch {
72
+ resolvedTarget = "<unreadable>";
73
+ }
74
+ if (resolvedTarget === targetAbs)
75
+ return;
76
+ unlinkSync(linkPath);
77
+ symlinkSync(targetAbs, linkPath);
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.62",
3
+ "version": "0.1.63",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -4,48 +4,70 @@ description: "Platform administration plugin. Provides system-status, public-hos
4
4
  tools:
5
5
  - name: system-status
6
6
  publicAllowlist: false
7
+ adminAllowlist: false
7
8
  - name: public-hostname
8
9
  publicAllowlist: false
10
+ adminAllowlist: false
9
11
  - name: brand-settings
10
12
  publicAllowlist: false
13
+ adminAllowlist: false
11
14
  - name: account-manage
12
15
  publicAllowlist: false
16
+ adminAllowlist: false
13
17
  - name: account-update
14
18
  publicAllowlist: false
19
+ adminAllowlist: false
15
20
  - name: admin-add
16
21
  publicAllowlist: false
22
+ adminAllowlist: true
17
23
  - name: admin-remove
18
24
  publicAllowlist: false
25
+ adminAllowlist: true
19
26
  - name: admin-list
20
27
  publicAllowlist: false
28
+ adminAllowlist: true
21
29
  - name: admin-update-pin
22
30
  publicAllowlist: false
31
+ adminAllowlist: true
23
32
  - name: agent-list
24
33
  publicAllowlist: false
34
+ adminAllowlist: true
25
35
  - name: agent-config-read
26
36
  publicAllowlist: false
37
+ adminAllowlist: true
27
38
  - name: logs-read
28
39
  publicAllowlist: false
40
+ adminAllowlist: false
29
41
  - name: plugin-read
30
42
  publicAllowlist: false
43
+ adminAllowlist: false
31
44
  - name: skill-load
32
45
  publicAllowlist: false
46
+ adminAllowlist: true
33
47
  - name: store-skill
34
48
  publicAllowlist: false
49
+ adminAllowlist: true
35
50
  - name: session-reset
36
51
  publicAllowlist: false
52
+ adminAllowlist: true
37
53
  - name: session-resume
38
54
  publicAllowlist: false
55
+ adminAllowlist: true
39
56
  - name: file-attach
40
57
  publicAllowlist: false
58
+ adminAllowlist: false
41
59
  - name: action-pending
42
60
  publicAllowlist: false
61
+ adminAllowlist: true
43
62
  - name: action-approve
44
63
  publicAllowlist: false
64
+ adminAllowlist: true
45
65
  - name: action-reject
46
66
  publicAllowlist: false
67
+ adminAllowlist: true
47
68
  - name: action-edit
48
69
  publicAllowlist: false
70
+ adminAllowlist: true
49
71
  hidden:
50
72
  - remote-auth-status
51
73
  - remote-auth-set-password
@@ -184,10 +184,11 @@ run_hook "admin" "" "$ENVELOPE"
184
184
  [[ -z "$HOOK_STDERR" ]] || fail "case-5 stderr must be empty, got: $HOOK_STDERR"
185
185
 
186
186
  # 5a. exactly ONE `trigger` log-ingest line carrying the operator session id
187
- # and `lastTurnBytes=<n>` (Task 131 replaces `filteredRecords`).
188
- TRIGGER_COUNT=$(ingest_lines | grep -cE "^trigger sessionId=${OP_ID} turnIndex=0 transcriptBytes=[0-9]+ lastTurnBytes=[0-9]+$" || true)
187
+ # and `conversationBytes=<n>` (Task 165 full transcript walker
188
+ # replaces last-turn-only extractor; field is renamed for clarity).
189
+ TRIGGER_COUNT=$(ingest_lines | grep -cE "^trigger sessionId=${OP_ID} turnIndex=0 transcriptBytes=[0-9]+ conversationBytes=[0-9]+$" || true)
189
190
  if [[ "$TRIGGER_COUNT" -eq 1 ]]; then
190
- pass "case-5a exactly one trigger line emitted via log-ingest carrying operator id + lastTurnBytes"
191
+ pass "case-5a exactly one trigger line emitted via log-ingest carrying operator id + conversationBytes"
191
192
  else
192
193
  fail "case-5a expected exactly 1 trigger line carrying ${OP_ID}, got $TRIGGER_COUNT (lines: $(ingest_lines))"
193
194
  fi
@@ -209,7 +210,9 @@ BODY_OK=$(printf '%s' "$RECORDER_BODY" | python3 -c '
209
210
  import sys, json
210
211
  op = sys.argv[1]
211
212
  INSTRUCTION = "update the graph with any new or missing information or intent"
212
- EXPECTED_LAST = "hello, assistant reply"
213
+ # Task 165 initialMessage carries the full transcript layout
214
+ # "user: <text>\nassistant: <text>\n…", not just the last assistant text.
215
+ EXPECTED_CONVERSATION = "user: hello operator\nassistant: hello, assistant reply"
213
216
  try:
214
217
  b = json.load(sys.stdin)
215
218
  msg = b.get("initialMessage") or ""
@@ -219,7 +222,7 @@ try:
219
222
  b.get("model") == "haiku",
220
223
  b.get("adminSessionId") == op,
221
224
  b.get("channel") == "browser",
222
- msg == INSTRUCTION + "\n\n" + EXPECTED_LAST,
225
+ msg == INSTRUCTION + "\n\n" + EXPECTED_CONVERSATION,
223
226
  "Conversation id:" not in msg,
224
227
  "Conversation transcript:" not in msg,
225
228
  "originatingSessionId" not in b,
@@ -229,7 +232,7 @@ except Exception as e:
229
232
  print("parse-fail:"+str(e))
230
233
  ' "$OP_ID" 2>/dev/null)
231
234
  if [[ "$BODY_OK" == "yes" ]]; then
232
- pass "case-5c body is Sidebar-shape with three overrides (specialist, model, initialMessage)"
235
+ pass "case-5c body carries full transcript layout (user: …\\nassistant: ) — Task 165"
233
236
  else
234
237
  fail "case-5c body shape wrong ($BODY_OK): $RECORDER_BODY"
235
238
  fi
@@ -249,9 +252,10 @@ else
249
252
  pass "case-5e no legacy stderr emissions"
250
253
  fi
251
254
 
252
- # --- Case 6 (Task 131): transcript has no assistant/user text---------
253
- # trigger-skipped reason=last-turn-empty. The hook emits the trigger
254
- # line first (lastTurnBytes=0) and then the new skip reason.
255
+ # --- Case 6 (Task 165): transcript has zero qualifying records--------
256
+ # trigger-skipped reason=conversation-empty. The hook emits the
257
+ # trigger line first (conversationBytes=0) and then the new skip
258
+ # reason.
255
259
  EMPTY_TRANSCRIPT=$(mktemp); TMPFILES+=("$EMPTY_TRANSCRIPT")
256
260
  printf '{"type":"system","summary":"noop"}\n' > "$EMPTY_TRANSCRIPT"
257
261
  EMPTY_ENVELOPE=$(python3 -c '
@@ -261,14 +265,47 @@ print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
261
265
  : > "$REQ_LOG"
262
266
  run_hook "admin" "" "$EMPTY_ENVELOPE"
263
267
  [[ "$HOOK_RC" -eq 0 ]] || fail "case-6 rc=$HOOK_RC"
264
- if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=last-turn-empty$"; then
265
- pass "case-6 empty last-turn text → trigger-skipped reason=last-turn-empty emitted"
268
+ if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=conversation-empty$"; then
269
+ pass "case-6 zero user/assistant records → trigger-skipped reason=conversation-empty emitted"
266
270
  else
267
- fail "case-6 expected trigger-skipped last-turn-empty, got: $(ingest_lines)"
271
+ fail "case-6 expected trigger-skipped conversation-empty, got: $(ingest_lines)"
268
272
  fi
269
273
  # And no recorder-spawn POST is made.
270
274
  if grep -qE '^/api/admin/claude-sessions ' "$REQ_LOG"; then
271
- fail "case-6 recorder-spawn must NOT be called when last-turn-empty"
275
+ fail "case-6 recorder-spawn must NOT be called when conversation-empty"
276
+ fi
277
+
278
+ # --- Case 7 (Task 165): three-turn conversation walks user/assistant/user
279
+ # in transcript order; layout is "user: …\nassistant: …\nuser: …". ---
280
+ THREE_TURN_TRANSCRIPT=$(mktemp); TMPFILES+=("$THREE_TURN_TRANSCRIPT")
281
+ {
282
+ printf '{"type":"user","message":{"role":"user","content":"hello"}}\n'
283
+ printf '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"what'"'"'s your business?"}]}}\n'
284
+ printf '{"type":"user","message":{"role":"user","content":"Smalleys"}}\n'
285
+ } > "$THREE_TURN_TRANSCRIPT"
286
+ THREE_ENVELOPE=$(python3 -c '
287
+ import json, sys
288
+ print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
289
+ ' "$OP_ID" "$THREE_TURN_TRANSCRIPT")
290
+ : > "$REQ_LOG"
291
+ run_hook "admin" "" "$THREE_ENVELOPE"
292
+ [[ "$HOOK_RC" -eq 0 ]] || fail "case-7 rc=$HOOK_RC stderr=$HOOK_STDERR"
293
+ THREE_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
294
+ THREE_OK=$(printf '%s' "$THREE_BODY" | python3 -c '
295
+ import sys, json
296
+ INSTRUCTION = "update the graph with any new or missing information or intent"
297
+ EXPECTED = "user: hello\nassistant: what'"'"'s your business?\nuser: Smalleys"
298
+ try:
299
+ b = json.load(sys.stdin)
300
+ msg = b.get("initialMessage") or ""
301
+ print("yes" if msg == INSTRUCTION + "\n\n" + EXPECTED else "no:"+repr(msg))
302
+ except Exception as e:
303
+ print("parse-fail:"+str(e))
304
+ ' 2>/dev/null)
305
+ if [[ "$THREE_OK" == "yes" ]]; then
306
+ pass "case-7 three-turn conversation walks user/assistant/user in order"
307
+ else
308
+ fail "case-7 expected multi-turn layout, got: $THREE_OK | body: $THREE_BODY"
272
309
  fi
273
310
 
274
311
  # --- Summary ------------------------------------------------------------
@@ -23,8 +23,8 @@
23
23
  # "database-operator" (recursion guard)
24
24
  # - Stop-hook stdin must be non-empty → reason=empty-stdin
25
25
  # - transcript_path must exist on disk → reason=missing-transcript
26
- # - a most-recent assistant or user turn → reason=last-turn-empty
27
- # must be extractable from the JSONL
26
+ # - at least one user or assistant record with → reason=conversation-empty
27
+ # non-empty text must exist in the JSONL (Task 165)
28
28
  #
29
29
  # Input: Claude Code's Stop hook stdin shape
30
30
  # { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
@@ -91,8 +91,12 @@ if [ -z "$ADMIN_SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIP
91
91
  exit 0
92
92
  fi
93
93
 
94
- # Extract the operator's most-recent turn text from the JSONL.
95
- LAST_TURN_TEXT=$(python3 - "$TRANSCRIPT_PATH" <<'PY'
94
+ # Task 165 walk every user/assistant record in transcript order; emit
95
+ # "<type>: <text>" per record; join with "\n". The recorder needs the
96
+ # whole exchange to infer operator-asserted facts that span multiple
97
+ # turns (e.g. "hello / what's your business? / Smalleys"). The earlier
98
+ # "last-turn-only" extractor stripped the framing the recorder needed.
99
+ CONVERSATION_TEXT=$(python3 - "$TRANSCRIPT_PATH" <<'PY'
96
100
  import sys, json
97
101
 
98
102
  def block_text(block):
@@ -113,8 +117,7 @@ def record_text(rec):
113
117
  return ""
114
118
 
115
119
  path = sys.argv[1]
116
- last_assistant = ""
117
- last_user = ""
120
+ turns = []
118
121
  try:
119
122
  with open(path, "r", encoding="utf-8") as f:
120
123
  for line in f:
@@ -123,26 +126,24 @@ try:
123
126
  try: rec = json.loads(line)
124
127
  except Exception: continue
125
128
  t = rec.get("type") if isinstance(rec, dict) else None
126
- if t == "assistant":
127
- tx = record_text(rec).strip()
128
- if tx: last_assistant = tx
129
- elif t == "user":
130
- tx = record_text(rec).strip()
131
- if tx: last_user = tx
129
+ if t not in ("user", "assistant"): continue
130
+ tx = record_text(rec).strip()
131
+ if not tx: continue
132
+ turns.append(f"{t}: {tx}")
132
133
  except Exception:
133
134
  pass
134
135
 
135
- print(last_assistant if last_assistant else last_user, end="")
136
+ print("\n".join(turns), end="")
136
137
  PY
137
138
  )
138
139
 
139
- LAST_TURN_BYTES=$(printf '%s' "$LAST_TURN_TEXT" | wc -c | tr -d ' ')
140
+ CONVERSATION_BYTES=$(printf '%s' "$CONVERSATION_TEXT" | wc -c | tr -d ' ')
140
141
  TRANSCRIPT_BYTES=$(wc -c <"$TRANSCRIPT_PATH" 2>/dev/null | tr -d ' ' || echo 0)
141
142
 
142
- emit_log "trigger sessionId=${ADMIN_SESSION_ID} turnIndex=0 transcriptBytes=${TRANSCRIPT_BYTES} lastTurnBytes=${LAST_TURN_BYTES}"
143
+ emit_log "trigger sessionId=${ADMIN_SESSION_ID} turnIndex=0 transcriptBytes=${TRANSCRIPT_BYTES} conversationBytes=${CONVERSATION_BYTES}"
143
144
 
144
- if [ -z "$LAST_TURN_TEXT" ]; then
145
- emit_log "trigger-skipped sessionId=${ADMIN_SESSION_ID} reason=last-turn-empty"
145
+ if [ -z "$CONVERSATION_TEXT" ]; then
146
+ emit_log "trigger-skipped sessionId=${ADMIN_SESSION_ID} reason=conversation-empty"
146
147
  exit 0
147
148
  fi
148
149
 
@@ -153,9 +154,9 @@ fi
153
154
  SPAWN_BODY=$(python3 -c '
154
155
  import sys, json
155
156
  sid = sys.argv[1]
156
- last_turn = sys.argv[2]
157
+ conversation = sys.argv[2]
157
158
  instruction = "update the graph with any new or missing information or intent"
158
- text = instruction + "\n\n" + last_turn
159
+ text = instruction + "\n\n" + conversation
159
160
  body = {
160
161
  "adminSessionId": sid,
161
162
  "channel": "browser",
@@ -164,9 +165,9 @@ body = {
164
165
  "initialMessage": text,
165
166
  }
166
167
  print(json.dumps(body))
167
- ' "$ADMIN_SESSION_ID" "$LAST_TURN_TEXT" 2>/dev/null)
168
+ ' "$ADMIN_SESSION_ID" "$CONVERSATION_TEXT" 2>/dev/null)
168
169
 
169
- INITIAL_BYTES=$(printf '%s\n\n%s' "update the graph with any new or missing information or intent" "$LAST_TURN_TEXT" | wc -c | tr -d ' ')
170
+ INITIAL_BYTES=$(printf '%s\n\n%s' "update the graph with any new or missing information or intent" "$CONVERSATION_TEXT" | wc -c | tr -d ' ')
170
171
  emit_log "spawn-request sessionId=${ADMIN_SESSION_ID} specialist=database-operator initialMessageBytes=${INITIAL_BYTES}"
171
172
 
172
173
  SPAWN_RES_FILE=$(mktemp)
@@ -4,22 +4,31 @@ description: "CRM contacts plugin. Provides contact-create, contact-lookup, cont
4
4
  tools:
5
5
  - name: contact-create
6
6
  publicAllowlist: false
7
+ adminAllowlist: false
7
8
  - name: contact-lookup
8
9
  publicAllowlist: false
10
+ adminAllowlist: false
9
11
  - name: contact-update
10
12
  publicAllowlist: false
13
+ adminAllowlist: false
11
14
  - name: contact-delete
12
15
  publicAllowlist: false
16
+ adminAllowlist: false
13
17
  - name: contact-list
14
18
  publicAllowlist: false
19
+ adminAllowlist: false
15
20
  - name: contact-export
16
21
  publicAllowlist: false
22
+ adminAllowlist: false
17
23
  - name: contact-erase
18
24
  publicAllowlist: false
25
+ adminAllowlist: false
19
26
  - name: group-create
20
27
  publicAllowlist: false
28
+ adminAllowlist: false
21
29
  - name: group-manage
22
30
  publicAllowlist: false
31
+ adminAllowlist: false
23
32
  always: false
24
33
  embed: false
25
34
  metadata: {"platform":{}}