@rubytech/create-maxy-code 0.1.157 → 0.1.158

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.
@@ -1,16 +1,40 @@
1
1
  #!/usr/bin/env bash
2
2
  #
3
3
  # smoke-boot-services.sh — boot every compiled service against a synthetic
4
- # install fixture, assert the canonical `boot port=` log line appears and no
5
- # `^\[.*\] fatal ` line is emitted before SIGTERM.
4
+ # install fixture, run the cypher schema against a live Neo4j, and assert
5
+ # every startup-self-test the production boot would log fires here.
6
6
  #
7
- # Motivation: Task 099 `claude-session-manager` shipped a `require()` call
8
- # in an ESM module. tsc + vitest (Vite shims `require`) were green; native
9
- # Node ESM at boot threw `ReferenceError: require is not defined` and crash-
10
- # looped the service. This gate boots the compiled service from a temp
11
- # install dir — the same shape the installer writes — so the runtime
12
- # evaluator runs every module-load and arm()/init path the production
13
- # service runs.
7
+ # Failure modes caught (Task 438):
8
+ # - specialist-tool-drift: a specialist template (or per-account override)
9
+ # names a tool the plugin does not export.
10
+ # - system-prompt-sentinels: the composer drops a `<name>` the
11
+ # IDENTITY.md prose references.
12
+ # - identity-drift: IDENTITY.md mentions a `<name>` the composer does not
13
+ # emit.
14
+ # - cypher-schema: schema.cypher contains syntax the live Neo4j rejects
15
+ # (e.g. Neo4j 4 `DROP FULLTEXT INDEX` against a Neo4j 5 parser).
16
+ # - module-load: the original Task 099 motivation — `require()` in ESM,
17
+ # missing externals, stray top-level throws.
18
+ #
19
+ # Fixture shape: the temp install dir mirrors what the installer writes on
20
+ # first boot. Real templates are copied (not stubs) so the drift gates run
21
+ # against the same content production would see:
22
+ # $INSTALL_DIR/platform/config/brand.json
23
+ # $INSTALL_DIR/platform/plugins/<name>/PLUGIN.md (real manifests)
24
+ # $INSTALL_DIR/platform/templates/agents/admin/*.md (real templates)
25
+ # $INSTALL_DIR/platform/templates/specialists/agents/*.md (real templates)
26
+ # $INSTALL_DIR/data/accounts/<uuid>/account.json
27
+ # $INSTALL_DIR/data/accounts/<uuid>/agents/admin/{IDENTITY,SOUL,AGENTS}.md
28
+ # $INSTALL_DIR/data/accounts/<uuid>/specialists/agents/*.md (per-account)
29
+ # $INSTALL_DIR/.claude/ (CLAUDE_CONFIG_DIR)
30
+ #
31
+ # Cypher gate: apply schema.cypher to the dev's local Neo4j via cypher-shell.
32
+ # NEO4J_URI / NEO4J_USER / NEO4J_PASSWORD must be set (same shape as
33
+ # seed-neo4j.sh — the maintainer's existing local install). The gate is a
34
+ # real apply, not parse-only: schema commands use `IF NOT EXISTS` / `IF
35
+ # EXISTS` and the dev's Neo4j is the test surface — pollution is the point.
36
+ # If cypher-shell is absent or NEO4J_URI is unset, the gate exits non-zero
37
+ # so the publish blocker maps to the same code path a real install hits.
14
38
  #
15
39
  # Service list: extend SERVICES below as additional services adopt the
16
40
  # smoke gate. Each entry needs a fixture function that creates the temp
@@ -42,28 +66,28 @@ cleanup() {
42
66
  }
43
67
  trap cleanup EXIT
44
68
 
45
- # Build the synthetic install dir for claude-session-manager. The layout
46
- # mirrors what the installer writes (and what loadConfig walks):
47
- # $INSTALL_DIR/platform/config/brand.json
48
- # $INSTALL_DIR/platform/plugins/<name>/PLUGIN.md (real manifests — tool surface)
49
- # $INSTALL_DIR/platform/templates/specialists/agents/*.md (real shipped set)
50
- # $INSTALL_DIR/data/accounts/<uuid>/account.json
51
- # $INSTALL_DIR/data/accounts/<uuid>/agents/admin/{IDENTITY,SOUL}.md
69
+ # Build the synthetic install dir for claude-session-manager. Layout mirrors
70
+ # what the installer writes on first boot, with REAL template content (not
71
+ # stubs) so the drift gates fire against the same files production sees.
52
72
  #
53
- # Specialist templates and plugin PLUGIN.md files are copied from the real
54
- # source so assertSpecialistDrift runs against the actual shipped content.
55
- # An empty specialists dir or empty plugins dir both yield a trivial pass
56
- # (0 specialists inspected = no defects found), hiding tool-name mismatches
57
- # that only surface at production boot. Only PLUGIN.md manifests are copied;
58
- # MCP binaries are not needed — the drift check parses frontmatter only.
73
+ # Why real-templates: a 0-specialist or empty-IDENTITY fixture lets every
74
+ # drift gate fall through trivially (Task 438). The 0.1.155 install regressed
75
+ # because the librarian per-account override referenced an MCP tool the
76
+ # memory plugin no longer exports a defect the smoke fixture missed because
77
+ # the fixture's per-account specialists dir was empty.
59
78
  fixture_claude_session_manager() {
60
79
  local install_dir="$1"
61
80
  local platform_root="$install_dir/platform"
81
+ local templates_src="$REPO_ROOT/platform/templates"
62
82
  local account_id="00000000-0000-4000-8000-000000000000"
63
83
  local account_dir="$install_dir/data/accounts/$account_id"
64
84
 
65
85
  mkdir -p "$platform_root/config"
66
86
  mkdir -p "$account_dir/agents/admin"
87
+ mkdir -p "$account_dir/agents/public"
88
+ mkdir -p "$account_dir/specialists/agents"
89
+ mkdir -p "$account_dir/.claude"
90
+ mkdir -p "$install_dir/.claude"
67
91
 
68
92
  cat > "$platform_root/config/brand.json" <<'BRAND'
69
93
  {
@@ -72,20 +96,39 @@ fixture_claude_session_manager() {
72
96
  }
73
97
  BRAND
74
98
 
75
- echo '{}' > "$account_dir/account.json"
76
- # IDENTITY.md references no `<sentinels>`, so runIdentityDriftAssertion has
77
- # nothing to verify and trivially passes. SOUL.md is read per-spawn (not at
78
- # boot) but the file presence is convention.
79
- echo "smoke identity" > "$account_dir/agents/admin/IDENTITY.md"
80
- echo "smoke soul" > "$account_dir/agents/admin/SOUL.md"
81
-
82
- # Real specialist templates — the drift check looks at
83
- # $PLATFORM_ROOT/templates/specialists/agents/; an absent or empty dir
84
- # means 0 specialists inspected and the drift gate is a no-op.
85
- local specialists_src="$REPO_ROOT/packages/create-maxy-code/payload/platform/templates/specialists/agents"
86
- local specialists_dst="$platform_root/templates/specialists/agents"
87
- mkdir -p "$specialists_dst"
88
- cp "$specialists_src"/*.md "$specialists_dst/"
99
+ # account.json field set mirrors the live template's required fields
100
+ # (resolveAccount throws on missing adminModel/publicModel). Values just
101
+ # need to satisfy the field check boot never hits Anthropic.
102
+ cat > "$account_dir/account.json" <<JSON
103
+ {
104
+ "accountId": "$account_id",
105
+ "tier": "smoke-test",
106
+ "adminModel": "claude-sonnet-4-6",
107
+ "publicModel": "claude-haiku-4-5"
108
+ }
109
+ JSON
110
+
111
+ # Real admin templates — the drift gate reads the per-account IDENTITY.md;
112
+ # SOUL/AGENTS are not boot-gated but presence is convention.
113
+ cp "$templates_src/agents/admin/IDENTITY.md" "$account_dir/agents/admin/IDENTITY.md"
114
+ cp "$templates_src/agents/admin/SOUL.md" "$account_dir/agents/admin/SOUL.md"
115
+ [ -f "$templates_src/agents/admin/AGENTS.md" ] && cp "$templates_src/agents/admin/AGENTS.md" "$account_dir/agents/admin/AGENTS.md"
116
+ [ -f "$templates_src/agents/public/IDENTITY.md" ] && cp "$templates_src/agents/public/IDENTITY.md" "$account_dir/agents/public/IDENTITY.md"
117
+
118
+ # Real per-account specialist overrides — the drift gate walks
119
+ # $accountDir/specialists/agents/ alongside the template path. 0.1.155
120
+ # regressed because this dir was empty in the smoke fixture; a per-account
121
+ # override referencing a withdrawn tool slipped through to a real install.
122
+ for specialist in "$templates_src/specialists/agents/"*.md; do
123
+ [ -f "$specialist" ] && cp "$specialist" "$account_dir/specialists/agents/$(basename "$specialist")"
124
+ done
125
+
126
+ # Real templates copied into $PLATFORM_ROOT/templates so the drift gate's
127
+ # template-path walk sees the same content as production.
128
+ mkdir -p "$platform_root/templates/agents/admin"
129
+ mkdir -p "$platform_root/templates/specialists/agents"
130
+ cp "$templates_src/agents/admin/"*.md "$platform_root/templates/agents/admin/" 2>/dev/null || true
131
+ cp "$templates_src/specialists/agents/"*.md "$platform_root/templates/specialists/agents/"
89
132
 
90
133
  # Real plugin manifests — loadToolSurface walks platform/plugins/*/PLUGIN.md
91
134
  # to build the tool surface. An empty plugins dir gives an empty `all` set,
@@ -129,21 +172,34 @@ run_one() {
129
172
 
130
173
  # Spawn the service. PLATFORM_ROOT is the synthetic platform dir; loadConfig
131
174
  # walks $(dirname PLATFORM_ROOT)/data/accounts for the account dir.
175
+ # CLAUDE_CONFIG_DIR + NEO4J_URI + NEO4J_PASSWORD are stamped to satisfy the
176
+ # manager's env-presence checks at boot. NEO4J values are dummies — the
177
+ # manager only asserts non-empty here, it does not connect. The cypher-
178
+ # schema gate runs separately against the dev's real Neo4j.
132
179
  PLATFORM_ROOT="$install_dir/platform" \
133
180
  CLAUDE_SESSION_MANAGER_PORT="$port" \
134
181
  CLAUDE_SESSION_MANAGER_PERSIST_DIR="$install_dir/persist" \
182
+ CLAUDE_CONFIG_DIR="$install_dir/.claude" \
183
+ NEO4J_URI="bolt://127.0.0.1:65535" \
184
+ NEO4J_PASSWORD="smoke" \
135
185
  node "$dist_path" >"$log_file" 2>&1 &
136
186
  local pid=$!
137
187
 
138
- # Poll up to 5s for either the success line or a fatal line.
188
+ # Poll up to 10s for the final readiness signal (identity-drift line — last
189
+ # startup-self-test logged) or any fast-fail line. The boot port= line fires
190
+ # before self-tests; using it as the kill trigger lost the drift gates'
191
+ # output (Task 438). identity-drift=ok|fail is the last log line on the
192
+ # self-test path; boot-failed reason= is the fast-fail surface.
139
193
  local boot_line=""
140
194
  local fatal_line=""
195
+ local readiness_line=""
141
196
  local waited_ms=0
142
- while [ "$waited_ms" -lt 5000 ]; do
197
+ while [ "$waited_ms" -lt 10000 ]; do
143
198
  if [ -s "$log_file" ]; then
199
+ readiness_line="$(grep -m1 -E 'startup-self-test identity-drift=' "$log_file" || true)"
144
200
  boot_line="$(grep -m1 -E '^\[claude-session-manager\] boot port=' "$log_file" || true)"
145
- fatal_line="$(grep -m1 -E '^\[.*\] fatal ' "$log_file" || true)"
146
- if [ -n "$boot_line" ] || [ -n "$fatal_line" ]; then break; fi
201
+ fatal_line="$(grep -m1 -E '^\[.*\] fatal |boot-failed reason=' "$log_file" || true)"
202
+ if [ -n "$readiness_line" ] || [ -n "$fatal_line" ]; then break; fi
147
203
  fi
148
204
  if ! kill -0 "$pid" 2>/dev/null; then break; fi
149
205
  sleep 0.1
@@ -182,6 +238,17 @@ run_one() {
182
238
  cat "$log_file" >&2
183
239
  return 1
184
240
  fi
241
+ # boot-failed lines mean the manager rejected a startup-self-test before
242
+ # printing the boot line. Surface the gate-specific reason verbatim — same
243
+ # signal a real device would print to journald.
244
+ local boot_failed_line
245
+ boot_failed_line="$(grep -m1 -E 'boot-failed reason=' "$log_file" || true)"
246
+ if [ -n "$boot_failed_line" ]; then
247
+ echo "[smoke] service=$name reason=boot-failed matched=$boot_failed_line" >&2
248
+ echo "--- log ($log_file) ---" >&2
249
+ cat "$log_file" >&2
250
+ return 1
251
+ fi
185
252
  if [ -z "$boot_line" ]; then
186
253
  echo "[smoke] service=$name reason=no-boot-line matched=" >&2
187
254
  echo "--- log ($log_file) ---" >&2
@@ -197,7 +264,86 @@ run_one() {
197
264
  return 1
198
265
  fi
199
266
 
200
- echo "[smoke] service=$name ok boot-line=\"$boot_line\" elapsed-ms=$elapsed_ms"
267
+ # Assert every startup-self-test the manager would log on a real boot fired
268
+ # here. The fixture-from-real-templates change above is what makes these
269
+ # gates non-trivial — without the per-account specialists/IDENTITY copies,
270
+ # the drift gates would log =ok against an empty surface.
271
+ local required_tests=(
272
+ "startup-self-test specialist-tool-drift=ok"
273
+ "startup-self-test system-prompt-sentinels=ok"
274
+ "startup-self-test identity-drift=ok"
275
+ )
276
+ for test_line in "${required_tests[@]}"; do
277
+ if ! grep -qF "$test_line" "$log_file"; then
278
+ echo "[smoke] service=$name reason=startup-self-test-missing expected=\"$test_line\"" >&2
279
+ echo "--- log ($log_file) ---" >&2
280
+ cat "$log_file" >&2
281
+ return 1
282
+ fi
283
+ done
284
+
285
+ echo "[smoke-boot] service=$name specialist-tool-drift=ok system-prompt-sentinels=ok identity-drift=ok boot=ok elapsed-ms=$elapsed_ms"
286
+ return 0
287
+ }
288
+
289
+ # Apply schema.cypher to the dev's local Neo4j via cypher-shell. Catches
290
+ # Neo4j 4/5 syntax drift (e.g. 0.1.151's `DROP FULLTEXT INDEX`) the same way
291
+ # a real `seed-neo4j.sh` apply would. The dev's database is the test surface;
292
+ # schema commands use IF NOT EXISTS / IF EXISTS so re-runs are idempotent.
293
+ #
294
+ # Hard prerequisite: NEO4J_URI (and a password source) must be set, and
295
+ # cypher-shell must be on PATH. Absence = publish refusal — the maintainer
296
+ # already runs Neo4j locally for their own brand install.
297
+ run_cypher_schema() {
298
+ local schema_path="$REPO_ROOT/platform/neo4j/schema.cypher"
299
+ if [ ! -f "$schema_path" ]; then
300
+ echo "[smoke-boot] cypher-schema=fail err=schema-missing path=$schema_path" >&2
301
+ return 1
302
+ fi
303
+
304
+ if ! command -v cypher-shell >/dev/null 2>&1; then
305
+ echo "[smoke-boot] cypher-schema=fail err=cypher-shell-not-on-path" >&2
306
+ echo "[smoke-boot] cypher-schema requires a local Neo4j (cypher-shell on PATH) — same toolchain seed-neo4j.sh uses." >&2
307
+ return 1
308
+ fi
309
+
310
+ if [ -z "${NEO4J_URI:-}" ]; then
311
+ echo "[smoke-boot] cypher-schema=fail err=neo4j-uri-unset env=NEO4J_URI" >&2
312
+ return 1
313
+ fi
314
+
315
+ local neo4j_user="${NEO4J_USER:-neo4j}"
316
+ local neo4j_password="${NEO4J_PASSWORD:-}"
317
+ if [ -z "$neo4j_password" ]; then
318
+ # Fall back to the same password-file convention seed-neo4j.sh uses.
319
+ local pw_file="$REPO_ROOT/platform/config/.neo4j-password"
320
+ if [ -f "$pw_file" ]; then
321
+ neo4j_password="$(cat "$pw_file")"
322
+ fi
323
+ fi
324
+ if [ -z "$neo4j_password" ]; then
325
+ echo "[smoke-boot] cypher-schema=fail err=neo4j-password-unset env=NEO4J_PASSWORD" >&2
326
+ return 1
327
+ fi
328
+
329
+ local log_file="$TMP_ROOT/cypher-schema.log"
330
+ local statement_count
331
+ # Count CREATE/DROP/CALL statement-starts. Approximate but stable signal
332
+ # for the log line; the real authority is cypher-shell's exit code.
333
+ statement_count="$(grep -cE '^(CREATE|DROP|CALL)\b' "$schema_path" || true)"
334
+
335
+ if ! cypher-shell -u "$neo4j_user" -p "$neo4j_password" -a "$NEO4J_URI" \
336
+ --non-interactive --fail-fast --format plain < "$schema_path" \
337
+ > "$log_file" 2>&1; then
338
+ local first_err
339
+ first_err="$(grep -m1 -E '^(Invalid|Error|Neo\.)' "$log_file" || head -1 "$log_file" || true)"
340
+ echo "[smoke-boot] cypher-schema=fail err=${first_err:-unknown}" >&2
341
+ echo "--- log ($log_file) ---" >&2
342
+ cat "$log_file" >&2
343
+ return 1
344
+ fi
345
+
346
+ echo "[smoke-boot] cypher-schema=ok statements=$statement_count uri=$NEO4J_URI"
201
347
  return 0
202
348
  }
203
349
 
@@ -259,4 +405,8 @@ for entry in "${MCP_PLUGINS[@]}"; do
259
405
  fi
260
406
  done
261
407
 
408
+ if ! run_cypher_schema; then
409
+ rc=1
410
+ fi
411
+
262
412
  exit "$rc"