@rubytech/create-maxy-code 0.1.112 → 0.1.114
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.
- package/dist/__tests__/brew-install.test.js +10 -0
- package/dist/__tests__/brew-resolve.test.js +35 -0
- package/dist/__tests__/launchd-plist.test.js +46 -0
- package/dist/__tests__/macos-darwin-branch.test.js +85 -0
- package/dist/brew-install.js +5 -0
- package/dist/index.js +26 -6
- package/package.json +1 -1
- package/payload/platform/neo4j/schema.cypher +7 -2
- package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +14 -7
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +105 -0
- package/payload/platform/plugins/docs/references/deployment.md +22 -4
- package/payload/platform/plugins/docs/references/internals.md +10 -26
- package/payload/platform/plugins/memory/PLUGIN.md +2 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js +35 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.d.ts +15 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.js +64 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.js.map +1 -0
- package/payload/platform/plugins/memory/references/schema-estate-agent.md +25 -0
- package/payload/platform/plugins/venture-studio/PLUGIN.md +7 -3
- package/payload/platform/plugins/venture-studio/bin/scaffold.sh +14 -2
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +10 -0
- package/payload/platform/plugins/venture-studio/skills/prototype-host/SKILL.md +179 -0
- package/payload/platform/plugins/venture-studio/skills/prototype-host/references/cloudflared-ingress-edit.md +81 -0
- package/payload/platform/plugins/venture-studio/skills/prototype-host/references/scaffold-frameworks.md +60 -0
- package/payload/platform/plugins/venture-studio/skills/prototype-host/references/systemd-user-service.md +104 -0
- package/payload/platform/plugins/whatsapp/.claude-plugin/plugin.json +9 -0
- package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
- package/payload/platform/services/claude-session-manager/dist/brand-foreign-filter.d.ts +30 -0
- package/payload/platform/services/claude-session-manager/dist/brand-foreign-filter.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/brand-foreign-filter.js +86 -0
- package/payload/platform/services/claude-session-manager/dist/brand-foreign-filter.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/index.js +51 -9
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +35 -4
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +7 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +7 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/templates/agents/public/IDENTITY.md +11 -1
- package/payload/premium-plugins/real-agent/BUNDLE.md +1 -1
- package/payload/premium-plugins/real-agent/agents/listing-curator.md +21 -2
- package/payload/premium-plugins/real-agent/plugins/buyers/.claude-plugin/plugin.json +1 -1
- package/payload/premium-plugins/real-agent/plugins/buyers/PLUGIN.md +3 -2
- package/payload/premium-plugins/real-agent/plugins/buyers/skills/property-recommender/SKILL.md +116 -0
- package/payload/premium-plugins/venture-studio/PLUGIN.md +7 -3
- package/payload/premium-plugins/venture-studio/bin/scaffold.sh +14 -2
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +10 -0
- package/payload/premium-plugins/venture-studio/skills/prototype-host/SKILL.md +179 -0
- package/payload/premium-plugins/venture-studio/skills/prototype-host/references/cloudflared-ingress-edit.md +81 -0
- package/payload/premium-plugins/venture-studio/skills/prototype-host/references/scaffold-frameworks.md +60 -0
- package/payload/premium-plugins/venture-studio/skills/prototype-host/references/systemd-user-service.md +104 -0
- package/payload/server/server.js +92 -41
|
@@ -73,9 +73,14 @@ test("decideBrewInstallPlan: all pkgs already installed → empty install list,
|
|
|
73
73
|
],
|
|
74
74
|
});
|
|
75
75
|
assert.deepEqual(plan.toInstall, []);
|
|
76
|
+
// Each pkg emits the legacy `[homebrew]` line plus the uniform-prefix
|
|
77
|
+
// `[create-maxy] brew-formula=… action=…` mirror Task 297 added for the
|
|
78
|
+
// grep recipe.
|
|
76
79
|
assert.deepEqual(plan.logs, [
|
|
77
80
|
" [homebrew] node@22 already installed (v22.18.1)",
|
|
81
|
+
" [create-maxy] brew-formula=node@22 action=present",
|
|
78
82
|
" [homebrew] neo4j already installed (v5.20.0)",
|
|
83
|
+
" [create-maxy] brew-formula=neo4j action=present",
|
|
79
84
|
]);
|
|
80
85
|
});
|
|
81
86
|
test("decideBrewInstallPlan: some missing → install list contains only missing formulas, install + already logs interleaved", () => {
|
|
@@ -89,8 +94,11 @@ test("decideBrewInstallPlan: some missing → install list contains only missing
|
|
|
89
94
|
assert.deepEqual(plan.toInstall, ["neo4j", "cloudflared"]);
|
|
90
95
|
assert.deepEqual(plan.logs, [
|
|
91
96
|
" [homebrew] node@22 already installed (v22.18.1)",
|
|
97
|
+
" [create-maxy] brew-formula=node@22 action=present",
|
|
92
98
|
" [homebrew] installing neo4j …",
|
|
99
|
+
" [create-maxy] brew-formula=neo4j action=install",
|
|
93
100
|
" [homebrew] installing cloudflared …",
|
|
101
|
+
" [create-maxy] brew-formula=cloudflared action=install",
|
|
94
102
|
]);
|
|
95
103
|
});
|
|
96
104
|
test("decideBrewInstallPlan: all missing → install list = every formula, only install logs", () => {
|
|
@@ -103,7 +111,9 @@ test("decideBrewInstallPlan: all missing → install list = every formula, only
|
|
|
103
111
|
assert.deepEqual(plan.toInstall, ["node@22", "neo4j"]);
|
|
104
112
|
assert.deepEqual(plan.logs, [
|
|
105
113
|
" [homebrew] installing node@22 …",
|
|
114
|
+
" [create-maxy] brew-formula=node@22 action=install",
|
|
106
115
|
" [homebrew] installing neo4j …",
|
|
116
|
+
" [create-maxy] brew-formula=neo4j action=install",
|
|
107
117
|
]);
|
|
108
118
|
});
|
|
109
119
|
// ---------------------------------------------------------------------------
|
|
@@ -101,3 +101,38 @@ test("DARWIN_ALIASES contains nodejs → node@22 and is the only entry", () => {
|
|
|
101
101
|
assert.equal(DARWIN_ALIASES.nodejs, "node@22");
|
|
102
102
|
assert.equal(Object.keys(DARWIN_ALIASES).length, 1);
|
|
103
103
|
});
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// DARWIN_BASE_DEPS + neo4j shape lock — every brew dep the darwin install
|
|
106
|
+
// branch feeds into installAllBrewPackages must either (a) be a real Homebrew
|
|
107
|
+
// core formula already, or (b) resolve to one via DARWIN_ALIASES. The list is
|
|
108
|
+
// duplicated here from index.ts:installSystemDeps so a future edit of either
|
|
109
|
+
// surface has to keep them in sync.
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Mirrors `DARWIN_BASE_DEPS` in packages/create-maxy-code/src/index.ts. Every
|
|
112
|
+
// entry must be either a real brew formula (pass-through) or an alias.
|
|
113
|
+
const DARWIN_BASE_DEPS_FIXTURE = ["curl", "git", "unzip", "jq", "poppler", "ffmpeg"];
|
|
114
|
+
// `neo4j` and `cloudflared` are passed to installAllBrewPackages from
|
|
115
|
+
// installNeo4j (index.ts:1265) and installCloudflared (index.ts:1728). Both are
|
|
116
|
+
// concrete brew formula names — no alias hop expected.
|
|
117
|
+
const DARWIN_SOLO_DEPS_FIXTURE = ["neo4j", "cloudflared"];
|
|
118
|
+
test("every DARWIN_BASE_DEPS entry passes through the resolver as the brew formula name", () => {
|
|
119
|
+
for (const pkg of DARWIN_BASE_DEPS_FIXTURE) {
|
|
120
|
+
const r = decideBrewResolution({ pkg, brewInstalled: false });
|
|
121
|
+
assert.equal(r.resolved, pkg, `DARWIN_BASE_DEPS entry '${pkg}' must not be aliased`);
|
|
122
|
+
assert.equal(r.log, null, `DARWIN_BASE_DEPS entry '${pkg}' must not emit an alias log line`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
test("neo4j and cloudflared pass through unchanged — no alias hop", () => {
|
|
126
|
+
for (const pkg of DARWIN_SOLO_DEPS_FIXTURE) {
|
|
127
|
+
const r = decideBrewResolution({ pkg, brewInstalled: false });
|
|
128
|
+
assert.equal(r.resolved, pkg);
|
|
129
|
+
assert.equal(r.log, null);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
test("nodejs alias hop resolves to node@22 for darwin install", () => {
|
|
133
|
+
// The shared-dep call site (index.ts:988 installNodejs) passes "nodejs";
|
|
134
|
+
// this test pins the brew-formula landing for that single alias.
|
|
135
|
+
const r = decideBrewResolution({ pkg: "nodejs", brewInstalled: false });
|
|
136
|
+
assert.equal(r.resolved, "node@22");
|
|
137
|
+
assert.match(r.log ?? "", /node@22/);
|
|
138
|
+
});
|
|
@@ -128,6 +128,52 @@ test("renderPlist escapes `&` once, never double-escapes", () => {
|
|
|
128
128
|
// child to "" (current dir = launchd's cwd, /), which silently changes
|
|
129
129
|
// behaviour from "stay where invoked" to "/" — a different bug than absence.
|
|
130
130
|
// ---------------------------------------------------------------------------
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Brand-stamped labels — both shipping brands must render distinct labels so a
|
|
133
|
+
// co-resident install does not collide on a single LaunchAgent file.
|
|
134
|
+
// `launchdLabel()` in index.ts returns `com.rubytech.${BRAND.hostname}`; this
|
|
135
|
+
// test pins the brand-input → label contract end-to-end against the renderer.
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
test("renderPlist maxy-code brand fields land in the plist verbatim", () => {
|
|
138
|
+
// BRAND.hostname=maxy-code, configDir=.maxy-code per brands/maxy-code/brand.json.
|
|
139
|
+
// installServiceDarwin builds ProgramArguments=["/bin/bash", wrapperPath],
|
|
140
|
+
// WorkingDirectory=`${INSTALL_DIR}/server`, std{out,err}Path=`<logsDir>/server.log`.
|
|
141
|
+
const xml = renderPlist({
|
|
142
|
+
label: "com.rubytech.maxy-code",
|
|
143
|
+
programArguments: ["/bin/bash", "/Users/alice/.maxy-code/launchd-wrapper.sh"],
|
|
144
|
+
stdoutPath: "/Users/alice/.maxy-code/logs/server.log",
|
|
145
|
+
stderrPath: "/Users/alice/.maxy-code/logs/server.log",
|
|
146
|
+
keepAlive: true,
|
|
147
|
+
runAtLoad: true,
|
|
148
|
+
workingDirectory: "/Users/alice/maxy-code/server",
|
|
149
|
+
});
|
|
150
|
+
assert.match(xml, /<key>Label<\/key>\s*<string>com\.rubytech\.maxy-code<\/string>/);
|
|
151
|
+
assert.match(xml, /<string>\/bin\/bash<\/string>/);
|
|
152
|
+
assert.match(xml, /<string>\/Users\/alice\/\.maxy-code\/launchd-wrapper\.sh<\/string>/);
|
|
153
|
+
assert.match(xml, /<key>WorkingDirectory<\/key>\s*<string>\/Users\/alice\/maxy-code\/server<\/string>/);
|
|
154
|
+
assert.match(xml, /<key>StandardOutPath<\/key>\s*<string>\/Users\/alice\/\.maxy-code\/logs\/server\.log<\/string>/);
|
|
155
|
+
assert.match(xml, /<key>KeepAlive<\/key>\s*<true\/>/);
|
|
156
|
+
assert.match(xml, /<key>RunAtLoad<\/key>\s*<true\/>/);
|
|
157
|
+
});
|
|
158
|
+
test("renderPlist realagent-code brand fields land in the plist verbatim", () => {
|
|
159
|
+
// BRAND.hostname=realagent-code, configDir=.realagent-code per brands/realagent-code/brand.json.
|
|
160
|
+
// The label must differ from the maxy-code label so a co-resident install
|
|
161
|
+
// produces two distinct ~/Library/LaunchAgents/<label>.plist files.
|
|
162
|
+
const xml = renderPlist({
|
|
163
|
+
label: "com.rubytech.realagent-code",
|
|
164
|
+
programArguments: ["/bin/bash", "/Users/alice/.realagent-code/launchd-wrapper.sh"],
|
|
165
|
+
stdoutPath: "/Users/alice/.realagent-code/logs/server.log",
|
|
166
|
+
stderrPath: "/Users/alice/.realagent-code/logs/server.log",
|
|
167
|
+
keepAlive: true,
|
|
168
|
+
runAtLoad: true,
|
|
169
|
+
workingDirectory: "/Users/alice/realagent-code/server",
|
|
170
|
+
});
|
|
171
|
+
assert.match(xml, /<key>Label<\/key>\s*<string>com\.rubytech\.realagent-code<\/string>/);
|
|
172
|
+
assert.match(xml, /<string>\/Users\/alice\/\.realagent-code\/launchd-wrapper\.sh<\/string>/);
|
|
173
|
+
assert.match(xml, /<key>WorkingDirectory<\/key>\s*<string>\/Users\/alice\/realagent-code\/server<\/string>/);
|
|
174
|
+
// Label collision guard — the maxy-code label must not appear in a realagent plist.
|
|
175
|
+
assert.ok(!/com\.rubytech\.maxy-code/.test(xml), "realagent plist must not carry maxy-code label");
|
|
176
|
+
});
|
|
131
177
|
test("renderPlist omits WorkingDirectory key when workingDirectory is absent", () => {
|
|
132
178
|
const xml = renderPlist({
|
|
133
179
|
label: "com.rubytech.maxy",
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Static-grep contract for the darwin install branches in index.ts.
|
|
2
|
+
//
|
|
3
|
+
// Task 297 audited every `platform === "darwin"` block; this test pins the
|
|
4
|
+
// shape so a future refactor that removes a darwin branch without an explicit
|
|
5
|
+
// reason fails loudly here. The test reads src/index.ts at runtime (the
|
|
6
|
+
// authoritative source) and regex-asserts the per-branch markers — keeping
|
|
7
|
+
// the spec adjacent to the code rather than buried in a doc.
|
|
8
|
+
//
|
|
9
|
+
// Why static-grep, not behaviour: the install branches each spawn (sudo,
|
|
10
|
+
// brew, launchctl), and end-to-end darwin reproduction belongs to the manual
|
|
11
|
+
// smoke path in the task brief. The unit suite's job is to ensure the
|
|
12
|
+
// branch points still exist after a refactor.
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, resolve } from "node:path";
|
|
18
|
+
// dist/__tests__/macos-darwin-branch.test.js → ../../src/index.ts
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const INDEX_TS = resolve(here, "../../src/index.ts");
|
|
21
|
+
const SRC = readFileSync(INDEX_TS, "utf-8");
|
|
22
|
+
test("installSystemDeps has darwin branch that calls setMacosHostnameViaScutil on --hostname and scutil --get LocalHostName without", () => {
|
|
23
|
+
// The hostname block at index.ts:installSystemDeps must:
|
|
24
|
+
// - call setMacosHostnameViaScutil(HOSTNAME_FLAG) when HOSTNAME_FLAG is set
|
|
25
|
+
// - call scutil --get LocalHostName when HOSTNAME_FLAG is absent
|
|
26
|
+
// The two paths are the contract Task 297 locks in.
|
|
27
|
+
assert.match(SRC, /setMacosHostnameViaScutil\(HOSTNAME_FLAG\)/, "scutil-set path on --hostname must call setMacosHostnameViaScutil");
|
|
28
|
+
assert.match(SRC, /scutil["']?,\s*\[["']--get["']\s*,\s*["']LocalHostName["']\]/, "scutil --get LocalHostName must be the no-flag default on darwin");
|
|
29
|
+
assert.match(SRC, /darwin-hostname-mode=scutil-get/, "scutil-get success path must emit observability log");
|
|
30
|
+
assert.match(SRC, /darwin-hostname-mode=scutil-set/, "scutil-set path must emit observability log");
|
|
31
|
+
assert.match(SRC, /darwin-hostname-mode=brand-fallback/, "brand-fallback path must emit observability log");
|
|
32
|
+
});
|
|
33
|
+
test("installSystemDeps darwin branch installs DARWIN_BASE_DEPS via brew", () => {
|
|
34
|
+
// The shared dep set the darwin path feeds to installAllBrewPackages. If
|
|
35
|
+
// this list shrinks or a name changes, brew-resolve.test.ts (DARWIN_BASE_DEPS_FIXTURE)
|
|
36
|
+
// must move in lockstep — the comment in brew-resolve.test.ts says so.
|
|
37
|
+
assert.match(SRC, /DARWIN_BASE_DEPS\s*=\s*\[\s*"curl"\s*,\s*"git"\s*,\s*"unzip"\s*,\s*"jq"\s*,\s*"poppler"\s*,\s*"ffmpeg"\s*\]/);
|
|
38
|
+
assert.match(SRC, /installAllBrewPackages\(DARWIN_BASE_DEPS,\s*logFile\)/);
|
|
39
|
+
});
|
|
40
|
+
test("installCloudflared darwin branch logs the documented skip and brew-installs cloudflared", () => {
|
|
41
|
+
// Task 297 doctrine: skip tunnel provisioning entirely on darwin. The brew
|
|
42
|
+
// install puts the binary on PATH so the operator can opt in to `cloudflared
|
|
43
|
+
// tunnel login` post-install; the explicit log line is the diagnostic
|
|
44
|
+
// anchor for investigators grepping the install log.
|
|
45
|
+
assert.match(SRC, /darwin-cloudflare-skip=true/);
|
|
46
|
+
assert.match(SRC, /installAllBrewPackages\(\["cloudflared"\],\s*logFile\)/);
|
|
47
|
+
// No `cloudflared service install` or `cloudflared tunnel route dns`
|
|
48
|
+
// invocation in the installer surface — those are operator-driven,
|
|
49
|
+
// post-install steps. Match actual argv shapes (string-literal list to
|
|
50
|
+
// spawnSync/shell) rather than prose: argv arrays carry the args as
|
|
51
|
+
// quoted siblings inside `[...]`. Comment text is filtered by requiring
|
|
52
|
+
// a quote character immediately adjacent.
|
|
53
|
+
assert.ok(!/\[\s*["']cloudflared["'][^\]]*["']service["'][^\]]*["']install["']/.test(SRC), "installer must not invoke `cloudflared service install`");
|
|
54
|
+
assert.ok(!/\[\s*["']cloudflared["'][^\]]*["']tunnel["'][^\]]*["']route["'][^\]]*["']dns["']/.test(SRC), "installer must not invoke `cloudflared tunnel route dns`");
|
|
55
|
+
});
|
|
56
|
+
test("installService darwin branch delegates to installServiceDarwin (launchd path, not systemd)", () => {
|
|
57
|
+
// Two contracts: the platform switch routes darwin to installServiceDarwin,
|
|
58
|
+
// and installServiceDarwin writes the plist + bootstraps it via launchctl.
|
|
59
|
+
assert.match(SRC, /platform\s*===\s*"darwin"\s*\)\s*\{\s*installServiceDarwin\(\)/);
|
|
60
|
+
assert.match(SRC, /launchctl["']?,\s*\[["']bootstrap["']/);
|
|
61
|
+
assert.match(SRC, /launchd-plist=\$\{path\}\s+loaded=true/, "successful bootstrap must emit launchd-plist=… loaded=true");
|
|
62
|
+
assert.match(SRC, /launchd-plist=\$\{path\}\s+loaded=false/, "failed bootstrap must emit launchd-plist=… loaded=false");
|
|
63
|
+
});
|
|
64
|
+
test("installNodejs darwin branch brew-installs the `nodejs` alias name (resolver hops to node@22)", () => {
|
|
65
|
+
// The shared call site uses the apt-style `nodejs` name. brew-resolve.ts
|
|
66
|
+
// hops it to `node@22` via DARWIN_ALIASES — the alias is the deliberate
|
|
67
|
+
// wedge so a single dep name works across both backends.
|
|
68
|
+
assert.match(SRC, /platform\s*===\s*"darwin"[\s\S]{0,400}installAllBrewPackages\(\["nodejs"\]/);
|
|
69
|
+
});
|
|
70
|
+
test("installNeo4j darwin branch brew-installs `neo4j` (no alias hop)", () => {
|
|
71
|
+
// `neo4j` is the concrete brew formula name on darwin, no alias needed.
|
|
72
|
+
assert.match(SRC, /platform\s*===\s*"darwin"[\s\S]{0,800}installAllBrewPackages\(\["neo4j"\]/);
|
|
73
|
+
});
|
|
74
|
+
test("chromium binary-path file write skips darwin (Playwright-managed)", () => {
|
|
75
|
+
// writeChromiumBinaryPathFile early-returns on non-linux. The completion URL
|
|
76
|
+
// and VNC stack on darwin use Playwright/brew-managed Chromium instead of
|
|
77
|
+
// the linux per-brand chromium-binary.path file.
|
|
78
|
+
assert.match(SRC, /writeChromiumBinaryPathFile[\s\S]{0,200}process\.platform\s*!==\s*"linux"/);
|
|
79
|
+
});
|
|
80
|
+
test("uninstall darwin path bootsout the LaunchAgent and removes the plist", () => {
|
|
81
|
+
// The uninstall surface lives in uninstall.ts (separate file); this index.ts
|
|
82
|
+
// sanity check just confirms the launchdLabel helper still exists and is
|
|
83
|
+
// brand-keyed — install + uninstall both depend on the same label shape.
|
|
84
|
+
assert.match(SRC, /function launchdLabel\(\):\s*string\s*\{\s*return\s*`com\.rubytech\.\$\{BRAND\.hostname\}`/);
|
|
85
|
+
});
|
package/dist/brew-install.js
CHANGED
|
@@ -71,9 +71,14 @@ export function decideBrewInstallPlan(input) {
|
|
|
71
71
|
if (p.installed) {
|
|
72
72
|
const head = p.versions[0] ?? "?";
|
|
73
73
|
logs.push(` [homebrew] ${p.resolvedFormula} already installed (v${head})`);
|
|
74
|
+
// Uniform-prefix mirror line so the install-log grep recipe in
|
|
75
|
+
// platform/plugins/docs/references/deployment.md (`grep -E '^\[create-maxy\] …'`)
|
|
76
|
+
// covers every brew step alongside platform=, darwin-hostname-mode=, etc.
|
|
77
|
+
logs.push(` [create-maxy] brew-formula=${p.resolvedFormula} action=present`);
|
|
74
78
|
}
|
|
75
79
|
else {
|
|
76
80
|
logs.push(` [homebrew] installing ${p.resolvedFormula} …`);
|
|
81
|
+
logs.push(` [create-maxy] brew-formula=${p.resolvedFormula} action=install`);
|
|
77
82
|
toInstall.push(p.resolvedFormula);
|
|
78
83
|
}
|
|
79
84
|
}
|
package/dist/index.js
CHANGED
|
@@ -614,6 +614,7 @@ function installSystemDeps() {
|
|
|
614
614
|
// otherwise preserve the existing system hostname.
|
|
615
615
|
if (HOSTNAME_FLAG) {
|
|
616
616
|
console.log(` Hostname: ${HOSTNAME_FLAG} (from --hostname flag)`);
|
|
617
|
+
logFile(` [create-maxy] darwin-hostname-mode=scutil-set value=${HOSTNAME_FLAG}`);
|
|
617
618
|
try {
|
|
618
619
|
setMacosHostnameViaScutil(HOSTNAME_FLAG);
|
|
619
620
|
DEVICE_HOSTNAME = HOSTNAME_FLAG;
|
|
@@ -623,11 +624,25 @@ function installSystemDeps() {
|
|
|
623
624
|
}
|
|
624
625
|
}
|
|
625
626
|
else {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
627
|
+
// No flag — read the live LocalHostName so the printed URL matches mDNS
|
|
628
|
+
// resolution (`<LocalHostName>.local`). The `hostname` command on darwin
|
|
629
|
+
// returns the fully-qualified `<name>.local`, which would render the
|
|
630
|
+
// completion URL with a double `.local` suffix; scutil --get returns the
|
|
631
|
+
// bare token. No sudo, no system mutation.
|
|
632
|
+
const scutilGet = spawnSync("scutil", ["--get", "LocalHostName"], { encoding: "utf-8", stdio: "pipe", timeout: 5_000 });
|
|
633
|
+
const scutilName = ((scutilGet.stdout ?? "").trim());
|
|
634
|
+
if (scutilGet.status === 0 && scutilName.length > 0) {
|
|
635
|
+
DEVICE_HOSTNAME = scutilName;
|
|
636
|
+
console.log(` Hostname: ${DEVICE_HOSTNAME} (from scutil --get LocalHostName — no --hostname flag)`);
|
|
637
|
+
logFile(` [create-maxy] darwin-hostname-mode=scutil-get value=${DEVICE_HOSTNAME}`);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
// scutil failed or returned empty — fall back to BRAND.hostname so the
|
|
641
|
+
// completion URL at least matches the brand's documented .local name.
|
|
642
|
+
DEVICE_HOSTNAME = BRAND.hostname;
|
|
643
|
+
console.log(` Hostname: ${DEVICE_HOSTNAME} (brand fallback — scutil --get LocalHostName empty)`);
|
|
644
|
+
logFile(` [create-maxy] darwin-hostname-mode=brand-fallback value=${DEVICE_HOSTNAME} scutil-status=${scutilGet.status}`);
|
|
629
645
|
}
|
|
630
|
-
catch { /* fallback to brand — set at declaration */ }
|
|
631
646
|
}
|
|
632
647
|
// macOS has no apt analogue for the VNC/WiFi-AP stacks
|
|
633
648
|
// (kiosk display + hostapd/dnsmasq are Pi-specific's
|
|
@@ -1541,8 +1556,11 @@ function installCloudflared() {
|
|
|
1541
1556
|
if (platform === "darwin") {
|
|
1542
1557
|
// Homebrew's `cloudflared` formula tracks the Cloudflare release stream;
|
|
1543
1558
|
// tunnel-login flow stays unchanged (`feedback_no_api_token_route.md`).
|
|
1544
|
-
// `cloudflared service install`
|
|
1545
|
-
//
|
|
1559
|
+
// `cloudflared service install` and `cloudflared tunnel route dns` are not
|
|
1560
|
+
// invoked by the installer on darwin — public reach is operator-driven
|
|
1561
|
+
// post-install (`cloudflared tunnel login` + follow-up tooling). The brew
|
|
1562
|
+
// install puts the binary on PATH so the operator can opt in later.
|
|
1563
|
+
logFile(` [create-maxy] darwin-cloudflare-skip=true reason="Mac laptop — no public hostname provisioned at install time"`);
|
|
1546
1564
|
installAllBrewPackages(["cloudflared"], logFile);
|
|
1547
1565
|
return;
|
|
1548
1566
|
}
|
|
@@ -2592,11 +2610,13 @@ function installServiceDarwin() {
|
|
|
2592
2610
|
if (bootstrap.status === 0) {
|
|
2593
2611
|
console.log(` [launchd] bootstrap ${gui()}/${label} ok`);
|
|
2594
2612
|
logFile(` [launchd] bootstrap ${gui()}/${label} ok`);
|
|
2613
|
+
logFile(` [create-maxy] launchd-plist=${path} loaded=true`);
|
|
2595
2614
|
}
|
|
2596
2615
|
else {
|
|
2597
2616
|
const stderr = (bootstrap.stderr ?? "").trim();
|
|
2598
2617
|
console.error(` [launchd] bootstrap returned ${bootstrap.status}: ${stderr}`);
|
|
2599
2618
|
logFile(` [launchd] bootstrap returned ${bootstrap.status}: ${stderr}`);
|
|
2619
|
+
logFile(` [create-maxy] launchd-plist=${path} loaded=false exit=${bootstrap.status}`);
|
|
2600
2620
|
throw new Error(`launchctl bootstrap ${gui()} ${path} failed (exit ${bootstrap.status}): ${stderr}`);
|
|
2601
2621
|
}
|
|
2602
2622
|
// Wait for the server to come up.
|
package/package.json
CHANGED
|
@@ -792,8 +792,13 @@ FOR (ag:AccessGrant) ON (ag.status);
|
|
|
792
792
|
// displayName — operator-facing name from config.json
|
|
793
793
|
// status — 'active' | 'inactive' | <other> per config.json
|
|
794
794
|
// model — Anthropic model id used by the public agent
|
|
795
|
-
// liveMemory — bool;
|
|
796
|
-
//
|
|
795
|
+
// liveMemory — bool; when true, the public PTY spawn includes
|
|
796
|
+
// memory-search in --allowed-tools so the agent can
|
|
797
|
+
// hit the graph at message time (scoped via
|
|
798
|
+
// ALLOWED_SCOPES=public). When false, the agent
|
|
799
|
+
// runs with only its baked KNOWLEDGE.md.
|
|
800
|
+
// knowledgeKeywords — string[] used by update-knowledge to broaden the
|
|
801
|
+
// refresh query alongside operator-tagged nodes
|
|
797
802
|
// role — always 'agent' (mirrors KnowledgeDocument.role
|
|
798
803
|
// discriminator; surfaces in BM25 hits)
|
|
799
804
|
// createdAt — ISO 8601, set on first projection
|
|
@@ -41,13 +41,13 @@ Each agent lives at `{accountDir}/agents/{slug}/`:
|
|
|
41
41
|
|
|
42
42
|
### liveMemory
|
|
43
43
|
|
|
44
|
-
When `true`, the
|
|
44
|
+
Controls whether the public agent gets `memory-search` at message time. When `true`, the per-spawn `--allowed-tools` allowlist includes `memory-search` so the agent can hit the graph during a turn — scoped to `scope: "public"` nodes via `ALLOWED_SCOPES=public` on the spawn env. When `false` (default), the agent runs with only the baked KNOWLEDGE.md and has no graph access mid-conversation.
|
|
45
45
|
|
|
46
|
-
Present during creation and editing as a yes/no choice. Explain the trade-off: live memory adds a small latency per
|
|
46
|
+
Present during creation and editing as a yes/no choice. Explain the trade-off: live memory adds a small latency per turn when the agent decides to search but keeps the agent current with graph changes (new listings, fresh availability). For agents whose knowledge is fully covered by KNOWLEDGE.md (pricing, FAQs), leave off. For agents that need to surface graph content created after the agent was deployed (catalogue items, schedule slots), enable it.
|
|
47
47
|
|
|
48
48
|
### knowledgeKeywords
|
|
49
49
|
|
|
50
|
-
Topic keywords the agent subscribes to.
|
|
50
|
+
Topic keywords the agent subscribes to. During an `update-knowledge` refresh, nodes matching these keywords are included automatically — even if they are not directly tagged for the agent.
|
|
51
51
|
|
|
52
52
|
Maximum 5 keywords. Present during creation as a free-text input. Examples: `["climate science", "sustainability"]`, `["pricing", "services"]`.
|
|
53
53
|
|
|
@@ -113,7 +113,14 @@ SOUL.md answers "what does this agent feel like to talk to?" — tone, warmth, f
|
|
|
113
113
|
|
|
114
114
|
## KNOWLEDGE.md Population
|
|
115
115
|
|
|
116
|
-
KNOWLEDGE.md is the public agent's
|
|
116
|
+
KNOWLEDGE.md is the public agent's baked, static knowledge surface — the facts the agent should always have on hand without a tool call. It is assembled into the system prompt at invocation time.
|
|
117
|
+
|
|
118
|
+
Public agents run on the same native Claude Code PTY as the admin, dispatched via `dispatchOnce` with `role: 'public'`. The agent's `liveMemory` config toggle decides whether `memory-search` lands in the per-spawn `--allowed-tools` allowlist:
|
|
119
|
+
|
|
120
|
+
- **`liveMemory: false`** — KNOWLEDGE.md is the agent's only knowledge source. No graph access mid-conversation. Every gap in KNOWLEDGE.md is a question the agent cannot answer.
|
|
121
|
+
- **`liveMemory: true`** — KNOWLEDGE.md remains the baked, evergreen surface, and `memory-search` is also in the allowlist so the agent can hit the graph at message time. The memory MCP server runs with `ALLOWED_SCOPES=public` so only `scope: "public"` nodes come back; anything not marked public is invisible regardless of how the agent queries.
|
|
122
|
+
|
|
123
|
+
When `liveMemory: true`, treat KNOWLEDGE.md and `memory-search` as complementary, not alternatives. KNOWLEDGE.md owns the agent's evergreen facts (pricing, services, FAQs, contact info). `memory-search` is for the long-tail, public-scoped lookups the agent reaches for during a conversation (a named listing, a specific person, a current availability slot).
|
|
117
124
|
|
|
118
125
|
All `memory-search` queries for knowledge population use admin permissions — do not pass `agentSlug`. The `agentSlug` parameter is a runtime enforcement mechanism for the public agent's MCP server, not an admin-time discovery tool. At creation time, the agent may have zero tagged nodes — searching with `agentSlug` would return nothing.
|
|
119
126
|
|
|
@@ -175,7 +182,7 @@ After creation, no template metadata persists in the agent's files. The resultin
|
|
|
175
182
|
5. Present `IDENTITY.md` via document-editor with `filePath: "agents/{slug}/IDENTITY.md"` (draft from role description). Write on approval.
|
|
176
183
|
6. Present `SOUL.md` via document-editor with `filePath: "agents/{slug}/SOUL.md"` (draft personality from user conversation). Write on approval.
|
|
177
184
|
7. Present agent configuration via a single `form` component with these fields:
|
|
178
|
-
- `liveMemory` (`checkbox`): label "Live memory", description explaining the trade-off —
|
|
185
|
+
- `liveMemory` (`checkbox`): label "Live memory", description explaining the trade-off — when on, the agent can call `memory-search` mid-turn against public-scoped graph content; leave off if KNOWLEDGE.md fully covers the agent's role
|
|
179
186
|
- `knowledgeKeywords` (`text`): label "Knowledge keywords (max 5, comma-separated)", placeholder with examples relevant to the agent's role
|
|
180
187
|
- `model` (`select`): label "Model", options with descriptions:
|
|
181
188
|
- `claude-haiku-4-5` — "Haiku — Fast, cost-effective. FAQ, routing, high-volume."
|
|
@@ -265,9 +272,9 @@ The new default takes effect on the next page load. The previous default agent r
|
|
|
265
272
|
|
|
266
273
|
Invoke the agent with `[New session. Greet the visitor.]` and stream the response to the admin chat. The admin sees exactly what a visitor would see on first load.
|
|
267
274
|
|
|
268
|
-
##
|
|
275
|
+
## Runtime Surface
|
|
269
276
|
|
|
270
|
-
Public agents run on the same native Claude Code PTY
|
|
277
|
+
Public agents run on the same native Claude Code PTY as the admin, dispatched via `dispatchOnce` with `role: 'public'`. They have no filesystem access, no MCP write tools, no subagents — every tool is on a read-only, public-safe allowlist that the manager passes to the spawn as `--allowed-tools`. The agent's `liveMemory` config flag decides whether `memory-search` is in that allowlist; when present, the memory MCP server runs with `ALLOWED_SCOPES=public` so only `scope: "public"` graph nodes are reachable. Everything the agent needs to say comes from its directory files (assembled into the system prompt at invocation time) or from that public-safe tool surface.
|
|
271
278
|
|
|
272
279
|
## URL Routing
|
|
273
280
|
|
|
@@ -468,6 +468,111 @@ Removes the root-level systemd unit, `/etc/cloudflared/config.yml`, and the dupl
|
|
|
468
468
|
|
|
469
469
|
---
|
|
470
470
|
|
|
471
|
+
## Multi-ingress: adding hostnames for prototype services
|
|
472
|
+
|
|
473
|
+
Once the brand tunnel is up (admin + public + optionally SSH/SMB), additional service hostnames can be added without re-running the OAuth/cert/create-tunnel steps. This is the pattern the venture-studio `prototype-host` skill uses to expose hosted landing pages and wedge prototypes as `https://<surface>.<BRAND_HOSTNAME>`.
|
|
474
|
+
|
|
475
|
+
The flow is: append an ingress rule to `${CFG_DIR}/config.yml` immediately before the final `http_status:404` catch-all, validate the YAML, create the DNS row, restart the brand cloudflared service.
|
|
476
|
+
|
|
477
|
+
### 1. Append the ingress rule (awk, not sed)
|
|
478
|
+
|
|
479
|
+
`sed` cannot reliably insert before the last multi-line YAML rule. `awk` matches the literal catch-all line and inserts two lines before each match:
|
|
480
|
+
|
|
481
|
+
```
|
|
482
|
+
SURFACE="<surface>" # DNS label, e.g. landing or prototype
|
|
483
|
+
PORT="<port>" # local port the dev server / file server binds to
|
|
484
|
+
HOSTNAME="${SURFACE}.${BRAND_HOSTNAME}"
|
|
485
|
+
|
|
486
|
+
# Idempotency — skip if this hostname is already in config.yml.
|
|
487
|
+
if grep -qE "^[[:space:]]*-[[:space:]]+hostname:[[:space:]]+${HOSTNAME}\b" "${CFG_DIR}/config.yml"; then
|
|
488
|
+
echo "ingress-row-exists hostname=${HOSTNAME}"
|
|
489
|
+
else
|
|
490
|
+
awk -v host="${HOSTNAME}" -v port="${PORT}" '
|
|
491
|
+
/^[[:space:]]*-[[:space:]]+service:[[:space:]]+http_status:404[[:space:]]*$/ {
|
|
492
|
+
print " - hostname: " host
|
|
493
|
+
print " service: http://localhost:" port
|
|
494
|
+
}
|
|
495
|
+
{ print }
|
|
496
|
+
' "${CFG_DIR}/config.yml" > "${CFG_DIR}/config.yml.new"
|
|
497
|
+
mv "${CFG_DIR}/config.yml.new" "${CFG_DIR}/config.yml"
|
|
498
|
+
fi
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
After the edit the file looks like:
|
|
502
|
+
|
|
503
|
+
```yaml
|
|
504
|
+
tunnel: <TUNNEL_ID>
|
|
505
|
+
credentials-file: <CFG_DIR>/<TUNNEL_ID>.json
|
|
506
|
+
ingress:
|
|
507
|
+
- hostname: admin.<BRAND_HOSTNAME>
|
|
508
|
+
service: http://localhost:<PORT>
|
|
509
|
+
- hostname: public.<BRAND_HOSTNAME>
|
|
510
|
+
service: http://localhost:<PORT>
|
|
511
|
+
- hostname: <surface>.<BRAND_HOSTNAME>
|
|
512
|
+
service: http://localhost:<port>
|
|
513
|
+
- service: http_status:404
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### 2. Validate
|
|
517
|
+
|
|
518
|
+
```
|
|
519
|
+
cloudflared --origincert "${CFG_DIR}/cert.pem" \
|
|
520
|
+
--config "${CFG_DIR}/config.yml" \
|
|
521
|
+
tunnel ingress validate
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Non-zero exit aborts the flow; cloudflared's stderr is the literal diagnostic.
|
|
525
|
+
|
|
526
|
+
### 3. Create the DNS row
|
|
527
|
+
|
|
528
|
+
```
|
|
529
|
+
TUNNEL_ID=$(jq -r '.id // .tunnelId' "${CFG_DIR}/tunnel.state")
|
|
530
|
+
cloudflared --origincert "${CFG_DIR}/cert.pem" \
|
|
531
|
+
tunnel route dns --overwrite-dns "${TUNNEL_ID}" "${HOSTNAME}"
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
`--overwrite-dns` replaces any existing CNAME for the hostname (common after a prior install on the same account). Without it, `tunnel route dns` errors `record already exists`.
|
|
535
|
+
|
|
536
|
+
### 4. Reload the connector
|
|
537
|
+
|
|
538
|
+
```
|
|
539
|
+
systemctl --user restart "${BRAND}-cloudflared.service"
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Cloudflared does not watch `config.yml` for changes (the brand service unit launches it without `--config-watch`). Restart is the deterministic way to pick up the new rule; the brief ~1s connector blip is the same one every cloudflared upgrade incurs.
|
|
543
|
+
|
|
544
|
+
### Verification
|
|
545
|
+
|
|
546
|
+
```
|
|
547
|
+
curl -I https://<surface>.<BRAND_HOSTNAME>
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Anything other than `HTTP/2 200` (or the dev server's natural 200/304 response) means either the rule didn't land, the systemd unit serving `<port>` isn't running, or DNS hasn't propagated. Run `cloudflared --origincert "${CFG_DIR}/cert.pem" --config "${CFG_DIR}/config.yml" tunnel ingress validate` and `dig <surface>.<BRAND_HOSTNAME>` to isolate which.
|
|
551
|
+
|
|
552
|
+
### Removing a prototype hostname
|
|
553
|
+
|
|
554
|
+
Manual for now (no scripted removal — the `prototype-host` skill does not yet ship a remove companion):
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
# 1. Stop and disable the service unit.
|
|
558
|
+
systemctl --user disable --now "${BRAND}-prototype-<surface>.service"
|
|
559
|
+
|
|
560
|
+
# 2. Strip the ingress row from config.yml — match the literal hostname line plus the following service line.
|
|
561
|
+
awk -v host="${SURFACE}.${BRAND_HOSTNAME}" '
|
|
562
|
+
$0 ~ "^[[:space:]]*-[[:space:]]+hostname:[[:space:]]+" host "[[:space:]]*$" { skip = 2; next }
|
|
563
|
+
skip > 0 { skip--; next }
|
|
564
|
+
{ print }
|
|
565
|
+
' "${CFG_DIR}/config.yml" > "${CFG_DIR}/config.yml.new" && mv "${CFG_DIR}/config.yml.new" "${CFG_DIR}/config.yml"
|
|
566
|
+
|
|
567
|
+
# 3. Delete the DNS row (dashboard, since the cloudflared CLI has no "route dns remove").
|
|
568
|
+
# Cloudflare → DNS → <zone> → delete CNAME for <surface>.<BRAND_HOSTNAME>.
|
|
569
|
+
|
|
570
|
+
# 4. Restart cloudflared.
|
|
571
|
+
systemctl --user restart "${BRAND}-cloudflared.service"
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
471
576
|
## Full reset
|
|
472
577
|
|
|
473
578
|
If everything is in a bad state and you want to start over. Assumes Step 0 has been run so `${BRAND}` and `${CFG_DIR}` are set.
|
|
@@ -9,15 +9,33 @@
|
|
|
9
9
|
|
|
10
10
|
## macOS install
|
|
11
11
|
|
|
12
|
-
On macOS the installer uses Homebrew + launchd instead of apt + systemd
|
|
12
|
+
On macOS the installer uses Homebrew + launchd instead of apt + systemd. No flags are required on a laptop:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
npx -y @rubytech/create-maxy
|
|
15
|
+
npx -y @rubytech/create-maxy-code # default brand
|
|
16
|
+
npx -y @rubytech/create-realagent-code # realagent brand
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
**Prerequisite:** Homebrew. If `brew` is missing, the installer refuses with `Homebrew not found. Install from https://brew.sh and re-run.` Install Homebrew once via the official one-liner, then re-run.
|
|
19
|
+
**Prerequisite:** Homebrew. If `brew` is missing, the installer refuses with `Homebrew not found. Install from https://brew.sh and re-run.` Install Homebrew once via the official one-liner, then re-run. The installer never installs Homebrew itself.
|
|
20
|
+
|
|
21
|
+
**Hostname / printed URL.** Without `--hostname`, the installer reads `scutil --get LocalHostName` and prints the completion URL as `http://<that-name>.local:<port>`. No sudo, no system change — your Mac's existing local network name is what mDNS will resolve. With `--hostname <h>`, the installer sets `HostName` / `LocalHostName` / `ComputerName` to `<h>` via `sudo scutil` (one password prompt, all-or-nothing rollback within the three-call batch) so the URL becomes `http://<h>.local:<port>`. Grep `~/.<brand>/logs/install-*.log` for `[create-maxy] darwin-hostname-mode=` to confirm which path ran (`scutil-get`, `scutil-set`, or `brand-fallback`).
|
|
22
|
+
|
|
23
|
+
**LaunchAgent.** The installer registers `{{productName}}` as a launchd LaunchAgent at `~/Library/LaunchAgents/com.rubytech.<brand-hostname>.plist` — for example `com.rubytech.maxy-code.plist`. Survives logout/login and reboot via `KeepAlive=true` and `RunAtLoad=true`. Two brands on the same Mac get two distinct plists (brand-hostname-keyed), so install order is independent. Use `launchctl print gui/$UID/com.rubytech.<brand-hostname>` for service state. The `[create-maxy] launchd-plist=<path> loaded=true` line in the install log confirms `launchctl bootstrap` accepted the plist; `loaded=false exit=<n>` is the failure signal (run `plutil -lint <path>` to diagnose).
|
|
24
|
+
|
|
25
|
+
**Cloudflare on darwin.** The installer brew-installs the `cloudflared` binary so it is on PATH, but does not invoke `cloudflared service install` or `cloudflared tunnel route dns` — public reach is opt-in. After install, the operator runs `cloudflared tunnel login` (browser-driven) followed by the existing tunnel-setup flow if they want a public address. Grep `[create-maxy] darwin-cloudflare-skip=true` in the install log to confirm the installer took the documented skip path.
|
|
26
|
+
|
|
27
|
+
**Uninstall.** `npx -y @rubytech/create-maxy-code uninstall` (or the realagent equivalent) bootsout the LaunchAgent, removes the plist, and deletes `~/.<brand>/`. Homebrew-installed dependencies (curl, git, unzip, jq, poppler, ffmpeg, node@22, neo4j, cloudflared) remain — remove them with `brew uninstall` if you want a clean slate.
|
|
28
|
+
|
|
29
|
+
**Pre-flight.** macOS < 14 is refused at pre-flight via `parseSwVers` (`sw_vers -productVersion` must be `≥ 14`).
|
|
30
|
+
|
|
31
|
+
**Diagnostic grep recipe.** After a Mac install, the canonical log path is `~/.<brand>/logs/install-*.log`. One pass tells you everything:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
grep -E '^\[create-maxy\] (platform|darwin-hostname-mode|darwin-cloudflare-skip|launchd-plist|init-logging FAILED)=' \
|
|
35
|
+
~/.<brand>/logs/install-*.log
|
|
36
|
+
```
|
|
19
37
|
|
|
20
|
-
|
|
38
|
+
Every successful Mac install contains, in order: `platform=darwin`, `darwin-hostname-mode=…`, `darwin-cloudflare-skip=true`, every brew install/verify line, `launchd-plist=… loaded=true`. Absence of any of these is the failure signal.
|
|
21
39
|
|
|
22
40
|
## Initial Setup
|
|
23
41
|
|
|
@@ -197,13 +197,7 @@ When `search` is true and `query` is non-null, the rewritten query replaces the
|
|
|
197
197
|
|
|
198
198
|
### Knowledge retrieval gate
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
| Agent config | Classifier runs? | Search runs? |
|
|
203
|
-
|---|---|---|
|
|
204
|
-
| `liveMemory: true` | No | Always — the admin opted into live knowledge and the classifier cannot know what the graph contains. ~200ms cost is negligible vs. cost of a miss. |
|
|
205
|
-
| `knowledgeBaked: true`, `liveMemory: false` | No | Never — KNOWLEDGE.md is the sole knowledge source. |
|
|
206
|
-
| `knowledgeBaked: false` | Yes | Only if classifier says `search: true` (or classifier fails → fallback to always-search). |
|
|
200
|
+
On the public PTY surface the agent itself decides when to call `memory-search` — there is no server-side classifier interposed between the user message and the agent's first tool call. KNOWLEDGE.md (when present) is assembled into the agent's system prompt at spawn time. Whether `memory-search` is reachable at message time is controlled by the agent's `liveMemory` config flag: when `true`, the per-spawn allowlist includes `memory-search` and reads run with `ALLOWED_SCOPES=public`; when `false`, the agent has no graph access mid-turn.
|
|
207
201
|
|
|
208
202
|
### Observability
|
|
209
203
|
|
|
@@ -325,10 +319,7 @@ Results from keyword subscription searches are merged into the same scored map a
|
|
|
325
319
|
|
|
326
320
|
### Lifecycle
|
|
327
321
|
|
|
328
|
-
|
|
329
|
-
|---|---|
|
|
330
|
-
| `liveMemory: true` | Keywords are passed as `KNOWLEDGE_KEYWORDS` env var to the memory MCP server at runtime. Every query includes keyword subscription searches alongside the primary search. |
|
|
331
|
-
| `liveMemory: false` | Keywords are used by the `update-knowledge` admin skill when regenerating KNOWLEDGE.md. The regeneration queries the graph with both direct tags and keyword subscriptions. |
|
|
322
|
+
Keywords are consumed by the `update-knowledge` admin skill when regenerating KNOWLEDGE.md — the regeneration query broadens the operator-tagged set with keyword matches so newly-added graph content that shares a subscribed topic lands in the next baked snapshot. There is no runtime keyword-injection path on the public PTY surface.
|
|
332
323
|
|
|
333
324
|
---
|
|
334
325
|
|
|
@@ -368,23 +359,12 @@ The final step in the retrieval pipeline is injecting retrieved content into the
|
|
|
368
359
|
|
|
369
360
|
### Public agent paths
|
|
370
361
|
|
|
371
|
-
|
|
362
|
+
Public agents run on the same native Claude Code PTY surface as the admin, dispatched through the channel PTY-bridge with `role: 'public'`. The agent's directory files (IDENTITY.md, SOUL.md, KNOWLEDGE.md, KNOWLEDGE-SUMMARY.md when present) are assembled into the system prompt at spawn time. There is no per-turn server-side knowledge injection.
|
|
372
363
|
|
|
373
|
-
|
|
374
|
-
- KNOWLEDGE.md content is embedded directly in the system prompt as `<knowledge>...</knowledge>`
|
|
375
|
-
- No runtime search. No classifier. No memory MCP server call.
|
|
376
|
-
- KNOWLEDGE.md quality is the ceiling of the agent's factual knowledge.
|
|
364
|
+
Two paths, selected by the agent's `liveMemory` config flag:
|
|
377
365
|
|
|
378
|
-
|
|
379
|
-
- KNOWLEDGE.md
|
|
380
|
-
- Runtime search also runs (always, bypassing classifier)
|
|
381
|
-
- Results appended as `<live-memory>...</live-memory>`
|
|
382
|
-
- Agent sees both: curated knowledge file + fresh graph results
|
|
383
|
-
|
|
384
|
-
**Path 3 — Memory-search only** (`knowledgeBaked: false`):
|
|
385
|
-
- No KNOWLEDGE.md exists (e.g., on a fresh install before the operator has had any conversations)
|
|
386
|
-
- Classifier decides whether to search (or falls back to always-search)
|
|
387
|
-
- Results injected as `<memory>...</memory>`
|
|
366
|
+
- **`liveMemory: false`** — `memory-search` is excluded from the per-spawn `--allowed-tools` allowlist. The agent has no graph access mid-conversation; KNOWLEDGE.md is the ceiling of factual knowledge.
|
|
367
|
+
- **`liveMemory: true`** — `memory-search` is in the allowlist. The agent decides at message time whether to call it; reads run against the graph with `ALLOWED_SCOPES=public` so only public-scoped nodes return. KNOWLEDGE.md and the live `memory-search` surface are complementary — the baked file covers evergreen facts; the live tool covers the long-tail public-scoped lookups.
|
|
388
368
|
|
|
389
369
|
### KNOWLEDGE.md staleness guard
|
|
390
370
|
|
|
@@ -496,6 +476,10 @@ A second boot gate walks every specialist `.md` under `platform/templates/specia
|
|
|
496
476
|
|
|
497
477
|
This gate was Task 173. The `brand-excluded` branch closes the recurring crash-restart loop on brands that ship without a plugin the shared `personal-assistant.md` template references (e.g. `realagent-code` excludes `telegram` while the template hard-codes `mcp__telegram__*`). The brand-agnostic template stays a single file; the brand-aware filter expresses what the specialist *may* do on this install while the template expresses what it *can* do across brands. Tool typos and renamed plugins still refuse to boot — only namespaces explicitly named in `plugins.excluded` are demoted to strip-and-warn.
|
|
498
478
|
|
|
479
|
+
**Brand-foreign premium bundles (Task 343).** The same drift gate walks `premium-plugins/<bundle>/agents/` for every bundle present on disk. On brands whose installer ships every premium bundle but only intends to surface a subset, a foreign-bundle specialist file (e.g. `real-agent/listing-curator.md` on a `maxy-code` install) used to reach the drift assertion and refuse boot the moment any of its declared tools missed the active brand's tool surface — the `image-fetch` tool that registered at runtime but was undeclared in `memory/PLUGIN.md` produced exactly this loop. The manager resolves `brand.json#shipsPremiumBundles` into the same three modes `paths.ts` already enforces for the UI: `true` ships every premium bundle, a `string[]` ships only the named bundle directories, `false`/missing ships none (fail-closed). Premium bundles outside the resolved mode are dropped before drift, with one `agents-dir-skipped path=<p> reason=brand-foreign-bundle bundle=<n>` line per drop. When the premium root exists and the mode resolves to `none` (so every premium dir filters), one additional `brand-foreign-skip-all path=<premiumPluginsRoot> reason=shipsPremiumBundles-missing-or-empty premium-dirs-skipped=<N>` line surfaces the misconfig. `platform/plugins/` candidates are never filtered — brand-foreign is a premium-tree concept only.
|
|
480
|
+
|
|
481
|
+
**Structured journald mirror for boot-failed (Task 343).** Every `boot-failed reason=specialist-tool-drift …` line is mirrored to journald via `systemd-cat -t maxy-csm -p err` with the fields `specialist=`, `tool=`, `drift_reason=`, `agent_path=` so `journalctl --user -u <brand>-claude-session-manager.service -t maxy-csm` can filter by any of them without grep on `server.log`. The stdout line stays unchanged so the existing diagnostic one-liners keep working. `systemd-cat` absence (e.g. macOS dev box) is swallowed — the stdout line is the primary surface; the structured emit is auxiliary.
|
|
482
|
+
|
|
499
483
|
**Per-spawn signals (server.log).** Every spawn emits `pty-spawn-mcp-config servers=<N> tools=<M> bytes=<B> path=<…>` once, plus one `pty-spawn-agents-dir role=<admin|public> path=<…>` per added directory. Specialist spawns additionally emit `pty-spawn-allowlist specialist=<name> count=<N> stripped=<S> sourced-from=agent-frontmatter` where `stripped` is the count of brand-excluded tool names removed before argv emission. The diagnostic one-liner is `grep -E 'pty-spawn-mcp-config|pty-spawn-agents-dir|pty-spawn-allowlist|mcp-config-allowlist-coverage|specialist-tool-strip|boot-failed reason=' ~/.<brand>/logs/server.log | tail -50`.
|
|
500
484
|
|
|
501
485
|
**Brand-process start counter (Task 173).** `platform/ui/server-init.cjs` increments a persistent counter at `/tmp/server-init-<accountId>-restart.count` on every fresh start and emits `[server-init] start count=<N> account=<accountId> counter-path=<…>` to `server.log`. /tmp clears on reboot, so a clean reboot starts the count fresh; any value `>1` between operator-observed reboots means the brand process (driven by its `Requires=<brand>-claude-session-manager.service` clause) is restarting. The diagnostic one-liner is `grep '\[server-init\] start' ~/.<brand>/logs/server.log | tail -5` — the trailing `count=` value is the loop depth without counting SIGTERMs.
|