voidforge-build 23.10.0 → 23.11.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 (65) hide show
  1. package/dist/.claude/agents/bashir-field-medic.md +1 -0
  2. package/dist/.claude/agents/coulson-release.md +3 -0
  3. package/dist/.claude/agents/irulan-historian.md +3 -0
  4. package/dist/.claude/agents/loki-chaos.md +1 -0
  5. package/dist/.claude/agents/picard-architecture.md +3 -0
  6. package/dist/.claude/agents/silver-surfer-herald.md +3 -0
  7. package/dist/.claude/agents/sisko-campaign.md +3 -0
  8. package/dist/.claude/commands/architect.md +38 -0
  9. package/dist/.claude/commands/campaign.md +2 -0
  10. package/dist/.claude/commands/gauntlet.md +11 -0
  11. package/dist/.claude/commands/git.md +13 -3
  12. package/dist/CHANGELOG.md +63 -0
  13. package/dist/CLAUDE.md +13 -4
  14. package/dist/VERSION.md +2 -1
  15. package/dist/docs/methods/AI_INTELLIGENCE.md +15 -0
  16. package/dist/docs/methods/BACKEND_ENGINEER.md +48 -0
  17. package/dist/docs/methods/CAMPAIGN.md +196 -1
  18. package/dist/docs/methods/DEVOPS_ENGINEER.md +16 -0
  19. package/dist/docs/methods/FORGE_KEEPER.md +18 -0
  20. package/dist/docs/methods/GAUNTLET.md +2 -0
  21. package/dist/docs/methods/QA_ENGINEER.md +46 -0
  22. package/dist/docs/methods/RELEASE_MANAGER.md +59 -0
  23. package/dist/docs/methods/SECURITY_AUDITOR.md +53 -0
  24. package/dist/docs/methods/SUB_AGENTS.md +90 -0
  25. package/dist/docs/methods/SYSTEMS_ARCHITECT.md +42 -2
  26. package/dist/docs/methods/TESTING.md +17 -0
  27. package/dist/docs/methods/TIME_VAULT.md +17 -0
  28. package/dist/docs/patterns/adr-verification-gate.md +80 -0
  29. package/dist/docs/patterns/ai-eval.ts +87 -0
  30. package/dist/docs/patterns/ai-prompt-safety.ts +242 -0
  31. package/dist/docs/patterns/audit-log.ts +132 -0
  32. package/dist/docs/patterns/llm-state-dedup.ts +246 -0
  33. package/dist/docs/patterns/middleware.ts +83 -0
  34. package/dist/docs/patterns/multi-tenant-pool-bypass.ts +134 -0
  35. package/dist/docs/patterns/multi-tenant-property-test.ts +127 -0
  36. package/dist/docs/patterns/refactor-extraction.md +96 -0
  37. package/dist/scripts/voidforge.js +0 -0
  38. package/dist/wizard/lib/anomaly-detection.d.ts +59 -0
  39. package/dist/wizard/lib/anomaly-detection.js +122 -0
  40. package/dist/wizard/lib/asset-scanner.d.ts +23 -0
  41. package/dist/wizard/lib/asset-scanner.js +107 -0
  42. package/dist/wizard/lib/build-analytics.d.ts +39 -0
  43. package/dist/wizard/lib/build-analytics.js +91 -0
  44. package/dist/wizard/lib/codegen/erd-gen.d.ts +16 -0
  45. package/dist/wizard/lib/codegen/erd-gen.js +98 -0
  46. package/dist/wizard/lib/codegen/openapi-gen.d.ts +15 -0
  47. package/dist/wizard/lib/codegen/openapi-gen.js +79 -0
  48. package/dist/wizard/lib/codegen/prisma-types.d.ts +15 -0
  49. package/dist/wizard/lib/codegen/prisma-types.js +44 -0
  50. package/dist/wizard/lib/codegen/seed-gen.d.ts +16 -0
  51. package/dist/wizard/lib/codegen/seed-gen.js +128 -0
  52. package/dist/wizard/lib/correlation-engine.d.ts +59 -0
  53. package/dist/wizard/lib/correlation-engine.js +152 -0
  54. package/dist/wizard/lib/desktop-notify.d.ts +27 -0
  55. package/dist/wizard/lib/desktop-notify.js +98 -0
  56. package/dist/wizard/lib/image-gen.d.ts +56 -0
  57. package/dist/wizard/lib/image-gen.js +159 -0
  58. package/dist/wizard/lib/natural-language-deploy.d.ts +30 -0
  59. package/dist/wizard/lib/natural-language-deploy.js +186 -0
  60. package/dist/wizard/lib/project-init.js +57 -0
  61. package/dist/wizard/lib/route-optimizer.d.ts +28 -0
  62. package/dist/wizard/lib/route-optimizer.js +93 -0
  63. package/dist/wizard/lib/service-install.d.ts +18 -0
  64. package/dist/wizard/lib/service-install.js +182 -0
  65. package/package.json +1 -1
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Image generation provider abstraction — Celebrimbor's forge tools.
3
+ * Default: OpenAI (gpt-image-1). Extensible to other providers.
4
+ * Uses the same vault system as other VoidForge credentials.
5
+ */
6
+ import { writeFile, readFile, mkdir } from 'node:fs/promises';
7
+ import { existsSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { createHash } from 'node:crypto';
10
+ import { httpsPost, httpsGet, safeJsonParse } from './provisioners/http-client.js';
11
+ // ── OpenAI Provider ──────────────────────────────────────
12
+ const OPENAI_API = 'api.openai.com';
13
+ /**
14
+ * Generate an image via OpenAI's API.
15
+ * Returns the raw image bytes as a Buffer.
16
+ */
17
+ export async function generateImage(apiKey, options, emit) {
18
+ const model = options.model || 'gpt-image-1';
19
+ const size = `${options.width}x${options.height}`;
20
+ // OpenAI only supports specific sizes — map to nearest and warn
21
+ const validSizes = ['1024x1024', '1792x1024', '1024x1792'];
22
+ const actualSize = validSizes.includes(size) ? size : '1024x1024';
23
+ if (actualSize !== size) {
24
+ emit({ step: 'image-gen', status: 'started', message: `Requested ${size} → using ${actualSize} (API constraint)` });
25
+ }
26
+ const body = JSON.stringify({
27
+ model,
28
+ prompt: options.prompt,
29
+ n: 1,
30
+ size: actualSize,
31
+ quality: options.quality || 'medium',
32
+ response_format: 'b64_json',
33
+ });
34
+ // Retry logic: 3 attempts with exponential backoff (1s, 3s, 9s)
35
+ // DALL-E 3 returns 500 errors on ~15% of requests (field report #1)
36
+ const MAX_RETRIES = 3;
37
+ const BACKOFF_BASE_MS = 1000;
38
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
39
+ try {
40
+ const res = await httpsPost(OPENAI_API, '/v1/images/generations', {
41
+ 'Authorization': `Bearer ${apiKey}`,
42
+ 'Content-Type': 'application/json',
43
+ }, body, 120_000); // 2 min timeout for image generation
44
+ if (res.status === 500 || res.status === 502 || res.status === 503) {
45
+ // Server error — retry with backoff
46
+ if (attempt < MAX_RETRIES) {
47
+ const delay = BACKOFF_BASE_MS * Math.pow(3, attempt - 1);
48
+ emit({ step: 'image-gen', status: 'started', message: `Server error (${res.status}), retrying in ${delay / 1000}s (attempt ${attempt}/${MAX_RETRIES})` });
49
+ await new Promise((resolve) => setTimeout(resolve, delay));
50
+ continue;
51
+ }
52
+ emit({ step: 'image-gen', status: 'error', message: `Server error (${res.status}) after ${MAX_RETRIES} attempts` });
53
+ return null;
54
+ }
55
+ if (res.status !== 200) {
56
+ const errData = safeJsonParse(res.body);
57
+ const errMsg = errData?.error?.message || `API returned ${res.status}`;
58
+ emit({ step: 'image-gen', status: 'error', message: `Generation failed: ${errMsg}` });
59
+ return null;
60
+ }
61
+ const data = safeJsonParse(res.body);
62
+ const b64 = data?.data?.[0]?.b64_json;
63
+ if (!b64) {
64
+ emit({ step: 'image-gen', status: 'error', message: 'No image data in API response' });
65
+ return null;
66
+ }
67
+ if (attempt > 1) {
68
+ emit({ step: 'image-gen', status: 'done', message: `Succeeded on attempt ${attempt}` });
69
+ }
70
+ return Buffer.from(b64, 'base64');
71
+ }
72
+ catch (err) {
73
+ if (attempt < MAX_RETRIES) {
74
+ const delay = BACKOFF_BASE_MS * Math.pow(3, attempt - 1);
75
+ emit({ step: 'image-gen', status: 'started', message: `Request failed, retrying in ${delay / 1000}s (attempt ${attempt}/${MAX_RETRIES})`, detail: err.message });
76
+ await new Promise((resolve) => setTimeout(resolve, delay));
77
+ continue;
78
+ }
79
+ emit({ step: 'image-gen', status: 'error', message: `Image generation failed after ${MAX_RETRIES} attempts`, detail: err.message });
80
+ return null;
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+ /**
86
+ * Validate an OpenAI API key by making a lightweight models list request.
87
+ */
88
+ export async function validateOpenAIKey(apiKey) {
89
+ try {
90
+ const res = await httpsGet(OPENAI_API, '/v1/models', {
91
+ 'Authorization': `Bearer ${apiKey}`,
92
+ }, 10_000);
93
+ return res.status === 200;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ /**
100
+ * Estimate the cost of generating N images.
101
+ */
102
+ export function estimateImageCost(count, model = 'gpt-image-1') {
103
+ const costPerImage = {
104
+ 'gpt-image-1': 0.04,
105
+ 'dall-e-3': 0.08,
106
+ };
107
+ return count * (costPerImage[model] || 0.04);
108
+ }
109
+ // ── Asset Manifest ──────────────────────────────────────
110
+ const MANIFEST_FILENAME = 'manifest.json';
111
+ /**
112
+ * Read the asset manifest from disk.
113
+ */
114
+ export async function readManifest(imagesDir) {
115
+ const manifestPath = join(imagesDir, MANIFEST_FILENAME);
116
+ try {
117
+ const content = await readFile(manifestPath, 'utf-8');
118
+ return JSON.parse(content);
119
+ }
120
+ catch {
121
+ return null;
122
+ }
123
+ }
124
+ /**
125
+ * Write the asset manifest to disk.
126
+ */
127
+ export async function writeManifest(imagesDir, manifest) {
128
+ await mkdir(imagesDir, { recursive: true });
129
+ await writeFile(join(imagesDir, MANIFEST_FILENAME), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
130
+ }
131
+ /**
132
+ * Save a generated image to disk and update the manifest.
133
+ */
134
+ export async function saveGeneratedImage(imagesDir, category, name, imageBuffer, prompt, size, manifest) {
135
+ const categoryDir = join(imagesDir, category);
136
+ await mkdir(categoryDir, { recursive: true });
137
+ const filename = `${category}/${name}.png`;
138
+ const filepath = join(imagesDir, filename);
139
+ await writeFile(filepath, imageBuffer);
140
+ const hash = createHash('sha256').update(imageBuffer).digest('hex');
141
+ // Remove any existing entry with the same filename (dedup for --regen)
142
+ manifest.assets = manifest.assets.filter(a => a.filename !== filename);
143
+ manifest.assets.push({
144
+ name,
145
+ filename,
146
+ prompt,
147
+ size,
148
+ generatedAt: new Date().toISOString(),
149
+ hash: `sha256:${hash}`,
150
+ });
151
+ await writeManifest(imagesDir, manifest);
152
+ return filepath;
153
+ }
154
+ /**
155
+ * Check if an asset already exists on disk.
156
+ */
157
+ export function assetExists(imagesDir, category, name) {
158
+ return existsSync(join(imagesDir, category, `${name}.png`));
159
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Natural Language Deploy — resolve prose deployment descriptions to YAML frontmatter.
3
+ *
4
+ * Parse: "I want a $20/month server with SSL and daily backups"
5
+ * → { deploy: 'vps', instanceType: 't3.small', hostname: '', resilience: { backups: 'daily', ... } }
6
+ *
7
+ * Uses keyword matching and heuristics — no AI API call required.
8
+ */
9
+ export interface DeployConfig {
10
+ deploy: 'vps' | 'vercel' | 'railway' | 'cloudflare' | 'static' | 'docker';
11
+ instanceType: string;
12
+ hostname: string;
13
+ estimatedMonthlyCost: string;
14
+ resilience: {
15
+ multiEnv: boolean;
16
+ previewDeploys: boolean;
17
+ rollback: boolean;
18
+ migrations: 'auto' | 'manual' | 'no';
19
+ backups: 'daily' | 'weekly' | 'no';
20
+ healthCheck: boolean;
21
+ gracefulShutdown: boolean;
22
+ errorBoundaries: boolean;
23
+ rateLimiting: boolean;
24
+ deadLetterQueue: boolean;
25
+ };
26
+ reasoning: string[];
27
+ }
28
+ export declare function resolveDeployConfig(prose: string): DeployConfig | null;
29
+ /** Convert a DeployConfig to YAML frontmatter fragment. */
30
+ export declare function toFrontmatter(config: DeployConfig): string;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Natural Language Deploy — resolve prose deployment descriptions to YAML frontmatter.
3
+ *
4
+ * Parse: "I want a $20/month server with SSL and daily backups"
5
+ * → { deploy: 'vps', instanceType: 't3.small', hostname: '', resilience: { backups: 'daily', ... } }
6
+ *
7
+ * Uses keyword matching and heuristics — no AI API call required.
8
+ */
9
+ const BUDGET_TIERS = [
10
+ { maxMonthly: 10, instanceType: 't3.micro', label: '~$8/mo' },
11
+ { maxMonthly: 25, instanceType: 't3.small', label: '~$17/mo' },
12
+ { maxMonthly: 50, instanceType: 't3.medium', label: '~$34/mo' },
13
+ { maxMonthly: 100, instanceType: 't3.large', label: '~$68/mo' },
14
+ { maxMonthly: Infinity, instanceType: 't3.xlarge', label: '~$136/mo' },
15
+ ];
16
+ function resolveInstanceFromBudget(budget) {
17
+ return BUDGET_TIERS.find(t => budget <= t.maxMonthly) ?? BUDGET_TIERS[BUDGET_TIERS.length - 1];
18
+ }
19
+ // ── Keyword patterns ────────────────────────────
20
+ const PLATFORM_KEYWORDS = [
21
+ { pattern: /\bvercel\b/i, target: 'vercel', reason: 'Vercel mentioned explicitly' },
22
+ { pattern: /\brailway\b/i, target: 'railway', reason: 'Railway mentioned explicitly' },
23
+ { pattern: /\bcloudflare\b/i, target: 'cloudflare', reason: 'Cloudflare mentioned explicitly' },
24
+ { pattern: /\bdocker\b|\bcontainer\b/i, target: 'docker', reason: 'Docker/container mentioned' },
25
+ { pattern: /\bstatic\s*(?:site|hosting|files?)\b/i, target: 'static', reason: 'Static site hosting' },
26
+ { pattern: /\bvps\b|\bserver\b|\bec2\b|\baws\b|\bssh\b/i, target: 'vps', reason: 'Server/VPS/AWS mentioned' },
27
+ { pattern: /\bserverless\b|\bedge\b/i, target: 'vercel', reason: 'Serverless/edge → Vercel' },
28
+ { pattern: /\bfree\s*tier\b|\bno\s*cost\b|\bfree\b/i, target: 'railway', reason: 'Free tier → Railway' },
29
+ ];
30
+ const FEATURE_KEYWORDS = [
31
+ { pattern: /\bbackup/i, key: 'backups', reason: 'Backups requested' },
32
+ { pattern: /\bssl\b|\bhttps\b|\btls\b/i, key: 'ssl', reason: 'SSL/TLS requested' },
33
+ { pattern: /\bcustom\s*domain\b|\bmy\s*domain\b/i, key: 'customDomain', reason: 'Custom domain' },
34
+ { pattern: /\brollback\b|\brevert\b/i, key: 'rollback', reason: 'Rollback requested' },
35
+ { pattern: /\bpreview\b|\bpr\s*deploy/i, key: 'previewDeploys', reason: 'Preview deploys' },
36
+ { pattern: /\bhealth\s*check\b|\bmonitoring\b|\buptime\b/i, key: 'healthCheck', reason: 'Health monitoring' },
37
+ { pattern: /\brate\s*limit/i, key: 'rateLimiting', reason: 'Rate limiting' },
38
+ { pattern: /\bgraceful\b|\bzero\s*downtime\b/i, key: 'gracefulShutdown', reason: 'Zero-downtime' },
39
+ { pattern: /\bmulti\s*(?:env|environment)\b|\bstaging\b/i, key: 'multiEnv', reason: 'Multi-environment' },
40
+ { pattern: /\berror\s*boundar/i, key: 'errorBoundaries', reason: 'Error boundaries' },
41
+ { pattern: /\bdead\s*letter\b|\bdlq\b|\bretry\s*queue\b/i, key: 'deadLetterQueue', reason: 'Dead letter queue' },
42
+ { pattern: /\bmigration/i, key: 'migrations', reason: 'Database migrations' },
43
+ ];
44
+ const SCALE_KEYWORDS = [
45
+ { pattern: /\bsmall\b|\bsimple\b|\bblog\b|\bpersonal\b|\bside\s*project\b|\bmvp\b|\bprototype\b/i, scale: 'small', reason: 'Small/simple project' },
46
+ { pattern: /\bmedium\b|\bstartup\b|\bsaas\b|\bteam\b|\bgrow/i, scale: 'medium', reason: 'Medium/startup scale' },
47
+ { pattern: /\blarge\b|\benterprise\b|\bthousands\b|\bhigh\s*traffic\b|\bscale\b|\bproduction\b/i, scale: 'large', reason: 'Large/production scale' },
48
+ ];
49
+ // ── Main resolver ───────────────────────────────
50
+ export function resolveDeployConfig(prose) {
51
+ if (!prose.trim())
52
+ return null;
53
+ const reasoning = [];
54
+ const features = new Set();
55
+ // Extract budget — prefer amounts near cost keywords, fall back to first $N
56
+ const costContextMatch = prose.match(/(?:budget|spend|cost|month|mo)[^$]*\$(\d+(?:\.\d+)?)/i)
57
+ ?? prose.match(/\$(\d+(?:\.\d+)?)(?:\s*\/\s*mo(?:nth)?)/i)
58
+ ?? prose.match(/\$(\d+(?:\.\d+)?)/i);
59
+ const budget = costContextMatch ? Math.round(parseFloat(costContextMatch[1])) : -1;
60
+ // Detect explicit platform
61
+ let deploy = 'vps'; // default
62
+ let platformDetected = false;
63
+ for (const kw of PLATFORM_KEYWORDS) {
64
+ if (kw.pattern.test(prose)) {
65
+ deploy = kw.target;
66
+ reasoning.push(kw.reason);
67
+ platformDetected = true;
68
+ break;
69
+ }
70
+ }
71
+ // Detect features
72
+ for (const kw of FEATURE_KEYWORDS) {
73
+ if (kw.pattern.test(prose)) {
74
+ features.add(kw.key);
75
+ reasoning.push(kw.reason);
76
+ }
77
+ }
78
+ // Detect scale
79
+ let scale = 'small';
80
+ for (const kw of SCALE_KEYWORDS) {
81
+ if (kw.pattern.test(prose)) {
82
+ scale = kw.scale;
83
+ reasoning.push(kw.reason);
84
+ break;
85
+ }
86
+ }
87
+ // If no platform detected, infer from features and scale
88
+ if (!platformDetected) {
89
+ if (features.has('previewDeploys') || features.has('errorBoundaries')) {
90
+ deploy = 'vercel';
91
+ reasoning.push('Preview deploys/error boundaries → Vercel (best support)');
92
+ }
93
+ else if (scale === 'large' || features.has('customDomain') || budget > 30) {
94
+ deploy = 'vps';
95
+ reasoning.push('Large scale or custom domain with budget → VPS');
96
+ }
97
+ else if (scale === 'small' && budget < 0) {
98
+ deploy = 'railway';
99
+ reasoning.push('Small project, no budget specified → Railway (easiest start)');
100
+ }
101
+ else {
102
+ deploy = 'vps';
103
+ reasoning.push('Default → VPS (most flexible)');
104
+ }
105
+ }
106
+ // Resolve instance type from budget or scale
107
+ let instanceType = '';
108
+ let estimatedCost = '';
109
+ if (deploy === 'vps') {
110
+ if (budget >= 0) {
111
+ const tier = resolveInstanceFromBudget(budget);
112
+ instanceType = tier.instanceType;
113
+ estimatedCost = tier.label;
114
+ reasoning.push(`Budget $${budget}/mo → ${tier.instanceType} (${tier.label})`);
115
+ }
116
+ else {
117
+ const scaleMap = { small: 't3.micro', medium: 't3.small', large: 't3.medium' };
118
+ const costMap = { small: '~$8/mo', medium: '~$17/mo', large: '~$34/mo' };
119
+ instanceType = scaleMap[scale];
120
+ estimatedCost = costMap[scale];
121
+ reasoning.push(`${scale} scale → ${instanceType} (${estimatedCost})`);
122
+ }
123
+ }
124
+ else {
125
+ estimatedCost = deploy === 'railway' ? 'Free tier available' :
126
+ deploy === 'vercel' ? 'Free tier available' :
127
+ deploy === 'cloudflare' ? 'Free tier available' :
128
+ deploy === 'static' ? 'Minimal (~$1/mo S3)' : 'Varies';
129
+ }
130
+ // Extract hostname if mentioned
131
+ const hostnameMatch = prose.match(/(?:domain|hostname|url)[\s:]*([a-z0-9.-]+\.[a-z]{2,})/i);
132
+ const hostname = hostnameMatch ? hostnameMatch[1] : '';
133
+ if (hostname)
134
+ reasoning.push(`Hostname detected: ${hostname}`);
135
+ // Build resilience config — defaults based on deploy target + detected features
136
+ const isVps = deploy === 'vps';
137
+ const isPlatform = ['vercel', 'railway', 'cloudflare'].includes(deploy);
138
+ const resilience = {
139
+ multiEnv: features.has('multiEnv') || scale !== 'small',
140
+ previewDeploys: features.has('previewDeploys') || (isPlatform && scale !== 'small'),
141
+ rollback: features.has('rollback') || isPlatform,
142
+ migrations: features.has('migrations') ? 'auto' : (isVps ? 'manual' : 'no'),
143
+ backups: features.has('backups') ? 'daily' : (isVps && scale !== 'small' ? 'weekly' : 'no'),
144
+ healthCheck: features.has('healthCheck') || isVps || scale !== 'small',
145
+ gracefulShutdown: features.has('gracefulShutdown') || isVps,
146
+ errorBoundaries: features.has('errorBoundaries'),
147
+ rateLimiting: features.has('rateLimiting') || scale === 'large',
148
+ deadLetterQueue: features.has('deadLetterQueue'),
149
+ };
150
+ return {
151
+ deploy,
152
+ instanceType,
153
+ hostname,
154
+ estimatedMonthlyCost: estimatedCost,
155
+ resilience,
156
+ reasoning,
157
+ };
158
+ }
159
+ /** Sanitize a string for safe YAML double-quoted interpolation. */
160
+ function yamlSafe(value) {
161
+ return value.replace(/[\\"]/g, '');
162
+ }
163
+ /** Convert a DeployConfig to YAML frontmatter fragment. */
164
+ export function toFrontmatter(config) {
165
+ const lines = [
166
+ `deploy: "${yamlSafe(config.deploy)}"`,
167
+ ];
168
+ if (config.instanceType) {
169
+ lines.push(`instance_type: "${yamlSafe(config.instanceType)}"`);
170
+ }
171
+ if (config.hostname) {
172
+ lines.push(`hostname: "${yamlSafe(config.hostname.toLowerCase())}"`);
173
+ }
174
+ lines.push('resilience:');
175
+ lines.push(` multi-env: ${config.resilience.multiEnv ? 'yes' : 'no'}`);
176
+ lines.push(` preview-deploys: ${config.resilience.previewDeploys ? 'yes' : 'no'}`);
177
+ lines.push(` rollback: ${config.resilience.rollback ? 'yes' : 'no'}`);
178
+ lines.push(` migrations: "${yamlSafe(config.resilience.migrations)}"`);
179
+ lines.push(` backups: "${yamlSafe(config.resilience.backups)}"`);
180
+ lines.push(` health-check: ${config.resilience.healthCheck ? 'yes' : 'no'}`);
181
+ lines.push(` graceful-shutdown: ${config.resilience.gracefulShutdown ? 'yes' : 'no'}`);
182
+ lines.push(` error-boundaries: ${config.resilience.errorBoundaries ? 'yes' : 'no'}`);
183
+ lines.push(` rate-limiting: ${config.resilience.rateLimiting ? 'yes' : 'no'}`);
184
+ lines.push(` dead-letter-queue: ${config.resilience.deadLetterQueue ? 'yes' : 'no'}`);
185
+ return lines.join('\n');
186
+ }
@@ -113,8 +113,65 @@ async function copyMethodology(methodologyRoot, projectDir, core) {
113
113
  if (existsSync(thumperSrc)) {
114
114
  count += await copyDir(thumperSrc, join(projectDir, 'scripts', 'thumper'));
115
115
  }
116
+ // Surfer-gate scripts (ADR-051 enforcement — closes #317).
117
+ // Ship the gate to every new project so the hook can mechanically enforce
118
+ // CLAUDE.md's Silver Surfer procedure. Without this the prose-backstop is
119
+ // the only thing holding the line in consumer installs.
120
+ const surferGateSrc = join(methodologyRoot, 'scripts', 'surfer-gate');
121
+ if (existsSync(surferGateSrc)) {
122
+ count += await copyDir(surferGateSrc, join(projectDir, 'scripts', 'surfer-gate'));
123
+ await chmodShellScripts(join(projectDir, 'scripts', 'surfer-gate'));
124
+ await mergeSettingsHook(projectDir);
125
+ }
116
126
  return count;
117
127
  }
128
+ async function chmodShellScripts(dir) {
129
+ if (!existsSync(dir))
130
+ return;
131
+ const { chmod } = await import('node:fs/promises');
132
+ const entries = await readdir(dir, { withFileTypes: true });
133
+ for (const entry of entries) {
134
+ if (entry.isFile() && entry.name.endsWith('.sh')) {
135
+ await chmod(join(dir, entry.name), 0o755);
136
+ }
137
+ }
138
+ }
139
+ async function mergeSettingsHook(projectDir) {
140
+ const snippetPath = join(projectDir, 'scripts', 'surfer-gate', 'settings-snippet.json');
141
+ const settingsPath = join(projectDir, '.claude', 'settings.json');
142
+ if (!existsSync(snippetPath))
143
+ return;
144
+ const snippet = JSON.parse(await readFile(snippetPath, 'utf-8'));
145
+ const productionHook = snippet?.production_hook;
146
+ if (!productionHook?.PreToolUse)
147
+ return;
148
+ let settings = {};
149
+ if (existsSync(settingsPath)) {
150
+ try {
151
+ settings = JSON.parse(await readFile(settingsPath, 'utf-8'));
152
+ }
153
+ catch {
154
+ // Existing settings.json is unreadable — leave it alone.
155
+ return;
156
+ }
157
+ }
158
+ else {
159
+ await mkdir(join(projectDir, '.claude'), { recursive: true });
160
+ }
161
+ const existingHooks = (settings.hooks ?? {});
162
+ const existingPreTool = (existingHooks.PreToolUse ?? []);
163
+ const alreadyHasGate = existingPreTool.some((entry) => {
164
+ const hooks = (entry?.hooks ?? []);
165
+ return hooks.some((h) => typeof h?.command === 'string' && h.command.includes('surfer-gate/check.sh'));
166
+ });
167
+ if (alreadyHasGate)
168
+ return;
169
+ settings.hooks = {
170
+ ...existingHooks,
171
+ PreToolUse: [...existingPreTool, ...productionHook.PreToolUse],
172
+ };
173
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
174
+ }
118
175
  // ── Identity Injection ───────────────────────────────────
119
176
  async function injectIdentity(projectDir, config) {
120
177
  const claudePath = join(projectDir, 'CLAUDE.md');
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Paris's Route Optimizer — ROI-weighted campaign sequencing (v12.3).
3
+ *
4
+ * Given multiple possible campaigns (from the proposal generator), Paris
5
+ * computes the optimal execution order based on estimated ROI, dependencies,
6
+ * risk, and urgency.
7
+ *
8
+ * PRD Reference: ROADMAP v12.3, DEEP_CURRENT.md Paris's role
9
+ */
10
+ import type { CampaignProposal } from './campaign-proposer.js';
11
+ import type { SituationModel } from './deep-current.js';
12
+ interface RouteScore {
13
+ proposal: CampaignProposal;
14
+ roiScore: number;
15
+ urgencyScore: number;
16
+ riskScore: number;
17
+ totalScore: number;
18
+ }
19
+ /**
20
+ * Score and rank campaign proposals by optimal execution order.
21
+ * Returns proposals sorted by total score (highest first = execute first).
22
+ */
23
+ export declare function optimizeRoute(proposals: CampaignProposal[], model: SituationModel): RouteScore[];
24
+ /**
25
+ * Pick the single best campaign to execute next.
26
+ */
27
+ export declare function pickBestCampaign(proposals: CampaignProposal[], model: SituationModel): CampaignProposal | null;
28
+ export type { RouteScore };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Paris's Route Optimizer — ROI-weighted campaign sequencing (v12.3).
3
+ *
4
+ * Given multiple possible campaigns (from the proposal generator), Paris
5
+ * computes the optimal execution order based on estimated ROI, dependencies,
6
+ * risk, and urgency.
7
+ *
8
+ * PRD Reference: ROADMAP v12.3, DEEP_CURRENT.md Paris's role
9
+ */
10
+ // ── Scoring Weights ───────────────────────────────────
11
+ const WEIGHTS = {
12
+ roi: 0.40,
13
+ urgency: 0.35,
14
+ risk: 0.25, // Inverted — lower risk gets higher score
15
+ };
16
+ // ── ROI Estimation ────────────────────────────────────
17
+ function estimateRoi(proposal, model) {
18
+ // ROI = (dimension improvement potential) / (estimated effort)
19
+ const currentScore = proposal.dimensionScore;
20
+ const potentialGain = Math.min(30, 100 - currentScore); // Cap at 30-point improvement
21
+ const effort = proposal.estimatedSessions;
22
+ // Higher gain per session = higher ROI
23
+ const roiRatio = potentialGain / Math.max(effort, 1);
24
+ return Math.min(100, Math.round(roiRatio * 10)); // Scale to 0-100
25
+ }
26
+ // ── Urgency Scoring ───────────────────────────────────
27
+ function scoreUrgency(proposal, model) {
28
+ const dim = proposal.dimension;
29
+ const score = proposal.dimensionScore;
30
+ // Critical defects are always urgent
31
+ if (dim === 'quality' && score < 30)
32
+ return 100;
33
+ // Security issues are urgent
34
+ if (dim === 'performance' && model.lastSiteScan && !model.lastSiteScan.security.https)
35
+ return 90;
36
+ // Low scores are more urgent
37
+ if (score < 20)
38
+ return 80;
39
+ if (score < 40)
40
+ return 60;
41
+ if (score < 60)
42
+ return 40;
43
+ // Revenue is urgent for OPERATING projects
44
+ if (dim === 'revenuePotential' && model.projectState === 'OPERATING')
45
+ return 70;
46
+ return 20; // Low urgency by default
47
+ }
48
+ // ── Risk Scoring ──────────────────────────────────────
49
+ function scoreRisk(proposal) {
50
+ // Revenue/payment campaigns are higher risk (real money)
51
+ if (proposal.dimension === 'revenuePotential')
52
+ return 70;
53
+ // Feature campaigns have moderate risk (new code)
54
+ if (proposal.dimension === 'featureCompleteness')
55
+ return 50;
56
+ // Quality and performance campaigns are low risk
57
+ if (proposal.dimension === 'quality')
58
+ return 20;
59
+ if (proposal.dimension === 'performance')
60
+ return 25;
61
+ // Growth foundation is low risk (additive, no existing code modified)
62
+ if (proposal.dimension === 'growthReadiness')
63
+ return 30;
64
+ return 40;
65
+ }
66
+ // ── Route Optimization ────────────────────────────────
67
+ /**
68
+ * Score and rank campaign proposals by optimal execution order.
69
+ * Returns proposals sorted by total score (highest first = execute first).
70
+ */
71
+ export function optimizeRoute(proposals, model) {
72
+ const scored = proposals.map(proposal => {
73
+ const roiScore = estimateRoi(proposal, model);
74
+ const urgencyScore = scoreUrgency(proposal, model);
75
+ const riskScore = 100 - scoreRisk(proposal); // Invert: low risk = high score
76
+ const totalScore = Math.round(roiScore * WEIGHTS.roi +
77
+ urgencyScore * WEIGHTS.urgency +
78
+ riskScore * WEIGHTS.risk);
79
+ return { proposal, roiScore, urgencyScore, riskScore, totalScore };
80
+ });
81
+ // Sort by total score descending (best first)
82
+ scored.sort((a, b) => b.totalScore - a.totalScore);
83
+ return scored;
84
+ }
85
+ /**
86
+ * Pick the single best campaign to execute next.
87
+ */
88
+ export function pickBestCampaign(proposals, model) {
89
+ if (proposals.length === 0)
90
+ return null;
91
+ const ranked = optimizeRoute(proposals, model);
92
+ return ranked[0].proposal;
93
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Service Install — launchd/systemd/Task Scheduler integration (§9.18, §9.19.2).
3
+ *
4
+ * Creates system services for:
5
+ * 1. Heartbeat daemon (com.voidforge.heartbeat)
6
+ * 2. Wizard server (com.voidforge.server) — persistent when Cultivation is installed
7
+ *
8
+ * PRD Reference: §9.18 (macOS LaunchAgent), §9.19.2 (two services)
9
+ */
10
+ export declare function installHeartbeatService(): Promise<{
11
+ method: string;
12
+ path: string;
13
+ }>;
14
+ export declare function installServerService(port?: number): Promise<{
15
+ method: string;
16
+ path: string;
17
+ }>;
18
+ export declare function uninstallServices(): Promise<void>;