@esoteric-logic/praxis-harness 2.14.0 → 2.16.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 (39) hide show
  1. package/base/skills/px-prompt/SKILL.md +373 -0
  2. package/bin/praxis.js +7 -0
  3. package/bin/prompt-blocks.js +145 -0
  4. package/bin/prompt-compile.js +355 -0
  5. package/lib/assemblers.js +242 -0
  6. package/lib/loader.js +148 -0
  7. package/package.json +10 -3
  8. package/prompts/blocks/behaviors/flag-confidence.md +13 -0
  9. package/prompts/blocks/behaviors/handle-uncertainty.md +13 -0
  10. package/prompts/blocks/behaviors/no-flattery.md +15 -0
  11. package/prompts/blocks/behaviors/recommend-with-reasons.md +13 -0
  12. package/prompts/blocks/behaviors/verify-before-reporting.md +13 -0
  13. package/prompts/blocks/context/mcp-servers.md +12 -0
  14. package/prompts/blocks/context/official-docs-first.md +16 -0
  15. package/prompts/blocks/context/praxis-workflow.md +20 -0
  16. package/prompts/blocks/context/vault-integration.md +13 -0
  17. package/prompts/blocks/domains/cloud-infrastructure.md +13 -0
  18. package/prompts/blocks/domains/govcon.md +13 -0
  19. package/prompts/blocks/domains/web-development.md +13 -0
  20. package/prompts/blocks/formats/concise-responses.md +13 -0
  21. package/prompts/blocks/formats/what-so-what-now-what.md +16 -0
  22. package/prompts/blocks/identity/research-partner.md +10 -0
  23. package/prompts/blocks/identity/senior-engineer.md +15 -0
  24. package/prompts/blocks/identity/solutions-architect.md +13 -0
  25. package/prompts/profiles/_base.yaml +15 -0
  26. package/prompts/profiles/federal-cloud.yaml +18 -0
  27. package/prompts/profiles/praxis.yaml +13 -0
  28. package/prompts/projects/_template/prompt-config.yaml +34 -0
  29. package/prompts/projects/maximus/prompt-config.yaml +13 -0
  30. package/prompts/projects/maximus/references/maturity-questions.md +634 -0
  31. package/prompts/projects/maximus/references/phase-maturity-matrix.md +188 -0
  32. package/prompts/projects/maximus/references/proposal-writing-standards.md +367 -0
  33. package/prompts/projects/maximus/space-instructions.md +67 -0
  34. package/prompts/projects/maximus/system-prompt.md +641 -0
  35. package/prompts/projects/praxis/CLAUDE.md +81 -0
  36. package/prompts/projects/praxis/project-instructions.md +24 -0
  37. package/prompts/projects/praxis/prompt-config.yaml +40 -0
  38. package/prompts/projects/praxis/space-instructions.md +28 -0
  39. package/scripts/lint-harness.sh +42 -0
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const yaml = require('js-yaml');
7
+
8
+ const {
9
+ TARGETS,
10
+ PROMPTS_DIR,
11
+ loadPraxisConfig,
12
+ loadProfile,
13
+ mergeProfiles,
14
+ loadBlocks,
15
+ applyOverrides,
16
+ } = require('../lib/loader');
17
+
18
+ const {
19
+ interpolate,
20
+ findUnresolved,
21
+ assembleClaudeCode,
22
+ assembleClaudeProject,
23
+ assemblePerplexitySpace,
24
+ } = require('../lib/assemblers');
25
+
26
+ const PROJECTS_DIR = path.join(PROMPTS_DIR, 'projects');
27
+
28
+ const CHAR_BUDGETS = {
29
+ 'claude-code': Infinity,
30
+ 'claude-project': 2500,
31
+ 'perplexity-space': 4000,
32
+ };
33
+
34
+ // Global flags set by CLI parser
35
+ let PREVIEW_MODE = false;
36
+ let DIFF_MODE = false;
37
+ let STRICT_MODE = false;
38
+
39
+ // ── Helpers ──────────────────────────────────────────────────
40
+
41
+ function fail(msg) {
42
+ console.error(`\x1b[31mERROR:\x1b[0m ${msg}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ function warn(msg) {
47
+ console.error(`\x1b[33mWARN:\x1b[0m ${msg}`);
48
+ }
49
+
50
+ function ok(msg) {
51
+ console.log(`\x1b[32m✓\x1b[0m ${msg}`);
52
+ }
53
+
54
+ // ── Main ─────────────────────────────────────────────────────
55
+
56
+ /** Validate a standalone project — check files exist, report char budgets. Returns result rows for summary. */
57
+ function validateStandalone(projectName, projectDir, projectConfig) {
58
+ console.log(`\nValidating standalone: ${projectName}`);
59
+
60
+ const inventory = [
61
+ { file: 'system-prompt.md', budget: Infinity, required: true, label: 'System Prompt (Claude Projects)' },
62
+ { file: 'CLAUDE.md', budget: Infinity, required: false, label: 'Claude Code' },
63
+ { file: 'space-instructions.md', budget: CHAR_BUDGETS['perplexity-space'], required: false, label: 'Perplexity Space' },
64
+ { file: 'project-instructions.md', budget: CHAR_BUDGETS['claude-project'], required: false, label: 'Claude Project' },
65
+ ];
66
+
67
+ const results = [];
68
+ let missingGenerable = [];
69
+
70
+ for (const item of inventory) {
71
+ const filePath = path.join(projectDir, item.file);
72
+ if (fs.existsSync(filePath)) {
73
+ const content = fs.readFileSync(filePath, 'utf8');
74
+ const charCount = content.length;
75
+ const lineCount = content.split('\n').length;
76
+ const sizeInfo = item.budget < Infinity
77
+ ? `${charCount} chars (budget: ${item.budget})`
78
+ : `${charCount} chars, ${lineCount} lines`;
79
+ const overBudget = charCount > item.budget;
80
+
81
+ if (overBudget) {
82
+ warn(`${item.file} exceeds budget: ${charCount} chars (limit: ${item.budget})`);
83
+ } else {
84
+ ok(`${item.file} — ${sizeInfo}`);
85
+ }
86
+
87
+ results.push({ project: projectName, target: item.label, chars: charCount, budget: item.budget, status: overBudget ? 'OVER' : 'ok' });
88
+ } else if (item.required) {
89
+ warn(`${item.file} MISSING — standalone projects require a system prompt`);
90
+ results.push({ project: projectName, target: item.label, chars: 0, budget: item.budget, status: 'MISSING' });
91
+ } else {
92
+ missingGenerable.push(item.file);
93
+ }
94
+ }
95
+
96
+ if (missingGenerable.length > 0) {
97
+ console.log(`\n Missing platform outputs: ${missingGenerable.join(', ')}`);
98
+ console.log(' Run /px-prompt ' + projectName + ' to auto-generate from system-prompt.md');
99
+ }
100
+
101
+ // Version consistency check
102
+ const systemPromptPath = path.join(projectDir, 'system-prompt.md');
103
+ if (fs.existsSync(systemPromptPath) && projectConfig.version) {
104
+ const spContent = fs.readFileSync(systemPromptPath, 'utf8');
105
+ const versionMatch = spContent.match(/^version:\s*["']?([^"'\n]+)/m);
106
+ if (versionMatch && versionMatch[1].trim() !== String(projectConfig.version).trim()) {
107
+ warn(`Version mismatch: prompt-config.yaml says "${projectConfig.version}" but system-prompt.md says "${versionMatch[1].trim()}"`);
108
+ }
109
+ }
110
+
111
+ // Check for reference files
112
+ const refsDir = path.join(projectDir, 'references');
113
+ if (fs.existsSync(refsDir)) {
114
+ const refs = fs.readdirSync(refsDir).filter((f) => f.endsWith('.md'));
115
+ if (refs.length > 0) {
116
+ ok(`${refs.length} reference file(s): ${refs.join(', ')}`);
117
+ }
118
+ }
119
+
120
+ return { mode: 'standalone', results };
121
+ }
122
+
123
+ /** Compile a project. Returns { mode, results[] } for summary table. */
124
+ function compileProject(projectName, targets) {
125
+ const projectDir = path.join(PROJECTS_DIR, projectName);
126
+ const configPath = path.join(projectDir, 'prompt-config.yaml');
127
+
128
+ if (!fs.existsSync(configPath)) {
129
+ fail(`Project config not found: ${configPath}\nRun /px-prompt <project-name> to create one.`);
130
+ }
131
+
132
+ const projectConfig = yaml.load(fs.readFileSync(configPath, 'utf8'));
133
+
134
+ if (projectConfig.mode === 'standalone') {
135
+ return validateStandalone(projectName, projectDir, projectConfig);
136
+ }
137
+
138
+ const praxisConfig = loadPraxisConfig();
139
+
140
+ const vars = {
141
+ ...praxisConfig,
142
+ ...(projectConfig.vars || {}),
143
+ project: projectConfig.project || projectName,
144
+ };
145
+
146
+ let profile;
147
+ if (projectConfig.profile) {
148
+ profile = loadProfile(projectConfig.profile, fail);
149
+ } else if (projectConfig.blocks) {
150
+ const base = loadProfile('_base', fail);
151
+ profile = mergeProfiles(base, { blocks: projectConfig.blocks });
152
+ } else {
153
+ profile = loadProfile('_base', fail);
154
+ }
155
+ profile = applyOverrides(profile, projectConfig.overrides);
156
+
157
+ const profileName = projectConfig.profile || 'project-local';
158
+ console.log(`\nCompiling: ${projectName} (profile: ${profileName})`);
159
+
160
+ const targetAssemblers = {
161
+ 'claude-code': assembleClaudeCode,
162
+ 'claude-project': assembleClaudeProject,
163
+ 'perplexity-space': assemblePerplexitySpace,
164
+ };
165
+
166
+ const outputNames = {
167
+ 'claude-code': 'CLAUDE.md',
168
+ 'claude-project': 'project-instructions.md',
169
+ 'perplexity-space': 'space-instructions.md',
170
+ };
171
+
172
+ const results = [];
173
+
174
+ for (const target of targets) {
175
+ const blocks = loadBlocks(profile, target, warn);
176
+ let output = targetAssemblers[target](blocks, projectConfig, vars);
177
+
178
+ output = interpolate(output, vars);
179
+
180
+ const unresolved = findUnresolved(output);
181
+ if (unresolved.length > 0) {
182
+ if (STRICT_MODE) {
183
+ fail(`[strict] Unresolved placeholders in ${target}: ${unresolved.join(', ')}`);
184
+ }
185
+ warn(`Unresolved placeholders in ${target}: ${unresolved.join(', ')}`);
186
+ }
187
+
188
+ const budget = CHAR_BUDGETS[target];
189
+ const overBudget = output.length > budget;
190
+ if (overBudget) {
191
+ if (STRICT_MODE) {
192
+ fail(`[strict] ${target} exceeds budget: ${output.length} chars (limit: ${budget})`);
193
+ }
194
+ warn(`${target} output exceeds budget: ${output.length} chars (limit: ${budget})`);
195
+ }
196
+
197
+ const outputPath = path.join(projectDir, outputNames[target]);
198
+ let status = 'wrote';
199
+
200
+ if (PREVIEW_MODE) {
201
+ console.log(`\n--- ${outputNames[target]} (${output.length} chars) ---`);
202
+ console.log(output);
203
+ status = 'preview';
204
+ } else if (DIFF_MODE && fs.existsSync(outputPath)) {
205
+ const existing = fs.readFileSync(outputPath, 'utf8');
206
+ if (existing === output) {
207
+ ok(`${outputNames[target]} — unchanged (${output.length} chars)`);
208
+ status = 'unchanged';
209
+ } else {
210
+ const existingLines = existing.split('\n');
211
+ const outputLines = output.split('\n');
212
+ const addedCount = outputLines.filter((l) => !existingLines.includes(l)).length;
213
+ const removedCount = existingLines.filter((l) => !outputLines.includes(l)).length;
214
+ console.log(`\n--- ${outputNames[target]} changed ---`);
215
+ console.log(` +${addedCount} lines added, -${removedCount} lines removed`);
216
+ fs.writeFileSync(outputPath, output, 'utf8');
217
+ ok(`${outputNames[target]} — ${output.length} chars → ${outputPath}`);
218
+ status = 'updated';
219
+ }
220
+ } else {
221
+ fs.writeFileSync(outputPath, output, 'utf8');
222
+ ok(`${outputNames[target]} — ${output.length} chars → ${outputPath}`);
223
+ }
224
+
225
+ if (overBudget) status = 'OVER';
226
+ results.push({ project: projectName, target, chars: output.length, budget, status });
227
+ }
228
+
229
+ return { mode: 'compiled', results };
230
+ }
231
+
232
+ // ── CLI ──────────────────────────────────────────────────────
233
+
234
+ function main() {
235
+ const args = process.argv.slice(2);
236
+
237
+ if (args.length === 0 || args.includes('--help')) {
238
+ console.log('Usage: prompt-compile <project-name|--all|--sync> [options]');
239
+ console.log('Options:');
240
+ console.log(' --target <target> claude-code|claude-project|perplexity-space|all');
241
+ console.log(' --preview Print output to stdout without writing files');
242
+ console.log(' --diff Show what changed before writing');
243
+ console.log(' --strict Exit with error on budget overruns or unresolved vars');
244
+ console.log(' --sync Compile all projects with diff, show summary table');
245
+ console.log(' --list List all projects with mode and file status');
246
+ process.exit(0);
247
+ }
248
+
249
+ // --list mode: show all projects
250
+ if (args.includes('--list')) {
251
+ const projectDirs = fs.readdirSync(PROJECTS_DIR)
252
+ .filter((d) => d !== '_template' && fs.statSync(path.join(PROJECTS_DIR, d)).isDirectory());
253
+ if (projectDirs.length === 0) {
254
+ console.log('No projects found.');
255
+ process.exit(0);
256
+ }
257
+ console.log(`${'Project'.padEnd(20)} ${'Mode'.padEnd(12)} ${'System Prompt'.padEnd(15)} ${'Claude Proj'.padEnd(15)} ${'Perplexity'.padEnd(15)} ${'CLAUDE.md'.padEnd(12)} Refs`);
258
+ console.log('-'.repeat(95));
259
+ for (const name of projectDirs) {
260
+ const dir = path.join(PROJECTS_DIR, name);
261
+ const cfgPath = path.join(dir, 'prompt-config.yaml');
262
+ const cfg = fs.existsSync(cfgPath) ? yaml.load(fs.readFileSync(cfgPath, 'utf8')) : {};
263
+ const mode = cfg.mode || 'compiled';
264
+ const fileStatus = (f) => {
265
+ const p = path.join(dir, f);
266
+ if (!fs.existsSync(p)) return '—';
267
+ return `${fs.readFileSync(p, 'utf8').length} chars`;
268
+ };
269
+ const refsDir = path.join(dir, 'references');
270
+ const refCount = fs.existsSync(refsDir)
271
+ ? fs.readdirSync(refsDir).filter((f) => f.endsWith('.md')).length
272
+ : 0;
273
+ console.log(
274
+ `${name.padEnd(20)} ${mode.padEnd(12)} ${fileStatus('system-prompt.md').padEnd(15)} ${fileStatus('project-instructions.md').padEnd(15)} ${fileStatus('space-instructions.md').padEnd(15)} ${fileStatus('CLAUDE.md').padEnd(12)} ${refCount}`
275
+ );
276
+ }
277
+ process.exit(0);
278
+ }
279
+
280
+ // Parse global flags
281
+ PREVIEW_MODE = args.includes('--preview');
282
+ DIFF_MODE = args.includes('--diff');
283
+ STRICT_MODE = args.includes('--strict');
284
+
285
+ // Parse --target flag
286
+ const targetIdx = args.indexOf('--target');
287
+ let targets = TARGETS;
288
+ if (targetIdx !== -1 && args[targetIdx + 1]) {
289
+ const targetArg = args[targetIdx + 1];
290
+ if (TARGETS.includes(targetArg)) {
291
+ targets = [targetArg];
292
+ } else if (targetArg !== 'all') {
293
+ fail(`Unknown target: ${targetArg}. Use: ${TARGETS.join(', ')}, all`);
294
+ }
295
+ }
296
+
297
+ // Determine project(s) to compile
298
+ const flagValues = new Set();
299
+ if (targetIdx !== -1 && args[targetIdx + 1]) {
300
+ flagValues.add(args[targetIdx + 1]);
301
+ }
302
+ const projectArg = args.find((a) => !a.startsWith('--') && !flagValues.has(a));
303
+
304
+ const isSync = args.includes('--sync');
305
+ if (isSync) {
306
+ DIFF_MODE = true;
307
+ }
308
+
309
+ if (args.includes('--all') || isSync) {
310
+ const projectDirs = fs.readdirSync(PROJECTS_DIR)
311
+ .filter((d) => d !== '_template' && fs.statSync(path.join(PROJECTS_DIR, d)).isDirectory());
312
+
313
+ if (projectDirs.length === 0) {
314
+ fail('No projects found in prompts/projects/');
315
+ }
316
+
317
+ const allResults = [];
318
+ for (const projectName of projectDirs) {
319
+ const result = compileProject(projectName, targets);
320
+ if (result) allResults.push(result);
321
+ }
322
+
323
+ printSummaryTable(allResults);
324
+ } else if (projectArg) {
325
+ compileProject(projectArg, targets);
326
+ } else {
327
+ fail('Specify a project name or use --all / --sync');
328
+ }
329
+
330
+ console.log('\nDone.');
331
+ }
332
+
333
+ /** Print a summary table after --all or --sync compilation. */
334
+ function printSummaryTable(projectResults) {
335
+ if (projectResults.length === 0) return;
336
+
337
+ console.log('\n\x1b[1m── Summary ──────────────────────────────────────────────────────\x1b[0m');
338
+ console.log(
339
+ `${'Project'.padEnd(16)} ${'Mode'.padEnd(12)} ${'Target'.padEnd(18)} ${'Chars'.padEnd(10)} ${'Budget'.padEnd(10)} Status`
340
+ );
341
+ console.log('-'.repeat(78));
342
+
343
+ for (const { mode, results } of projectResults) {
344
+ for (const row of results) {
345
+ const budgetStr = row.budget === Infinity ? '—' : String(row.budget);
346
+ const statusColor = row.status === 'OVER' || row.status === 'MISSING' ? '\x1b[31m' :
347
+ row.status === 'unchanged' ? '\x1b[90m' : '\x1b[32m';
348
+ console.log(
349
+ `${row.project.padEnd(16)} ${mode.padEnd(12)} ${row.target.padEnd(18)} ${String(row.chars).padEnd(10)} ${budgetStr.padEnd(10)} ${statusColor}${row.status}\x1b[0m`
350
+ );
351
+ }
352
+ }
353
+ }
354
+
355
+ main();
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ /** Replace {{var}} placeholders with values from vars map. */
4
+ function interpolate(text, vars) {
5
+ return text.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
6
+ if (key in vars) return vars[key];
7
+ return `{{${key}}}`;
8
+ });
9
+ }
10
+
11
+ /** Check for unresolved {{...}} placeholders. */
12
+ function findUnresolved(text) {
13
+ const matches = text.match(/\{\{(\w+)\}\}/g);
14
+ return matches ? [...new Set(matches)] : [];
15
+ }
16
+
17
+ function assembleClaudeCode(blocks, projectConfig, vars) {
18
+ const lines = [];
19
+ const today = new Date().toISOString().slice(0, 10);
20
+ lines.push(`# ${vars.project || vars.repo_name || 'Project'}`);
21
+ lines.push(`<!-- Generated by Praxis prompt-compile | profile: ${projectConfig.profile} | ${today} -->`);
22
+ lines.push('');
23
+
24
+ // Identity
25
+ const identityBlocks = blocks.filter((b) => b.category === 'identity');
26
+ if (identityBlocks.length > 0) {
27
+ lines.push('## Identity');
28
+ for (const block of identityBlocks) lines.push(block.content, '');
29
+ }
30
+
31
+ // Global Rules reference
32
+ lines.push('## Global Rules');
33
+ lines.push('Inherits execution engine from `~/.claude/CLAUDE.md`.');
34
+ lines.push('');
35
+
36
+ // Git Identity
37
+ if (vars.git_email || vars.git_identity) {
38
+ lines.push('## Git Identity');
39
+ if (vars.git_identity) lines.push(`- **Type**: ${vars.git_identity}`);
40
+ if (vars.git_email) lines.push(`- **Email**: ${vars.git_email}`);
41
+ lines.push('');
42
+ }
43
+
44
+ // Behaviors
45
+ const behaviorBlocks = blocks.filter((b) => b.category === 'behaviors');
46
+ if (behaviorBlocks.length > 0) {
47
+ lines.push('## Behaviors');
48
+ for (const block of behaviorBlocks) lines.push(block.content, '');
49
+ }
50
+
51
+ // Domains
52
+ const domainBlocks = blocks.filter((b) => b.category === 'domains');
53
+ if (domainBlocks.length > 0) {
54
+ lines.push('## Domain Expertise');
55
+ for (const block of domainBlocks) lines.push(block.content, '');
56
+ }
57
+
58
+ // Formats
59
+ const formatBlocks = blocks.filter((b) => b.category === 'formats');
60
+ if (formatBlocks.length > 0) {
61
+ lines.push('## Output Format');
62
+ for (const block of formatBlocks) lines.push(block.content, '');
63
+ }
64
+
65
+ // Tech Stack + Commands (from claude_code_append)
66
+ const append = (projectConfig.overrides || {}).claude_code_append || {};
67
+ if (append.tech_stack) {
68
+ lines.push('## Tech Stack');
69
+ lines.push(append.tech_stack.trim(), '');
70
+ }
71
+ if (append.commands) {
72
+ lines.push('## Commands');
73
+ lines.push('```bash');
74
+ lines.push(append.commands.trim());
75
+ lines.push('```', '');
76
+ }
77
+
78
+ // Context
79
+ const contextBlocks = blocks.filter((b) => b.category === 'context');
80
+ if (contextBlocks.length > 0) {
81
+ for (const block of contextBlocks) lines.push(block.content, '');
82
+ }
83
+
84
+ // Extra notes
85
+ if (append.extra_notes) {
86
+ lines.push('## Important Notes');
87
+ lines.push(append.extra_notes.trim(), '');
88
+ }
89
+
90
+ // Standard footer
91
+ lines.push('## Verification');
92
+ lines.push('- Before marking any task complete, run the test suite');
93
+ lines.push('- Check logs before claiming a bug is fixed');
94
+ lines.push('');
95
+ lines.push('## Conventions');
96
+ lines.push('- **Commits**: conventional commits (feat:, fix:, docs:, refactor:, test:, chore:)');
97
+ lines.push('- **Branches**: `feat/description` or `fix/description`');
98
+ lines.push('');
99
+ lines.push('## Error Learning');
100
+ lines.push('<!-- Add project-specific learnings below -->');
101
+ lines.push('');
102
+
103
+ return lines.join('\n');
104
+ }
105
+
106
+ function assembleClaudeProject(blocks, projectConfig, vars) {
107
+ const lines = [];
108
+
109
+ // Layer 1: Role
110
+ const identityBlocks = blocks.filter((b) => b.category === 'identity');
111
+ lines.push('## Role');
112
+ if (projectConfig.description) lines.push(projectConfig.description);
113
+ for (const block of identityBlocks) lines.push(block.content);
114
+ lines.push('');
115
+
116
+ // Layer 2: Behavioral Constraints
117
+ const behaviorBlocks = blocks.filter((b) => b.category === 'behaviors');
118
+ if (behaviorBlocks.length > 0) {
119
+ lines.push('## Behavioral Constraints');
120
+ for (const block of behaviorBlocks) lines.push('- ' + block.content);
121
+ lines.push('');
122
+ }
123
+
124
+ // Layer 3: Domain Expertise
125
+ const domainBlocks = blocks.filter((b) => b.category === 'domains');
126
+ if (domainBlocks.length > 0) {
127
+ lines.push('## Domain Expertise');
128
+ for (const block of domainBlocks) lines.push('- ' + block.content);
129
+ lines.push('');
130
+ }
131
+
132
+ // Layer 3b: Output Format
133
+ const formatBlocks = blocks.filter((b) => b.category === 'formats');
134
+ if (formatBlocks.length > 0) {
135
+ lines.push('## Output Format');
136
+ for (const block of formatBlocks) lines.push('- ' + block.content);
137
+ lines.push('');
138
+ }
139
+
140
+ // Additional context
141
+ const append = (projectConfig.overrides || {}).claude_project_append || {};
142
+ if (append.additional_context) {
143
+ lines.push(append.additional_context.trim(), '');
144
+ }
145
+
146
+ // Layer 4: Quality Gates
147
+ if (append.quality_gates) {
148
+ lines.push('## Quality Gates');
149
+ lines.push(append.quality_gates.trim(), '');
150
+ }
151
+
152
+ // Context blocks (workflow, etc.)
153
+ const contextBlocks = blocks.filter((b) => b.category === 'context');
154
+ if (contextBlocks.length > 0) {
155
+ for (const block of contextBlocks) lines.push(block.content);
156
+ lines.push('');
157
+ }
158
+
159
+ // Layer 4b: Knowledge Files (if project has them)
160
+ const knowledgeFiles = projectConfig.knowledge_files || [];
161
+ if (knowledgeFiles.length > 0) {
162
+ lines.push('## Knowledge Files');
163
+ lines.push('Upload these alongside this prompt:');
164
+ for (const kf of knowledgeFiles) {
165
+ lines.push(`- **${kf.file}** — ${kf.description}`);
166
+ }
167
+ lines.push('');
168
+ }
169
+
170
+ // Layer 5: Failure Handling (always present)
171
+ lines.push('## When Uncertain');
172
+ lines.push('State uncertainty explicitly. Ask one clarifying question rather than guessing.');
173
+ lines.push('');
174
+
175
+ return lines.join('\n');
176
+ }
177
+
178
+ function assemblePerplexitySpace(blocks, projectConfig, vars) {
179
+ const lines = [];
180
+
181
+ // Purpose
182
+ const identityBlocks = blocks.filter((b) => b.category === 'identity');
183
+ lines.push('## Purpose');
184
+ if (projectConfig.description) lines.push(projectConfig.description);
185
+ for (const block of identityBlocks) lines.push(block.content);
186
+ lines.push('');
187
+
188
+ // Source Priority
189
+ const sourceBlocks = blocks.filter(
190
+ (b) => b.category === 'context' && (b.meta.tags || []).includes('sources')
191
+ );
192
+ if (sourceBlocks.length > 0) {
193
+ lines.push('## Source Priority');
194
+ for (const block of sourceBlocks) lines.push(block.content);
195
+ lines.push('');
196
+ }
197
+
198
+ // Domain Expertise
199
+ const domainBlocks = blocks.filter((b) => b.category === 'domains');
200
+ if (domainBlocks.length > 0) {
201
+ lines.push('## Domain Expertise');
202
+ for (const block of domainBlocks) lines.push(block.content);
203
+ lines.push('');
204
+ }
205
+
206
+ // Research Domains
207
+ const append = (projectConfig.overrides || {}).perplexity_space_append || {};
208
+ if (append.research_domains) {
209
+ lines.push('## Research Domains');
210
+ lines.push(append.research_domains.trim(), '');
211
+ }
212
+
213
+ // How to Answer
214
+ const behaviorBlocks = blocks.filter((b) => b.category === 'behaviors');
215
+ const formatBlocks = blocks.filter((b) => b.category === 'formats');
216
+ if (behaviorBlocks.length > 0 || formatBlocks.length > 0) {
217
+ lines.push('## How to Answer');
218
+ for (const block of behaviorBlocks) lines.push(block.content);
219
+ for (const block of formatBlocks) lines.push(block.content);
220
+ lines.push('');
221
+ }
222
+
223
+ // Anti-hallucination layer (auto-injected for all Perplexity outputs)
224
+ lines.push('## Accuracy Standards');
225
+ lines.push('- Flag your confidence level when synthesizing across sources');
226
+ lines.push('- Distinguish verified facts from analytical inferences');
227
+ lines.push('- If sources disagree, cite both and explain the discrepancy');
228
+ lines.push('- Never fabricate version numbers, API signatures, URLs, or code examples');
229
+ lines.push('- When information may be outdated (>12 months), note the publication date');
230
+ lines.push('- If you cannot find reliable sources, state that clearly rather than speculating');
231
+ lines.push('');
232
+
233
+ return lines.join('\n');
234
+ }
235
+
236
+ module.exports = {
237
+ interpolate,
238
+ findUnresolved,
239
+ assembleClaudeCode,
240
+ assembleClaudeProject,
241
+ assemblePerplexitySpace,
242
+ };