@rubytech/create-maxy-code 0.1.90 → 0.1.91

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/index.js CHANGED
@@ -3128,6 +3128,37 @@ WantedBy=multi-user.target
3128
3128
  spawnSync("sudo", ["loginctl", "enable-linger", currentUser], { stdio: "inherit" });
3129
3129
  }
3130
3130
  catch { /* not critical */ }
3131
+ // Task 249: persist the systemd journal so per-unit lifecycle events
3132
+ // (watchdog timeouts, SIGTERM/SIGKILL transitions, restart reasons) survive
3133
+ // reboots and are queryable via `journalctl --user -u <brand>.service`.
3134
+ //
3135
+ // Bookworm-canonical mechanism: journald uses persistent storage when
3136
+ // `/var/log/journal/<machine-id>/` exists. No user-level
3137
+ // systemd-journald.service exists by default on Debian; the system instance
3138
+ // stores user-unit events in the same tree, filtered at read time. We only
3139
+ // restart journald when we just created the directory — if the path already
3140
+ // existed, persistence was already active and restarting would needlessly
3141
+ // churn the system journal during the install.
3142
+ try {
3143
+ const machineId = readFileSync("/etc/machine-id", "utf-8").trim();
3144
+ if (machineId) {
3145
+ const journalPath = `/var/log/journal/${machineId}`;
3146
+ const alreadyPersistent = existsSync(journalPath);
3147
+ if (!alreadyPersistent) {
3148
+ console.log(" [privileged] mkdir /var/log/journal");
3149
+ shell("mkdir", ["-p", journalPath], { sudo: true });
3150
+ console.log(" [privileged] systemctl restart systemd-journald");
3151
+ spawnSync("sudo", ["systemctl", "restart", "systemd-journald"], { stdio: "inherit", timeout: 15_000 });
3152
+ logFile(` journald persistence: enabled (created ${journalPath} + restarted journald)`);
3153
+ }
3154
+ else {
3155
+ logFile(` journald persistence: already active (${journalPath} present)`);
3156
+ }
3157
+ }
3158
+ }
3159
+ catch (err) {
3160
+ console.error(` WARNING: failed to enable journald persistence: ${err instanceof Error ? err.message : String(err)}`);
3161
+ }
3131
3162
  // Reload and (re)start.
3132
3163
  //
3133
3164
  // ordering: on upgrades, the old main brand service still holds
@@ -97,7 +97,7 @@ ExecStartPre=-/bin/bash ${o.installDir}/platform/scripts/resume-tunnel.sh
97
97
  ExecStart=/usr/bin/node --require ./server-init.cjs server.js
98
98
  Restart=on-failure
99
99
  RestartSec=5
100
- WatchdogSec=30
100
+ WatchdogSec=120
101
101
  TimeoutStartSec=60
102
102
  TimeoutStopSec=10
103
103
  Environment=NODE_ENV=production
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.90",
3
+ "version": "0.1.91",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -48,7 +48,7 @@ Optional:
48
48
 
49
49
  - `force` — one of `brand`, `property`, `all`, or absent. When set, the named upstream step runs even if its output already exists.
50
50
  - `output_root` — defaults to the **caller's current working directory** (`./`). The orchestrator creates `<output_root>/<brand_slug>/` for the brand pack and nests properties at `<brand_slug>/properties/<property_slug>-<id>/`. To place the workspace elsewhere — e.g. alongside an existing brand pack from a prior run — pass `output_root` explicitly (absolute path, or relative to CWD). The orchestrator never infers location from an existing brand pack; if you already have one and want this run to reuse it, point `brand_source` at its directory path (see Routing rule 1) AND pass `output_root` to its parent so the new property nests under the same workspace.
51
- - `register` — one of `premium`, `branded`, or absent. **Premium** keeps the template's super-premium tokens verbatim (only the agent's logo and contact details change); the brand pack's DESIGN.md palette/type are deliberately *not* applied. **Branded** re-skins the template with the brand's tokens. Absent = use whichever the user's wording implies (the word "premium" or "super-premium" in the request premium; presence of a DESIGN.md without that wording branded; neither premium defaults). The orchestrator passes the resolved register through to `property-brochure`. See `property-brochure → Default visual scheme — and when to override it` for the full contract.
51
+ - `register` — one of `premium`, `branded`, or absent. **Premium** keeps the template's super-premium tokens verbatim (only the agent's logo and contact details change); the brand pack's DESIGN.md palette/type are deliberately *not* applied. **Branded** re-skins the template with the brand's tokens. **Absent branded.** Resolve the register only AFTER brand-design has run, so DESIGN.md presence is a guaranteed state (brand-design is step 1 of this same pipeline; gating "branded" on a pre-existing DESIGN.md made the branch structurally unreachable on first runs and silently fell through to premium). Premium is **opt-in only** — the operator must say "premium" or "super-premium" in the request, otherwise branded. The orchestrator passes the resolved register through to `property-brochure`. See `property-brochure → Default visual scheme — and when to override it` for the full contract.
52
52
 
53
53
  If only one input is supplied, **decline and ask for the other** — the orchestrator's reason for being is having both. Route a single-input request to the appropriate sub-skill directly.
54
54
 
@@ -17,6 +17,7 @@ const { spawnSync } = require('node:child_process')
17
17
  const { existsSync, readFileSync, writeFileSync } = require('node:fs')
18
18
  const os = require('node:os')
19
19
  const path = require('node:path')
20
+ const { monitorEventLoopDelay } = require('node:perf_hooks')
20
21
 
21
22
  // ---------------------------------------------------------------------------
22
23
  // 0. Start counter — Task 173
@@ -124,10 +125,41 @@ if (NOTIFY_SOCKET) {
124
125
  console.error('[server-init] sd_notify READY=1 sent')
125
126
  })
126
127
 
127
- // Watchdog ping every 15s (WatchdogSec=30, ping at half-interval)
128
- const watchdogInterval = setInterval(() => sdNotify('WATCHDOG=1'), 15_000)
128
+ // Task 249: WATCHDOG_USEC is injected by systemd when Type=notify +
129
+ // WatchdogSec= is configured (microseconds, integer). Reading it at runtime
130
+ // means a future unit-file bump (e.g. 120 → 180) propagates without code
131
+ // changes. Ping every 15s regardless of deadline: trivially satisfies
132
+ // systemd's "≤ half-deadline" requirement for any deadline ≥ 30s, and
133
+ // gives the near-miss check 15s observation windows so a long block is
134
+ // surfaced within 15s of unblocking.
135
+ const WATCHDOG_USEC = Number.parseInt(process.env.WATCHDOG_USEC ?? '', 10)
136
+ const watchdogDeadlineMs = Number.isFinite(WATCHDOG_USEC) && WATCHDOG_USEC > 0
137
+ ? Math.floor(WATCHDOG_USEC / 1000)
138
+ : null
139
+
140
+ // monitorEventLoopDelay records every libuv tick's lag; a sync block of N
141
+ // seconds shows up as histogram.max ≈ N*1000 ms in the cycle where the
142
+ // block ends. When max crosses 50% of the deadline we emit one line so
143
+ // `grep '[server-init] watchdog ping miss' server.log` answers "did we
144
+ // approach the deadline, and how close" without needing systemd state or
145
+ // journald. Histogram is reset every cycle so max reflects this window.
146
+ // Only enabled when the deadline is known — no point recording samples no
147
+ // one reads.
148
+ const loopHist = watchdogDeadlineMs !== null ? monitorEventLoopDelay({ resolution: 50 }) : null
149
+ if (loopHist) loopHist.enable()
150
+
151
+ const watchdogInterval = setInterval(() => {
152
+ sdNotify('WATCHDOG=1')
153
+ if (loopHist && watchdogDeadlineMs !== null) {
154
+ const maxMs = loopHist.max / 1_000_000
155
+ loopHist.reset()
156
+ if (maxMs >= watchdogDeadlineMs / 2) {
157
+ console.error(`[server-init] watchdog ping miss lag-ms=${Math.round(maxMs)} deadline-ms=${watchdogDeadlineMs}`)
158
+ }
159
+ }
160
+ }, 15_000)
129
161
  watchdogInterval.unref() // don't keep the process alive for this
130
- console.error('[server-init] watchdog enabled (15s interval)')
162
+ console.error(`[server-init] watchdog enabled (ping-interval-ms=15000 deadline-ms=${watchdogDeadlineMs ?? 'unset'})`)
131
163
  } else {
132
164
  console.error('[server-init] no NOTIFY_SOCKET, watchdog disabled')
133
165
  }
@@ -601,6 +601,7 @@ var serveStatic = (options = { root: "" }) => {
601
601
  import { readFileSync as readFileSync20, existsSync as existsSync22, watchFile } from "fs";
602
602
  import { resolve as resolve23, join as join13, basename as basename5 } from "path";
603
603
  import { homedir as homedir4 } from "os";
604
+ import { monitorEventLoopDelay } from "perf_hooks";
604
605
 
605
606
  // app/lib/agent-slug-pattern.ts
606
607
  var AGENT_SLUG_PATTERN = /^\/([a-z][a-z0-9-]{2,49})$/;
@@ -14085,6 +14086,21 @@ var port = requirePortEnv("MAXY_UI_INTERNAL_PORT", { tag: "ui-server" });
14085
14086
  var hostname = process.env.HOSTNAME ?? "127.0.0.1";
14086
14087
  var httpServer = serve({ fetch: app37.fetch, port, hostname });
14087
14088
  console.log(`${BRAND.productName} listening on http://${hostname}:${port}`);
14089
+ {
14090
+ const loopHist = monitorEventLoopDelay({ resolution: 50 });
14091
+ loopHist.enable();
14092
+ const LOG_THRESHOLD_MS = 1e3;
14093
+ const WINDOW_MS2 = 3e4;
14094
+ const lagTimer = setInterval(() => {
14095
+ const maxMs = loopHist.max / 1e6;
14096
+ const p99Ms = loopHist.percentile(99) / 1e6;
14097
+ loopHist.reset();
14098
+ if (maxMs >= LOG_THRESHOLD_MS) {
14099
+ console.error(`[event-loop] lag p99=${Math.round(p99Ms)} max=${Math.round(maxMs)} window=${WINDOW_MS2 / 1e3}s`);
14100
+ }
14101
+ }, WINDOW_MS2);
14102
+ lagTimer.unref();
14103
+ }
14088
14104
  console.log("[boot] auth-mode summary: oauth=8 api-key=1 (api-key consumer: invokePublicAgent only)");
14089
14105
  var SUBAPP_MANIFEST = [
14090
14106
  { prefix: "/api/health", file: "server/routes/health.ts", subapp: health_default },