genesis-compiler 1.2.17 → 1.2.19

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.
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
 
4
4
  import { readInstalledAsset } from './assets.js';
5
5
  import { buildProjectIndex } from './code-index.js';
6
+ import { readEngineering, readEngineeringBaseline } from './engineering.js';
6
7
  import { GenesisError } from './errors.js';
7
8
  import { gitContext } from './git.js';
8
9
  import { isProjectContentPath } from './paths.js';
@@ -115,9 +116,30 @@ async function optionalStack(projectRoot) {
115
116
  try { return await readStack(projectRoot); } catch { return null; }
116
117
  }
117
118
 
119
+ async function optionalEngineering(projectRoot) {
120
+ try {
121
+ return await readEngineering(projectRoot);
122
+ } catch {
123
+ return {
124
+ guidance: [
125
+ '## Universal complexity gate',
126
+ '',
127
+ await readEngineeringBaseline(),
128
+ '',
129
+ 'The project engineering profile is invalid. Do not infer a replacement; run `genesis check` and ask the user before implementation if the selected approach matters.',
130
+ ].join('\n'),
131
+ profile: null,
132
+ status: 'invalid',
133
+ };
134
+ }
135
+ }
136
+
118
137
  export async function codexSessionContext({ projectRoot } = {}) {
119
138
  const root = (await gitContext(projectRoot)).repositoryRoot;
120
- const stack = await optionalStack(root);
139
+ const [stack, engineering] = await Promise.all([
140
+ optionalStack(root),
141
+ optionalEngineering(root),
142
+ ]);
121
143
  const selected = stack?.components.map(({ id }) => id) || [];
122
144
  const stackStatus = stack
123
145
  ? (selected.length > 0 ? selected.join(', ') : 'none')
@@ -127,6 +149,7 @@ export async function codexSessionContext({ projectRoot } = {}) {
127
149
  output: [
128
150
  'This is a Genesis-enriched project.',
129
151
  '- `genesis/blueprint.md` describes non-technical product intent.',
152
+ '- `genesis/engineering.md` selects the project engineering approach and any project-specific requirements.',
130
153
  '- `genesis/stack.md` selects optional technology guidance and verification.',
131
154
  '- `genesis/program/<subsystem>/` explains public operations and useful internal seams.',
132
155
  '- Project Agent Skills live in `.agents/skills/`; load applicable skills progressively.',
@@ -134,9 +157,15 @@ export async function codexSessionContext({ projectRoot } = {}) {
134
157
  '- Before creating a helper or public operation, run `genesis index <name-or-path>` and reuse an existing function when it already owns the behavior.',
135
158
  '- `.genesis/machine-city.json` is the detailed code/function map; `.genesis/program-city.json` is the simpler subsystem/operation map.',
136
159
  '- Program is fallible explanation; code, tests, and runtime behavior remain evidence.',
137
- '- Keep implementation focused; the Stop hook owns optional Stack Post-change work, Blueprint/Program reconciliation, and Deslop turns.',
138
- '- After a code-changing turn, Genesis may request separate post-change, explanation, cleanup, and final-summary turns.',
160
+ '- Before changing project files for a user-requested implementation, run `genesis hook authorize`. Never authorize answer, explanation, review, or diagnosis-only turns.',
161
+ '- Apply the engineering approach below to implementation and cleanup; the Stop hook owns optional Stack Post-change work, Blueprint/Program reconciliation, and Deslop turns.',
162
+ '- After an authorized code-changing turn, Genesis may request separate post-change, explanation, and cleanup turns.',
163
+ `Engineering profile: ${engineering.profile?.id || 'invalid; run \`genesis check\`'}.`,
139
164
  `Selected Stack components: ${stackStatus}.`,
165
+ '',
166
+ 'ENGINEERING APPROACH',
167
+ '',
168
+ engineering.guidance,
140
169
  ].join('\n'),
141
170
  };
142
171
  }
@@ -222,6 +251,7 @@ export async function recordCodexTurn({ input, projectRoot } = {}) {
222
251
  schemaVersion: HOOK_STATE_SCHEMA_VERSION,
223
252
  turnId: hookId(input, 'turn_id'),
224
253
  phase: 'implementation',
254
+ implementationAuthorized: false,
225
255
  snapshot: await repositorySnapshot(root),
226
256
  };
227
257
  await writeTurnState(statePath, state);
@@ -235,6 +265,27 @@ async function readTurnState(statePath) {
235
265
  }
236
266
  }
237
267
 
268
+ export async function authorizeCodexTurn({
269
+ projectRoot = process.cwd(),
270
+ sessionId = process.env.CODEX_SESSION_ID,
271
+ } = {}) {
272
+ if (!sessionId) {
273
+ throw new GenesisError(
274
+ 'CODEX_SESSION_ID_MISSING',
275
+ 'genesis hook authorize requires the current Codex session.',
276
+ );
277
+ }
278
+ const root = (await gitContext(projectRoot)).repositoryRoot;
279
+ const statePath = await hookStatePath(root, { session_id: sessionId });
280
+ const state = await readTurnState(statePath);
281
+ if (!state || state.schemaVersion !== HOOK_STATE_SCHEMA_VERSION || state.phase !== 'implementation') {
282
+ return { status: 'not-applicable' };
283
+ }
284
+ if (state.implementationAuthorized === true) return { status: 'unchanged' };
285
+ await writeTurnState(statePath, { ...state, implementationAuthorized: true });
286
+ return { status: 'authorized' };
287
+ }
288
+
238
289
  async function committedPaths(projectRoot, before, after) {
239
290
  if (before === after) return [];
240
291
  if (!after) return ['repository history changed'];
@@ -279,9 +330,10 @@ function changedPathLines(changedPaths) {
279
330
  }
280
331
 
281
332
  async function reconciliationContinuation(projectRoot, changedPaths) {
282
- const [instructions, stack] = await Promise.all([
333
+ const [instructions, stack, engineering] = await Promise.all([
283
334
  readInstalledAsset('reconcile'),
284
335
  optionalStack(projectRoot),
336
+ optionalEngineering(projectRoot),
285
337
  ]);
286
338
  return [
287
339
  'This is the automatic Genesis explanation turn for the preceding implementation turn.',
@@ -292,11 +344,15 @@ async function reconciliationContinuation(projectRoot, changedPaths) {
292
344
  ...changedPathLines(changedPaths),
293
345
  '',
294
346
  instructions.trim(),
347
+ '',
348
+ 'ENGINEERING APPROACH',
349
+ '',
350
+ engineering.guidance,
295
351
  ...(stack?.guidance ? ['', 'SELECTED STACK GUIDANCE', '', stack.guidance] : []),
296
352
  ].join('\n');
297
353
  }
298
354
 
299
- function postChangeContinuation(changedPaths, stack) {
355
+ function postChangeContinuation(changedPaths, stack, engineering) {
300
356
  return [
301
357
  'This is the one automatic Stack Post-change turn for the preceding implementation.',
302
358
  'Use the same conversation context and the Git-visible changes below.',
@@ -310,14 +366,19 @@ function postChangeContinuation(changedPaths, stack) {
310
366
  'COMPOSED STACK POST-CHANGE WORK',
311
367
  '',
312
368
  stack.postChange,
369
+ '',
370
+ 'ENGINEERING APPROACH',
371
+ '',
372
+ engineering.guidance,
313
373
  ...(stack.guidance ? ['', 'SELECTED STACK GUIDANCE', '', stack.guidance] : []),
314
374
  ].join('\n');
315
375
  }
316
376
 
317
377
  async function deslopContinuation(projectRoot, changedPaths) {
318
- const [instructions, stack] = await Promise.all([
378
+ const [instructions, stack, engineering] = await Promise.all([
319
379
  readInstalledAsset('deslop'),
320
380
  optionalStack(projectRoot),
381
+ optionalEngineering(projectRoot),
321
382
  ]);
322
383
  return [
323
384
  'This is the final automatic Genesis Deslop turn for the preceding implementation.',
@@ -328,6 +389,10 @@ async function deslopContinuation(projectRoot, changedPaths) {
328
389
  ...changedPathLines(changedPaths),
329
390
  '',
330
391
  instructions.trim(),
392
+ '',
393
+ 'ENGINEERING APPROACH',
394
+ '',
395
+ engineering.guidance,
331
396
  ...(stack?.guidance ? ['', 'SELECTED STACK GUIDANCE', '', stack.guidance] : []),
332
397
  ...(stack?.deslop ? ['', 'SELECTED STACK CLEANUP GUIDANCE', '', stack.deslop] : []),
333
398
  '',
@@ -340,16 +405,17 @@ async function deslopContinuation(projectRoot, changedPaths) {
340
405
  ].join('\n');
341
406
  }
342
407
 
343
- async function finalSummaryContinuation() {
344
- return (await readInstalledAsset('finalSummary')).trim();
345
- }
346
-
347
408
  export async function completeCodexTurn({ input, projectRoot } = {}) {
348
409
  const root = (await gitContext(projectRoot || input?.cwd)).repositoryRoot;
349
410
  const statePath = await hookStatePath(root, input);
350
411
  const state = await readTurnState(statePath);
351
412
  if (!state || state.schemaVersion !== HOOK_STATE_SCHEMA_VERSION) return {};
352
413
 
414
+ if (state.implementationAuthorized !== true) {
415
+ await rm(statePath, { force: true });
416
+ return {};
417
+ }
418
+
353
419
  if (state.phase === 'implementation') {
354
420
  if (input?.stop_hook_active === true || state.turnId !== hookId(input, 'turn_id')) {
355
421
  await rm(statePath, { force: true });
@@ -360,7 +426,10 @@ export async function completeCodexTurn({ input, projectRoot } = {}) {
360
426
  await rm(statePath, { force: true });
361
427
  return {};
362
428
  }
363
- const stack = await optionalStack(root);
429
+ const [stack, engineering] = await Promise.all([
430
+ optionalStack(root),
431
+ optionalEngineering(root),
432
+ ]);
364
433
  const postChange = Boolean(stack?.postChange?.trim());
365
434
  await writeTurnState(statePath, {
366
435
  ...state,
@@ -370,7 +439,7 @@ export async function completeCodexTurn({ input, projectRoot } = {}) {
370
439
  return {
371
440
  decision: 'block',
372
441
  reason: postChange
373
- ? postChangeContinuation(changedPaths, stack)
442
+ ? postChangeContinuation(changedPaths, stack, engineering)
374
443
  : await reconciliationContinuation(root, changedPaths),
375
444
  };
376
445
  }
@@ -420,14 +489,8 @@ export async function completeCodexTurn({ input, projectRoot } = {}) {
420
489
 
421
490
  if (state.phase === 'deslop') {
422
491
  await refreshProjectIndex(root);
423
- await writeTurnState(statePath, {
424
- ...state,
425
- phase: 'summary',
426
- });
427
- return {
428
- decision: 'block',
429
- reason: await finalSummaryContinuation(),
430
- };
492
+ await rm(statePath, { force: true });
493
+ return {};
431
494
  }
432
495
 
433
496
  await rm(statePath, { force: true });
@@ -4,6 +4,7 @@ import path from 'node:path';
4
4
  import { inspectProjectSkills, renderAgentSkillCatalog } from './agent-skills.js';
5
5
  import { buildProjectIndex } from './code-index.js';
6
6
  import { asDiagnostic, GenesisError } from './errors.js';
7
+ import { readEngineering } from './engineering.js';
7
8
  import { gitContext } from './git.js';
8
9
  import { inspectProgram } from './program.js';
9
10
  import { readStack } from './stack.js';
@@ -56,13 +57,14 @@ export async function contextForProjectPaths({ paths, projectRoot, stackPackages
56
57
  const location = await gitContext(projectRoot);
57
58
  const root = location.repositoryRoot;
58
59
  const targets = [...new Set(paths.map((value) => projectPath(root, location.workingDirectory, value)))];
59
- const [program, stack] = await Promise.all([
60
+ const [program, stack, engineering] = await Promise.all([
60
61
  inspectProgram(root).catch((error) => ({
61
62
  status: 'invalid',
62
63
  modules: [],
63
64
  diagnostic: asDiagnostic(error),
64
65
  })),
65
66
  readStack(root, { stackPackages }),
67
+ readEngineering(root),
66
68
  ]);
67
69
  const modules = program.modules.filter((module) => targets.some((target) => citesTarget(module, target)));
68
70
  const moduleSources = await Promise.all(modules.map(async (module) => ({
@@ -98,6 +100,12 @@ export async function contextForProjectPaths({ paths, projectRoot, stackPackages
98
100
  '## Relevant Program',
99
101
  '',
100
102
  ...programDetails,
103
+ '## Engineering approach',
104
+ '',
105
+ `Profile: \`${engineering.profile.id}\` — ${engineering.profile.description}`,
106
+ '',
107
+ engineering.guidance,
108
+ '',
101
109
  ...stackSummary(stack),
102
110
  ...stackGuidance(stack),
103
111
  '',
@@ -125,6 +133,7 @@ export async function contextForProjectPaths({ paths, projectRoot, stackPackages
125
133
  paths: targets,
126
134
  modules: moduleSources.map(({ source: _source, ...module }) => module),
127
135
  components: stack.components.map(({ id }) => id),
136
+ engineeringProfile: engineering.profile.id,
128
137
  verificationCommands: stack.verificationCommands.map(({ label, argv }) => ({ label, argv })),
129
138
  warnings: [
130
139
  ...(program.diagnostic ? [program.diagnostic] : []),
@@ -1,5 +1,6 @@
1
1
  export const GENESIS_CONTRACTS = Object.freeze({
2
2
  derivedArtifacts: 'genesis.derived-artifacts.v1',
3
+ engineering: 'genesis.engineering.v1',
3
4
  environment: 'genesis.environment.v2',
4
5
  stackSection: 'genesis.stack-section.v1',
5
6
  verification: 'genesis.verification.v1',
@@ -0,0 +1,274 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { GENESIS_CONTRACTS } from './contracts.js';
5
+ import { GenesisError } from './errors.js';
6
+ import { gitContext } from './git.js';
7
+ import { ENGINEERING_PATH } from './paths.js';
8
+ import { normalizeSource, writeFileAtomic } from './utils.js';
9
+
10
+ const PROFILE_ID_PATTERN = /^[a-z][a-z0-9-]*\.v[1-9][0-9]*$/u;
11
+ const PROFILE_IDS = Object.freeze(['focused.v1', 'durable.v1', 'high-assurance.v1']);
12
+ const PROFILE_ASSETS = new Map(PROFILE_IDS.map((id) => [
13
+ id,
14
+ new URL(`../../profiles/engineering/${id}.md`, import.meta.url),
15
+ ]));
16
+ const BASELINE_ASSET = new URL('../../profiles/engineering/baseline.md', import.meta.url);
17
+
18
+ export const DEFAULT_ENGINEERING_PROFILE_ID = 'focused.v1';
19
+ export const ENGINEERING_SKELETON_SOURCE = `# Engineering approach
20
+
21
+ ## Profile
22
+
23
+ - \`${DEFAULT_ENGINEERING_PROFILE_ID}\`
24
+
25
+ ## Project requirements
26
+
27
+ - Nothing.
28
+ `;
29
+
30
+ function normalizeProfileId(value) {
31
+ const id = String(value ?? '').trim();
32
+ if (!PROFILE_ID_PATTERN.test(id)) {
33
+ throw new GenesisError(
34
+ 'ENGINEERING_PROFILE_INVALID',
35
+ 'Engineering profile ids use a lowercase name followed by a version, such as `focused.v1`.',
36
+ { profile: value },
37
+ );
38
+ }
39
+ return id;
40
+ }
41
+
42
+ function sections(source, { title, sourcePath }) {
43
+ const lines = normalizeSource(source).split('\n');
44
+ if (lines.filter((line) => line.trim() === title).length !== 1) {
45
+ throw new GenesisError('ENGINEERING_INVALID', `${sourcePath} needs exactly one \`${title}\` title.`);
46
+ }
47
+ const result = new Map();
48
+ let current = null;
49
+ for (const line of lines) {
50
+ const heading = line.match(/^##\s+(.+?)\s*$/u);
51
+ if (heading) {
52
+ if (result.has(heading[1])) {
53
+ throw new GenesisError('ENGINEERING_INVALID', `Duplicate engineering section: ${heading[1]}.`, {
54
+ path: sourcePath,
55
+ });
56
+ }
57
+ current = [];
58
+ result.set(heading[1], current);
59
+ } else if (current) {
60
+ current.push(line);
61
+ } else if (line.trim() && line.trim() !== title) {
62
+ throw new GenesisError('ENGINEERING_INVALID', `${sourcePath} contains content outside an engineering section.`);
63
+ }
64
+ }
65
+ return result;
66
+ }
67
+
68
+ function sectionText(all, name) {
69
+ return (all.get(name) || []).join('\n').trim();
70
+ }
71
+
72
+ function parseProfileSource(source, { id }) {
73
+ const normalized = normalizeSource(source);
74
+ const titleMatch = normalized.match(/^# ([^\r\n]+)[ \t]*$/mu);
75
+ if (!titleMatch || normalized.match(/^# [^\r\n]+[ \t]*$/gmu)?.length !== 1) {
76
+ throw new GenesisError('ENGINEERING_PROFILE_INVALID', `Engineering profile ${id} needs one title.`);
77
+ }
78
+ const all = sections(normalized, {
79
+ sourcePath: `profiles/engineering/${id}.md`,
80
+ title: titleMatch[0].trim(),
81
+ });
82
+ const unknown = [...all.keys()].filter((name) => !['Description', 'Guidance'].includes(name));
83
+ const description = sectionText(all, 'Description');
84
+ const guidance = sectionText(all, 'Guidance');
85
+ if (unknown.length > 0 || !description || !guidance) {
86
+ throw new GenesisError(
87
+ 'ENGINEERING_PROFILE_INVALID',
88
+ `Engineering profile ${id} needs only non-empty Description and Guidance sections.`,
89
+ { profile: id },
90
+ );
91
+ }
92
+ return Object.freeze({ id, name: titleMatch[1].trim(), description, guidance });
93
+ }
94
+
95
+ export async function engineeringProfile(value) {
96
+ const id = normalizeProfileId(value);
97
+ const location = PROFILE_ASSETS.get(id);
98
+ if (!location) {
99
+ throw new GenesisError(
100
+ 'ENGINEERING_PROFILE_UNKNOWN',
101
+ `No installed engineering profile exists for ${id}.`,
102
+ { profile: id },
103
+ );
104
+ }
105
+ return parseProfileSource(await readFile(location, 'utf8'), { id });
106
+ }
107
+
108
+ export async function readEngineeringBaseline() {
109
+ const source = normalizeSource(await readFile(BASELINE_ASSET, 'utf8')).trim();
110
+ if (!/^# Engineering baseline[ \t]*$/mu.test(source)) {
111
+ throw new GenesisError('ENGINEERING_PROFILE_INVALID', 'The installed engineering baseline is invalid.');
112
+ }
113
+ return source.replace(/^# Engineering baseline[ \t]*\n?/mu, '').trim();
114
+ }
115
+
116
+ export function parseEngineeringSource(source) {
117
+ const normalized = normalizeSource(source);
118
+ const all = sections(normalized, {
119
+ sourcePath: ENGINEERING_PATH,
120
+ title: '# Engineering approach',
121
+ });
122
+ const unknown = [...all.keys()].filter((name) => !['Profile', 'Project requirements'].includes(name));
123
+ if (unknown.length > 0 || !all.has('Profile') || !all.has('Project requirements')) {
124
+ throw new GenesisError(
125
+ 'ENGINEERING_INVALID',
126
+ `${ENGINEERING_PATH} needs only Profile and Project requirements sections.`,
127
+ { path: ENGINEERING_PATH },
128
+ );
129
+ }
130
+ const profileLines = (all.get('Profile') || []).filter((line) => line.trim());
131
+ const match = profileLines.length === 1
132
+ ? profileLines[0].trim().match(/^- `([^`]+)`$/u)
133
+ : null;
134
+ if (!match) {
135
+ throw new GenesisError(
136
+ 'ENGINEERING_INVALID',
137
+ 'The engineering Profile must be exactly one bullet containing a backticked profile id.',
138
+ { path: ENGINEERING_PATH },
139
+ );
140
+ }
141
+ const requirementsSource = sectionText(all, 'Project requirements');
142
+ if (!requirementsSource) {
143
+ throw new GenesisError(
144
+ 'ENGINEERING_INVALID',
145
+ 'Project requirements must contain requirements or `- Nothing.`.',
146
+ { path: ENGINEERING_PATH },
147
+ );
148
+ }
149
+ if (Buffer.byteLength(requirementsSource, 'utf8') > 64 * 1024) {
150
+ throw new GenesisError('ENGINEERING_INVALID', 'Project engineering requirements exceed 64 KiB.', {
151
+ path: ENGINEERING_PATH,
152
+ });
153
+ }
154
+ return {
155
+ path: ENGINEERING_PATH,
156
+ profileId: normalizeProfileId(match[1]),
157
+ requirements: requirementsSource === '- Nothing.' ? '' : requirementsSource,
158
+ source: normalized,
159
+ };
160
+ }
161
+
162
+ function renderEngineering({ profileId, requirements = '' }) {
163
+ return [
164
+ '# Engineering approach',
165
+ '',
166
+ '## Profile',
167
+ '',
168
+ `- \`${profileId}\``,
169
+ '',
170
+ '## Project requirements',
171
+ '',
172
+ requirements.trim() || '- Nothing.',
173
+ '',
174
+ ].join('\n');
175
+ }
176
+
177
+ function effectiveGuidance({ baseline, profile, requirements }) {
178
+ return [
179
+ '## Universal complexity gate',
180
+ '',
181
+ baseline,
182
+ '',
183
+ `## Selected profile: ${profile.name} (\`${profile.id}\`)`,
184
+ '',
185
+ profile.guidance,
186
+ ...(requirements ? [
187
+ '',
188
+ '## Project requirements',
189
+ '',
190
+ requirements,
191
+ ] : []),
192
+ ].join('\n');
193
+ }
194
+
195
+ export async function readEngineering(projectRoot) {
196
+ const location = path.join(projectRoot, ENGINEERING_PATH);
197
+ let parsed;
198
+ let status = 'configured';
199
+ try {
200
+ parsed = parseEngineeringSource(await readFile(location, 'utf8'));
201
+ } catch (error) {
202
+ if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error;
203
+ parsed = parseEngineeringSource(ENGINEERING_SKELETON_SOURCE);
204
+ status = 'defaulted';
205
+ }
206
+ const [baseline, profile] = await Promise.all([
207
+ readEngineeringBaseline(),
208
+ engineeringProfile(parsed.profileId),
209
+ ]);
210
+ return {
211
+ contract: GENESIS_CONTRACTS.engineering,
212
+ path: ENGINEERING_PATH,
213
+ status,
214
+ profile,
215
+ requirements: parsed.requirements,
216
+ source: status === 'configured' ? parsed.source : null,
217
+ guidance: effectiveGuidance({ baseline, profile, requirements: parsed.requirements }),
218
+ };
219
+ }
220
+
221
+ export function engineeringPromptContext(engineering) {
222
+ return {
223
+ path: engineering.path,
224
+ status: engineering.status,
225
+ profile: {
226
+ id: engineering.profile.id,
227
+ name: engineering.profile.name,
228
+ description: engineering.profile.description,
229
+ },
230
+ requirements: engineering.requirements || 'Nothing.',
231
+ };
232
+ }
233
+
234
+ export function listEngineeringProfileCatalog() {
235
+ return Promise.all(PROFILE_IDS.map(engineeringProfile));
236
+ }
237
+
238
+ export async function inspectProjectEngineering({ projectRoot } = {}) {
239
+ const root = (await gitContext(projectRoot)).repositoryRoot;
240
+ const [engineering, profiles] = await Promise.all([
241
+ readEngineering(root),
242
+ listEngineeringProfileCatalog(),
243
+ ]);
244
+ return {
245
+ contract: engineering.contract,
246
+ path: engineering.path,
247
+ status: engineering.status,
248
+ profile: engineering.profile,
249
+ profiles,
250
+ requirements: engineering.requirements,
251
+ guidance: engineering.guidance,
252
+ };
253
+ }
254
+
255
+ export async function selectEngineeringProfile({ profile, projectRoot } = {}) {
256
+ const root = (await gitContext(projectRoot)).repositoryRoot;
257
+ const selected = await engineeringProfile(profile);
258
+ const current = await readEngineering(root);
259
+ const rendered = renderEngineering({
260
+ profileId: selected.id,
261
+ requirements: current.requirements,
262
+ });
263
+ const changed = current.source !== rendered;
264
+ if (changed) await writeFileAtomic(path.join(root, ENGINEERING_PATH), rendered);
265
+ return {
266
+ contract: GENESIS_CONTRACTS.engineering,
267
+ status: changed ? 'updated' : 'unchanged',
268
+ summary: changed
269
+ ? `Selected engineering profile: ${selected.id}.`
270
+ : `Engineering profile ${selected.id} is already selected.`,
271
+ profile: selected,
272
+ changedFiles: changed ? [ENGINEERING_PATH] : [],
273
+ };
274
+ }
package/src/index/init.js CHANGED
@@ -4,8 +4,9 @@ import path from 'node:path';
4
4
  import { syncProjectSkills } from './agent-skills.js';
5
5
  import { BLUEPRINT_SKELETON_SOURCE } from './blueprint.js';
6
6
  import { installCodexHooks } from './codex-hooks.js';
7
+ import { ENGINEERING_SKELETON_SOURCE } from './engineering.js';
7
8
  import { gitContext } from './git.js';
8
- import { BLUEPRINT_PATH, PROGRAM_ROOT, STACK_PATH } from './paths.js';
9
+ import { BLUEPRINT_PATH, ENGINEERING_PATH, PROGRAM_ROOT, STACK_PATH } from './paths.js';
9
10
  import { EMPTY_STACK_SOURCE, readStack } from './stack.js';
10
11
 
11
12
  async function createIfMissing(projectRoot, relativePath, source) {
@@ -24,6 +25,7 @@ export async function initializeProject({ projectRoot, stackPackages = [] } = {}
24
25
  const root = (await gitContext(projectRoot)).repositoryRoot;
25
26
  const created = (await Promise.all([
26
27
  createIfMissing(root, BLUEPRINT_PATH, BLUEPRINT_SKELETON_SOURCE),
28
+ createIfMissing(root, ENGINEERING_PATH, ENGINEERING_SKELETON_SOURCE),
27
29
  createIfMissing(root, STACK_PATH, EMPTY_STACK_SOURCE),
28
30
  ])).filter(Boolean);
29
31
  await mkdir(path.join(root, PROGRAM_ROOT), { recursive: true });
@@ -42,6 +44,7 @@ export async function initializeProject({ projectRoot, stackPackages = [] } = {}
42
44
  'Open Codex and use /hooks to review and trust the project hooks.',
43
45
  'Genesis workflow skills are available in .agents/skills/.',
44
46
  'Describe product intent in genesis/blueprint.md.',
47
+ 'Choose the project engineering approach with genesis engineering set <profile>.',
45
48
  'Stack components are optional; add them with genesis stack add <piece...>.',
46
49
  ].join(' '),
47
50
  };
@@ -1,4 +1,5 @@
1
1
  export const BLUEPRINT_PATH = 'genesis/blueprint.md';
2
+ export const ENGINEERING_PATH = 'genesis/engineering.md';
2
3
  export const STACK_PATH = 'genesis/stack.md';
3
4
  export const PROGRAM_ROOT = 'genesis/program';
4
5
  export const VERIFICATION_PATH = '.genesis/verification.json';