@esoteric-logic/praxis-harness 2.15.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.
@@ -53,7 +53,7 @@ function ok(msg) {
53
53
 
54
54
  // ── Main ─────────────────────────────────────────────────────
55
55
 
56
- /** Validate a standalone project — check files exist, report char budgets. */
56
+ /** Validate a standalone project — check files exist, report char budgets. Returns result rows for summary. */
57
57
  function validateStandalone(projectName, projectDir, projectConfig) {
58
58
  console.log(`\nValidating standalone: ${projectName}`);
59
59
 
@@ -64,6 +64,7 @@ function validateStandalone(projectName, projectDir, projectConfig) {
64
64
  { file: 'project-instructions.md', budget: CHAR_BUDGETS['claude-project'], required: false, label: 'Claude Project' },
65
65
  ];
66
66
 
67
+ const results = [];
67
68
  let missingGenerable = [];
68
69
 
69
70
  for (const item of inventory) {
@@ -75,14 +76,18 @@ function validateStandalone(projectName, projectDir, projectConfig) {
75
76
  const sizeInfo = item.budget < Infinity
76
77
  ? `${charCount} chars (budget: ${item.budget})`
77
78
  : `${charCount} chars, ${lineCount} lines`;
79
+ const overBudget = charCount > item.budget;
78
80
 
79
- if (charCount > item.budget) {
81
+ if (overBudget) {
80
82
  warn(`${item.file} exceeds budget: ${charCount} chars (limit: ${item.budget})`);
81
83
  } else {
82
84
  ok(`${item.file} — ${sizeInfo}`);
83
85
  }
86
+
87
+ results.push({ project: projectName, target: item.label, chars: charCount, budget: item.budget, status: overBudget ? 'OVER' : 'ok' });
84
88
  } else if (item.required) {
85
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' });
86
91
  } else {
87
92
  missingGenerable.push(item.file);
88
93
  }
@@ -111,8 +116,11 @@ function validateStandalone(projectName, projectDir, projectConfig) {
111
116
  ok(`${refs.length} reference file(s): ${refs.join(', ')}`);
112
117
  }
113
118
  }
119
+
120
+ return { mode: 'standalone', results };
114
121
  }
115
122
 
123
+ /** Compile a project. Returns { mode, results[] } for summary table. */
116
124
  function compileProject(projectName, targets) {
117
125
  const projectDir = path.join(PROJECTS_DIR, projectName);
118
126
  const configPath = path.join(projectDir, 'prompt-config.yaml');
@@ -123,22 +131,18 @@ function compileProject(projectName, targets) {
123
131
 
124
132
  const projectConfig = yaml.load(fs.readFileSync(configPath, 'utf8'));
125
133
 
126
- // Standalone mode: validate files, report budgets, skip compilation
127
134
  if (projectConfig.mode === 'standalone') {
128
- validateStandalone(projectName, projectDir, projectConfig);
129
- return;
135
+ return validateStandalone(projectName, projectDir, projectConfig);
130
136
  }
131
137
 
132
138
  const praxisConfig = loadPraxisConfig();
133
139
 
134
- // Build vars map: project vars + praxis config + project name
135
140
  const vars = {
136
141
  ...praxisConfig,
137
142
  ...(projectConfig.vars || {}),
138
143
  project: projectConfig.project || projectName,
139
144
  };
140
145
 
141
- // Load profile: from named profile, project-local blocks, or _base fallback
142
146
  let profile;
143
147
  if (projectConfig.profile) {
144
148
  profile = loadProfile(projectConfig.profile, fail);
@@ -153,7 +157,7 @@ function compileProject(projectName, targets) {
153
157
  const profileName = projectConfig.profile || 'project-local';
154
158
  console.log(`\nCompiling: ${projectName} (profile: ${profileName})`);
155
159
 
156
- const assemblers = {
160
+ const targetAssemblers = {
157
161
  'claude-code': assembleClaudeCode,
158
162
  'claude-project': assembleClaudeProject,
159
163
  'perplexity-space': assemblePerplexitySpace,
@@ -165,14 +169,14 @@ function compileProject(projectName, targets) {
165
169
  'perplexity-space': 'space-instructions.md',
166
170
  };
167
171
 
172
+ const results = [];
173
+
168
174
  for (const target of targets) {
169
175
  const blocks = loadBlocks(profile, target, warn);
170
- let output = assemblers[target](blocks, projectConfig, vars);
176
+ let output = targetAssemblers[target](blocks, projectConfig, vars);
171
177
 
172
- // Interpolate variables
173
178
  output = interpolate(output, vars);
174
179
 
175
- // Validate no unresolved placeholders
176
180
  const unresolved = findUnresolved(output);
177
181
  if (unresolved.length > 0) {
178
182
  if (STRICT_MODE) {
@@ -181,9 +185,9 @@ function compileProject(projectName, targets) {
181
185
  warn(`Unresolved placeholders in ${target}: ${unresolved.join(', ')}`);
182
186
  }
183
187
 
184
- // Check character budget
185
188
  const budget = CHAR_BUDGETS[target];
186
- if (output.length > budget) {
189
+ const overBudget = output.length > budget;
190
+ if (overBudget) {
187
191
  if (STRICT_MODE) {
188
192
  fail(`[strict] ${target} exceeds budget: ${output.length} chars (limit: ${budget})`);
189
193
  }
@@ -191,32 +195,38 @@ function compileProject(projectName, targets) {
191
195
  }
192
196
 
193
197
  const outputPath = path.join(projectDir, outputNames[target]);
198
+ let status = 'wrote';
194
199
 
195
- // Preview mode: print to stdout instead of writing
196
200
  if (PREVIEW_MODE) {
197
201
  console.log(`\n--- ${outputNames[target]} (${output.length} chars) ---`);
198
202
  console.log(output);
199
- continue;
200
- }
201
-
202
- // Diff mode: show diff against existing file before writing
203
- if (DIFF_MODE && fs.existsSync(outputPath)) {
203
+ status = 'preview';
204
+ } else if (DIFF_MODE && fs.existsSync(outputPath)) {
204
205
  const existing = fs.readFileSync(outputPath, 'utf8');
205
206
  if (existing === output) {
206
207
  ok(`${outputNames[target]} — unchanged (${output.length} chars)`);
207
- continue;
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';
208
219
  }
209
- console.log(`\n--- ${outputNames[target]} changed ---`);
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(` +${addedCount} lines added, -${removedCount} lines removed`);
220
+ } else {
221
+ fs.writeFileSync(outputPath, output, 'utf8');
222
+ ok(`${outputNames[target]} ${output.length} chars → ${outputPath}`);
215
223
  }
216
224
 
217
- fs.writeFileSync(outputPath, output, 'utf8');
218
- ok(`${outputNames[target]} ${output.length} chars ${outputPath}`);
225
+ if (overBudget) status = 'OVER';
226
+ results.push({ project: projectName, target, chars: output.length, budget, status });
219
227
  }
228
+
229
+ return { mode: 'compiled', results };
220
230
  }
221
231
 
222
232
  // ── CLI ──────────────────────────────────────────────────────
@@ -225,12 +235,13 @@ function main() {
225
235
  const args = process.argv.slice(2);
226
236
 
227
237
  if (args.length === 0 || args.includes('--help')) {
228
- console.log('Usage: prompt-compile <project-name|--all> [options]');
238
+ console.log('Usage: prompt-compile <project-name|--all|--sync> [options]');
229
239
  console.log('Options:');
230
240
  console.log(' --target <target> claude-code|claude-project|perplexity-space|all');
231
241
  console.log(' --preview Print output to stdout without writing files');
232
242
  console.log(' --diff Show what changed before writing');
233
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');
234
245
  console.log(' --list List all projects with mode and file status');
235
246
  process.exit(0);
236
247
  }
@@ -290,7 +301,12 @@ function main() {
290
301
  }
291
302
  const projectArg = args.find((a) => !a.startsWith('--') && !flagValues.has(a));
292
303
 
293
- if (args.includes('--all')) {
304
+ const isSync = args.includes('--sync');
305
+ if (isSync) {
306
+ DIFF_MODE = true;
307
+ }
308
+
309
+ if (args.includes('--all') || isSync) {
294
310
  const projectDirs = fs.readdirSync(PROJECTS_DIR)
295
311
  .filter((d) => d !== '_template' && fs.statSync(path.join(PROJECTS_DIR, d)).isDirectory());
296
312
 
@@ -298,16 +314,42 @@ function main() {
298
314
  fail('No projects found in prompts/projects/');
299
315
  }
300
316
 
317
+ const allResults = [];
301
318
  for (const projectName of projectDirs) {
302
- compileProject(projectName, targets);
319
+ const result = compileProject(projectName, targets);
320
+ if (result) allResults.push(result);
303
321
  }
322
+
323
+ printSummaryTable(allResults);
304
324
  } else if (projectArg) {
305
325
  compileProject(projectArg, targets);
306
326
  } else {
307
- fail('Specify a project name or use --all');
327
+ fail('Specify a project name or use --all / --sync');
308
328
  }
309
329
 
310
330
  console.log('\nDone.');
311
331
  }
312
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
+
313
355
  main();
package/lib/assemblers.js CHANGED
@@ -87,13 +87,6 @@ function assembleClaudeCode(blocks, projectConfig, vars) {
87
87
  lines.push(append.extra_notes.trim(), '');
88
88
  }
89
89
 
90
- // Vault Project
91
- if (vars.vault_project_path) {
92
- lines.push('## Vault Project');
93
- lines.push(`- **Vault path**: ${vars.vault_project_path}`);
94
- lines.push('');
95
- }
96
-
97
90
  // Standard footer
98
91
  lines.push('## Verification');
99
92
  lines.push('- Before marking any task complete, run the test suite');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esoteric-logic/praxis-harness",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "Layered Claude Code harness — workflow discipline, AI-Kits, persistent vault integration",
5
5
  "bin": {
6
6
  "praxis-harness": "./bin/praxis.js"
@@ -69,9 +69,6 @@ Praxis owns the outer loop: discuss → plan → execute → verify → simplify
69
69
  - No AI-generated comments or attributions in code or commits
70
70
  - Prefer simple, readable code over clever abstractions
71
71
 
72
- ## Vault Project
73
- - **Vault path**: /Users/esoteric-mac/Documents/Esoteric Vault/01_Projects/Personal/_active/praxis
74
-
75
72
  ## Verification
76
73
  - Before marking any task complete, run the test suite
77
74
  - Check logs before claiming a bug is fixed