voidforge-build 23.11.3 → 23.12.0

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 (42) hide show
  1. package/dist/.claude/agents/batman-qa.md +1 -0
  2. package/dist/.claude/agents/galadriel-frontend.md +2 -0
  3. package/dist/.claude/agents/kusanagi-devops.md +4 -0
  4. package/dist/.claude/agents/lucius-config.md +6 -0
  5. package/dist/.claude/agents/silver-surfer-herald.md +11 -4
  6. package/dist/.claude/commands/architect.md +9 -0
  7. package/dist/.claude/commands/assemble.md +4 -1
  8. package/dist/.claude/commands/assess.md +13 -1
  9. package/dist/.claude/commands/audit-docs.md +106 -0
  10. package/dist/.claude/commands/deploy.md +28 -0
  11. package/dist/.claude/commands/engage.md +2 -0
  12. package/dist/.claude/commands/gauntlet.md +23 -4
  13. package/dist/.claude/commands/imagine.md +15 -0
  14. package/dist/.claude/commands/ux.md +32 -0
  15. package/dist/.claude/commands/void.md +1 -0
  16. package/dist/CHANGELOG.md +68 -0
  17. package/dist/CLAUDE.md +9 -0
  18. package/dist/VERSION.md +3 -1
  19. package/dist/docs/methods/AI_INTELLIGENCE.md +33 -0
  20. package/dist/docs/methods/ASSEMBLER.md +31 -2
  21. package/dist/docs/methods/BUILD_PROTOCOL.md +1 -0
  22. package/dist/docs/methods/CAMPAIGN.md +31 -3
  23. package/dist/docs/methods/DEVOPS_ENGINEER.md +158 -0
  24. package/dist/docs/methods/DOC_AUDIT.md +92 -0
  25. package/dist/docs/methods/FORGE_KEEPER.md +16 -5
  26. package/dist/docs/methods/GAUNTLET.md +33 -0
  27. package/dist/docs/methods/PRODUCT_DESIGN_FRONTEND.md +54 -0
  28. package/dist/docs/methods/QA_ENGINEER.md +20 -0
  29. package/dist/docs/methods/RELEASE_MANAGER.md +27 -0
  30. package/dist/docs/methods/SUB_AGENTS.md +31 -0
  31. package/dist/docs/methods/SYSTEMS_ARCHITECT.md +13 -0
  32. package/dist/docs/methods/TESTING.md +19 -0
  33. package/dist/docs/patterns/README.md +3 -0
  34. package/dist/docs/patterns/ai-eval.ts +63 -0
  35. package/dist/docs/patterns/autonomous-ops-triage-policy.md +102 -0
  36. package/dist/docs/patterns/daemon-process.ts +90 -0
  37. package/dist/docs/patterns/deploy-preflight.ts +85 -2
  38. package/dist/docs/patterns/design-tokens.ts +338 -0
  39. package/dist/docs/patterns/error-message-categorization.tsx +376 -0
  40. package/dist/wizard/lib/patterns/daemon-process.d.ts +2 -1
  41. package/dist/wizard/lib/patterns/daemon-process.js +89 -1
  42. package/package.json +2 -2
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Pattern: Error Message Categorization at the UI Boundary
3
+ *
4
+ * Key principles:
5
+ * - Categorize the error BEFORE selecting user-facing copy. The copy you show
6
+ * is a function of the error CATEGORY, never of where in the call stack the
7
+ * catch happened. A quota/billing failure must never render "try a different
8
+ * file" messaging just because it surfaced inside the upload flow.
9
+ * - Classification is driven by two signals: the HTTP status code AND the error
10
+ * SHAPE (machine-readable `code`/`type` fields, Retry-After header, well-known
11
+ * message fragments). Status alone is ambiguous (429 = rate limit OR quota),
12
+ * so we inspect the shape to disambiguate.
13
+ * - Every category maps to copy that is honest and actionable: tell the user
14
+ * what actually happened and what they can do about it. Quota → "you've hit
15
+ * your plan limit, upgrade or wait for reset", not "something went wrong".
16
+ * - Unknown/unclassifiable errors fall back to a safe generic category. We
17
+ * never guess a specific category we can't justify from the signals.
18
+ *
19
+ * Agents: Bilbo (copy), Legolas (code), Samwise (a11y), Stark (error shapes)
20
+ *
21
+ * Framework adaptations:
22
+ * Next.js/React: This file (classify() + copy map + hook + component)
23
+ * Vue/Svelte: Same classify() + COPY map; render via the framework's
24
+ * conditional blocks. The categorization logic is framework-agnostic —
25
+ * only the rendering changes.
26
+ * Django/Rails (server-rendered): Run classify() server-side on the caught
27
+ * exception, pass the category + copy into the template context, render the
28
+ * matching block. Same union, same map.
29
+ *
30
+ * Why this pattern exists:
31
+ * (Field report #343 F8: an upload component caught a 402 billing/quota error
32
+ * from the storage backend and rendered "try a different file" because its
33
+ * only error branch was shaped for validation failures. The user re-uploaded
34
+ * the same file five times before contacting support. The copy was selected
35
+ * by call site, not by error category. Categorize first, then choose copy.)
36
+ *
37
+ * === The Anti-Pattern This Replaces ===
38
+ *
39
+ * // WRONG — copy chosen by call site, not by error category
40
+ * try {
41
+ * await uploadFile(file)
42
+ * } catch (err) {
43
+ * // This branch assumes every failure here is the file's fault.
44
+ * setMessage('That file could not be uploaded. Try a different file.')
45
+ * }
46
+ *
47
+ * A 402 (quota), 429 (rate limit), 401 (auth expired), or 503 (server down)
48
+ * all hit this branch and all get blamed on the file. Categorize instead.
49
+ */
50
+
51
+ 'use client'
52
+
53
+ import { useCallback, useState } from 'react'
54
+
55
+ // ── Error category union ─────────────────────────────
56
+ // The exhaustive set of categories the UI knows how to talk about. Keep this
57
+ // closed — adding a category forces you to add copy for it (the COPY map below
58
+ // is keyed by this union, so the compiler flags a missing entry).
59
+ export type ErrorCategory =
60
+ | 'quota' // plan/usage limit reached (billing dimension), e.g. 402 + quota code
61
+ | 'rate-limit' // too many requests in a window; retry after a delay (429)
62
+ | 'timeout' // request took too long / aborted (408, AbortError)
63
+ | 'network' // could not reach the server at all (offline, DNS, CORS, fetch throw)
64
+ | 'validation' // the input was rejected (400/422 with field errors)
65
+ | 'auth' // not authenticated / session expired (401)
66
+ | 'forbidden' // authenticated but not allowed (403)
67
+ | 'not-found' // the target does not exist (404)
68
+ | 'server' // backend fault, not the user's fault (500/502/503/504)
69
+ | 'unknown' // safe fallback — none of the signals matched
70
+
71
+ // ── Normalized error input ───────────────────────────
72
+ // Real catch blocks receive heterogeneous junk: fetch Responses, thrown
73
+ // Errors, parsed JSON error bodies, DOMExceptions. classify() accepts a
74
+ // normalized shape so callers can adapt their transport once at the boundary.
75
+ export interface NormalizedError {
76
+ /** HTTP status code, if the error came from an HTTP response. */
77
+ status?: number
78
+ /**
79
+ * Machine-readable error code from the response body. Backends commonly send
80
+ * `{ code: 'quota_exceeded' }` or `{ type: 'insufficient_quota' }`. We check
81
+ * both `code` and `type` because providers disagree on the field name.
82
+ */
83
+ code?: string
84
+ /** Human/loggable message — used only as a last-resort signal, never as copy. */
85
+ message?: string
86
+ /** Retry-After value in seconds, if the server sent one (rate limit / quota). */
87
+ retryAfterSeconds?: number
88
+ /** True if this was a fetch/network throw (no response was received at all). */
89
+ isNetworkError?: boolean
90
+ /** True if the request was aborted or timed out client-side. */
91
+ isTimeout?: boolean
92
+ /** Field-level validation errors, if the backend returned them. */
93
+ fieldErrors?: Record<string, string[]>
94
+ }
95
+
96
+ // Codes (from the body) that signal a billing/quota exhaustion regardless of
97
+ // the status code. Different providers use different strings — match on any.
98
+ const QUOTA_CODES = new Set([
99
+ 'quota_exceeded',
100
+ 'insufficient_quota',
101
+ 'plan_limit_reached',
102
+ 'billing_quota_exceeded',
103
+ 'usage_limit_exceeded',
104
+ 'storage_quota_exceeded',
105
+ ])
106
+
107
+ const RATE_LIMIT_CODES = new Set([
108
+ 'rate_limited',
109
+ 'rate_limit_exceeded',
110
+ 'too_many_requests',
111
+ 'throttled',
112
+ ])
113
+
114
+ const AUTH_CODES = new Set([
115
+ 'token_expired',
116
+ 'invalid_token',
117
+ 'session_expired',
118
+ 'unauthenticated',
119
+ ])
120
+
121
+ // ── classify(): error shape + status -> category ─────
122
+ //
123
+ // Order matters. We check the most specific, least-ambiguous signals first
124
+ // (network/timeout, then body codes), and only fall back to bare status codes
125
+ // when the shape gave us nothing. This is what stops a 402 quota error from
126
+ // being miscategorized as a generic 4xx validation failure.
127
+ export function classify(err: NormalizedError): ErrorCategory {
128
+ // 1. No response at all — we never reached the server. This must come before
129
+ // status checks because a network throw has no status.
130
+ if (err.isNetworkError) return 'network'
131
+
132
+ // 2. Client-side timeout / abort.
133
+ if (err.isTimeout || err.status === 408) return 'timeout'
134
+
135
+ const code = (err.code ?? '').toLowerCase()
136
+
137
+ // 3. Body-code disambiguation — THE critical step (#343 F8). A 402 or even a
138
+ // 429 can be a quota/billing problem; the body code tells us which copy to
139
+ // show. Decide on the code before falling through to status-only logic.
140
+ if (QUOTA_CODES.has(code)) return 'quota'
141
+ if (RATE_LIMIT_CODES.has(code)) return 'rate-limit'
142
+ if (AUTH_CODES.has(code)) return 'auth'
143
+
144
+ // 4. Status-driven categories for the unambiguous cases.
145
+ switch (err.status) {
146
+ case 401:
147
+ return 'auth'
148
+ case 402:
149
+ // Payment Required is, by spec, a billing/quota signal. If we got here the
150
+ // body code didn't say "rate-limit", so treat 402 as quota — NOT as a
151
+ // generic validation error the file-upload branch would have shown.
152
+ return 'quota'
153
+ case 403:
154
+ return 'forbidden'
155
+ case 404:
156
+ return 'not-found'
157
+ case 422:
158
+ return 'validation'
159
+ case 429:
160
+ // 429 with no quota code is a plain rate limit (retry later). A 429 WITH a
161
+ // quota code was already caught in step 3.
162
+ return 'rate-limit'
163
+ case 500:
164
+ case 502:
165
+ case 503:
166
+ case 504:
167
+ return 'server'
168
+ case 400:
169
+ // 400 with field errors is a validation failure; a bare 400 is ambiguous,
170
+ // so only call it validation when the backend actually itemized fields.
171
+ return err.fieldErrors && Object.keys(err.fieldErrors).length > 0
172
+ ? 'validation'
173
+ : 'unknown'
174
+ }
175
+
176
+ // 5. Last-resort message sniffing for transports that drop status/code.
177
+ const msg = (err.message ?? '').toLowerCase()
178
+ if (msg.includes('quota') || msg.includes('billing')) return 'quota'
179
+ if (msg.includes('rate limit') || msg.includes('too many requests')) return 'rate-limit'
180
+ if (msg.includes('timeout') || msg.includes('timed out')) return 'timeout'
181
+ if (msg.includes('network') || msg.includes('failed to fetch')) return 'network'
182
+
183
+ // 6. We could not justify a specific category from any signal.
184
+ return 'unknown'
185
+ }
186
+
187
+ // ── normalizeError(): transport junk -> NormalizedError ─
188
+ // Reference adapter for the fetch/Response + thrown-Error world. Call this once
189
+ // at your data-access boundary so classify() always receives a clean shape.
190
+ export async function normalizeError(input: unknown): Promise<NormalizedError> {
191
+ // fetch network failures throw a TypeError before any Response exists.
192
+ if (input instanceof TypeError && /fetch/i.test(input.message)) {
193
+ return { isNetworkError: true, message: input.message }
194
+ }
195
+
196
+ // AbortController / client timeout surfaces as a DOMException.
197
+ if (input instanceof DOMException && input.name === 'AbortError') {
198
+ return { isTimeout: true, message: input.message }
199
+ }
200
+
201
+ // An HTTP Response that came back non-OK.
202
+ if (input instanceof Response) {
203
+ const retryAfterHeader = input.headers.get('Retry-After')
204
+ const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : undefined
205
+ let body: { code?: string; type?: string; message?: string; errors?: Record<string, string[]> } = {}
206
+ try {
207
+ body = await input.clone().json()
208
+ } catch {
209
+ // Non-JSON body (HTML error page, empty 502, etc.) — status alone drives it.
210
+ }
211
+ return {
212
+ status: input.status,
213
+ code: body.code ?? body.type,
214
+ message: body.message,
215
+ retryAfterSeconds: Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined,
216
+ fieldErrors: body.errors,
217
+ }
218
+ }
219
+
220
+ // A plain thrown Error.
221
+ if (input instanceof Error) {
222
+ return { message: input.message }
223
+ }
224
+
225
+ return { message: String(input) }
226
+ }
227
+
228
+ // ── Copy map: category -> user-facing copy ───────────
229
+ // Keyed by the full ErrorCategory union, so dropping a category is a compile
230
+ // error. `action` is the label for the primary recovery button; `retryable`
231
+ // tells the UI whether to even offer a retry (retrying a 403 is pointless).
232
+ export interface ErrorCopy {
233
+ title: string
234
+ body: string
235
+ action: string
236
+ retryable: boolean
237
+ }
238
+
239
+ export const COPY: Record<ErrorCategory, ErrorCopy> = {
240
+ quota: {
241
+ title: "You've hit your plan limit",
242
+ body: 'This action would exceed your current plan. Upgrade your plan or wait for your usage to reset — re-trying the same request will not help.',
243
+ action: 'View plans',
244
+ retryable: false,
245
+ },
246
+ 'rate-limit': {
247
+ title: 'Slow down a moment',
248
+ body: "You're sending requests faster than we allow. Wait a few seconds and try again.",
249
+ action: 'Try again',
250
+ retryable: true,
251
+ },
252
+ timeout: {
253
+ title: 'That took too long',
254
+ body: 'The request timed out before we got a response. Your connection may be slow — try again.',
255
+ action: 'Try again',
256
+ retryable: true,
257
+ },
258
+ network: {
259
+ title: "Can't reach the server",
260
+ body: "We couldn't connect. Check your internet connection and try again.",
261
+ action: 'Try again',
262
+ retryable: true,
263
+ },
264
+ validation: {
265
+ title: 'Check your input',
266
+ body: 'Some of the information you entered was rejected. Fix the highlighted fields and resubmit.',
267
+ action: 'Review fields',
268
+ retryable: false,
269
+ },
270
+ auth: {
271
+ title: 'Your session expired',
272
+ body: 'You need to sign in again to continue. Re-trying will not work until you do.',
273
+ action: 'Sign in',
274
+ retryable: false,
275
+ },
276
+ forbidden: {
277
+ title: "You don't have access",
278
+ body: "Your account isn't permitted to do this. Contact an administrator if you think that's wrong.",
279
+ action: 'Go back',
280
+ retryable: false,
281
+ },
282
+ 'not-found': {
283
+ title: "We couldn't find that",
284
+ body: 'The item you were looking for no longer exists or was moved.',
285
+ action: 'Go back',
286
+ retryable: false,
287
+ },
288
+ server: {
289
+ title: 'Something broke on our end',
290
+ body: "This isn't your fault — our server hit an error. We've been notified. Try again in a moment.",
291
+ action: 'Try again',
292
+ retryable: true,
293
+ },
294
+ unknown: {
295
+ title: 'Something went wrong',
296
+ body: "An unexpected error occurred. If it keeps happening, contact support.",
297
+ action: 'Try again',
298
+ retryable: true,
299
+ },
300
+ }
301
+
302
+ /** Convenience: go straight from a caught value to the copy to display. */
303
+ export async function copyForError(input: unknown): Promise<{ category: ErrorCategory; copy: ErrorCopy }> {
304
+ const category = classify(await normalizeError(input))
305
+ return { category, copy: COPY[category] }
306
+ }
307
+
308
+ // ── React usage example ──────────────────────────────
309
+ // A small hook that turns any caught value into a renderable error state, plus
310
+ // the component that renders it. Note the upload handler NEVER hardcodes
311
+ // "try a different file" — the copy is whatever the CATEGORY dictates.
312
+
313
+ interface ErrorState {
314
+ category: ErrorCategory
315
+ copy: ErrorCopy
316
+ }
317
+
318
+ function useCategorizedError() {
319
+ const [errorState, setErrorState] = useState<ErrorState | null>(null)
320
+
321
+ const capture = useCallback(async (input: unknown) => {
322
+ setErrorState(await copyForError(input))
323
+ }, [])
324
+
325
+ const clear = useCallback(() => setErrorState(null), [])
326
+
327
+ return { errorState, capture, clear }
328
+ }
329
+
330
+ export function UploadPanel({ uploadFile }: { uploadFile: (file: File) => Promise<void> }) {
331
+ const { errorState, capture, clear } = useCategorizedError()
332
+ const [busy, setBusy] = useState(false)
333
+
334
+ async function handleUpload(file: File) {
335
+ setBusy(true)
336
+ clear()
337
+ try {
338
+ await uploadFile(file)
339
+ } catch (err) {
340
+ // Categorize first. A 402 quota error here renders the quota copy, never
341
+ // the validation "try a different file" copy. (#343 F8)
342
+ await capture(err)
343
+ } finally {
344
+ setBusy(false)
345
+ }
346
+ }
347
+
348
+ return (
349
+ <div>
350
+ <input
351
+ type="file"
352
+ disabled={busy}
353
+ aria-label="Choose a file to upload"
354
+ onChange={(e) => {
355
+ const file = e.target.files?.[0]
356
+ if (file) handleUpload(file)
357
+ }}
358
+ />
359
+
360
+ {errorState && (
361
+ <div role="alert" className="mt-3 rounded-lg border border-red-200 bg-red-50 p-4">
362
+ <p className="font-medium text-red-800">{errorState.copy.title}</p>
363
+ <p className="mt-1 text-sm text-red-600">{errorState.copy.body}</p>
364
+ {errorState.copy.retryable && (
365
+ <button
366
+ onClick={clear}
367
+ className="mt-3 text-sm font-medium text-red-700 underline hover:text-red-800"
368
+ >
369
+ {errorState.copy.action}
370
+ </button>
371
+ )}
372
+ </div>
373
+ )}
374
+ </div>
375
+ )
376
+ }
@@ -84,5 +84,6 @@ declare function createLogger(logPath: string): {
84
84
  log: (msg: string) => void;
85
85
  close: () => void;
86
86
  };
87
- export { writePidFile, checkStalePid, removePidFile, generateSessionToken, validateToken, createSocketServer, startSocketServer, writeState, setupSignalHandlers, JobScheduler, createLogger, PID_FILE, SOCKET_PATH, TOKEN_FILE, STATE_FILE, LOG_FILE, };
87
+ declare function parseDotenv(contents: string): Record<string, string>;
88
+ export { writePidFile, checkStalePid, removePidFile, generateSessionToken, validateToken, createSocketServer, startSocketServer, writeState, setupSignalHandlers, JobScheduler, createLogger, parseDotenv, PID_FILE, SOCKET_PATH, TOKEN_FILE, STATE_FILE, LOG_FILE, };
88
89
  export type { DaemonState, HeartbeatState, ScheduledJob };
@@ -268,4 +268,92 @@ function createLogger(logPath) {
268
268
  close() { stream.end(); }
269
269
  };
270
270
  }
271
- export { writePidFile, checkStalePid, removePidFile, generateSessionToken, validateToken, createSocketServer, startSocketServer, writeState, setupSignalHandlers, JobScheduler, createLogger, PID_FILE, SOCKET_PATH, TOKEN_FILE, STATE_FILE, LOG_FILE, };
271
+ // ── .env Parsing (literal, $-safe) ────────────────────
272
+ // field report #344 F1: never source secrets via `export $(cat .env)` /
273
+ // `eval "$(cat .env)"`. The shell performs variable expansion and word
274
+ // splitting on the RHS, so a `$`-bearing secret — bcrypt hashes
275
+ // ($2b$...), JWTs, Postgres URLs with `$` in the password, anything with
276
+ // `$VAR`/`${...}`/backticks — gets mangled or silently truncated. Parse
277
+ // literally instead: read each line, split on the FIRST `=` only, and keep
278
+ // the value byte-for-byte (no expansion, no eval). For shells, the
279
+ // equivalent is `while IFS='=' read -r k v; do export "$k=$v"; done < .env`
280
+ // — note IFS='=' and `read -r` (raw, no backslash processing), which never
281
+ // re-expands the value.
282
+ //
283
+ // Prefer a runtime-native loader where available — it sidesteps the shell
284
+ // entirely:
285
+ // - Node 20.6+: `node --env-file=.env daemon.js` (literal parse, no shell).
286
+ // - systemd: `EnvironmentFile=/etc/voidforge/heartbeat.env` (also literal;
287
+ // unit-file `Environment=` lines do NOT undergo shell expansion).
288
+ // Use this helper only when you must parse `.env` in-process.
289
+ function parseDotenv(contents) {
290
+ const out = {};
291
+ for (const rawLine of contents.split('\n')) {
292
+ const line = rawLine.replace(/\r$/, '');
293
+ // Skip blanks and comments. A leading `export ` prefix is tolerated.
294
+ const trimmed = line.trimStart();
295
+ if (trimmed === '' || trimmed.startsWith('#'))
296
+ continue;
297
+ const body = trimmed.startsWith('export ') ? trimmed.slice(7) : trimmed;
298
+ // Split on the FIRST `=` only — values may legitimately contain `=`.
299
+ const eq = body.indexOf('=');
300
+ if (eq < 0)
301
+ continue; // not a KEY=VALUE line — ignore, don't guess
302
+ const key = body.slice(0, eq).trim();
303
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
304
+ continue; // invalid env name
305
+ let value = body.slice(eq + 1);
306
+ // Strip a single layer of matching surrounding quotes. Inside quotes the
307
+ // value is taken LITERALLY — no `$` expansion, no eval — which is the
308
+ // whole point: `PASS='p@$$w0rd'` keeps its `$$` intact.
309
+ if (value.length >= 2 &&
310
+ ((value[0] === '"' && value[value.length - 1] === '"') ||
311
+ (value[0] === "'" && value[value.length - 1] === "'"))) {
312
+ value = value.slice(1, -1);
313
+ }
314
+ else {
315
+ // Unquoted: trim trailing inline whitespace only (POSIX-ish), never
316
+ // touch interior `$` characters.
317
+ value = value.trimEnd();
318
+ }
319
+ out[key] = value;
320
+ }
321
+ return out;
322
+ }
323
+ // ── systemd hardening stanza (Node daemons) ───────────
324
+ // field report #344 F3: when running this daemon under systemd, harden the
325
+ // unit — but DO NOT set `MemoryDenyWriteExecute=true` for a Node/V8 process.
326
+ // V8's JIT allocates pages that are written and then executed (it manages its
327
+ // own W^X internally); MDWE forbids any write+exec mapping, so the daemon
328
+ // takes a SIGTRAP and dies at boot, usually before it logs a single line. The
329
+ // safe, high-value sandbox flags below give most of MDWE's benefit without the
330
+ // JIT collision:
331
+ //
332
+ // [Unit]
333
+ // Description=VoidForge Heartbeat daemon
334
+ // After=network-online.target
335
+ // Wants=network-online.target
336
+ //
337
+ // [Service]
338
+ // Type=simple
339
+ // ExecStart=/usr/bin/node /opt/voidforge/daemon.js
340
+ // EnvironmentFile=/etc/voidforge/heartbeat.env # literal parse — see #344 F1
341
+ // Restart=on-failure
342
+ // RestartSec=5
343
+ //
344
+ // # Hardening — keep these:
345
+ // NoNewPrivileges=true # no setuid/setgid privilege escalation
346
+ // ProtectSystem=full # /usr, /boot, /etc mounted read-only
347
+ // ProtectHome=true # /home, /root, /run/user hidden
348
+ // PrivateTmp=true # private /tmp + /var/tmp namespace
349
+ // # MemoryDenyWriteExecute=true # <-- OMITTED ON PURPOSE: breaks V8 JIT
350
+ // # (SIGTRAP at boot). Re-enable ONLY for
351
+ // # Go/Rust/static daemons with no JIT.
352
+ //
353
+ // [Install]
354
+ // WantedBy=multi-user.target
355
+ //
356
+ // Go, Rust, and other AOT-compiled daemons emit no executable pages at
357
+ // runtime, so for THEM you can and should keep `MemoryDenyWriteExecute=true`.
358
+ // The omission above is V8-specific, not a general weakening.
359
+ export { writePidFile, checkStalePid, removePidFile, generateSessionToken, validateToken, createSocketServer, startSocketServer, writeState, setupSignalHandlers, JobScheduler, createLogger, parseDotenv, PID_FILE, SOCKET_PATH, TOKEN_FILE, STATE_FILE, LOG_FILE, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voidforge-build",
3
- "version": "23.11.3",
3
+ "version": "23.12.0",
4
4
  "description": "From nothing, everything. A methodology framework for building with Claude Code.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -45,7 +45,7 @@
45
45
  "@aws-sdk/client-rds": "^3.700.0",
46
46
  "@aws-sdk/client-s3": "^3.700.0",
47
47
  "@aws-sdk/client-sts": "^3.700.0",
48
- "voidforge-build-methodology": "^23.11.3",
48
+ "voidforge-build-methodology": "^23.12.0",
49
49
  "node-pty": "^1.2.0-beta.12",
50
50
  "ws": "^8.19.0"
51
51
  },