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.
- package/README.md +80 -22
- package/docs/assurance-model.md +5 -3
- package/docs/prompt-integration.md +19 -13
- package/package.json +2 -2
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/profiles/engineering/baseline.md +6 -0
- package/profiles/engineering/durable.v1.md +11 -0
- package/profiles/engineering/focused.v1.md +11 -0
- package/profiles/engineering/high-assurance.v1.md +11 -0
- package/prompts/deslop.txt +5 -0
- package/prompts/start.txt +8 -0
- package/skills/genesis-deslop/SKILL.md +5 -0
- package/skills/genesis-project/SKILL.md +17 -5
- package/src/cli.js +86 -3
- package/src/index/assets.js +0 -1
- package/src/index/check.js +12 -0
- package/src/index/codex-hooks.js +83 -20
- package/src/index/context.js +10 -1
- package/src/index/contracts.js +1 -0
- package/src/index/engineering.js +274 -0
- package/src/index/init.js +4 -1
- package/src/index/paths.js +1 -0
- package/src/index/prompt.js +35 -3
- package/src/index.js +17 -0
- package/prompts/final-summary.txt +0 -12
package/src/index/codex-hooks.js
CHANGED
|
@@ -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
|
|
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
|
-
'-
|
|
138
|
-
'-
|
|
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
|
|
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
|
|
424
|
-
|
|
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 });
|
package/src/index/context.js
CHANGED
|
@@ -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] : []),
|
package/src/index/contracts.js
CHANGED
|
@@ -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
|
};
|
package/src/index/paths.js
CHANGED
|
@@ -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';
|