genesis-compiler 1.0.0 → 1.2.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 (73) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +443 -0
  3. package/bin/genesis.js +15 -0
  4. package/docs/assurance-model.md +26 -0
  5. package/docs/prompt-integration.md +98 -0
  6. package/docs/stack-components.md +304 -0
  7. package/package.json +57 -7
  8. package/plugins/genesis/.codex-plugin/plugin.json +19 -0
  9. package/plugins/genesis/hooks.json +18 -0
  10. package/prompts/blueprint.txt +9 -0
  11. package/prompts/describe.txt +17 -0
  12. package/prompts/deslop.txt +14 -0
  13. package/prompts/program.txt +12 -0
  14. package/prompts/reconcile.txt +12 -0
  15. package/prompts/review.txt +12 -0
  16. package/prompts/start.txt +30 -0
  17. package/prompts/work.txt +28 -0
  18. package/skills/genesis-deslop/SKILL.md +36 -0
  19. package/skills/genesis-deslop/agents/openai.yaml +4 -0
  20. package/skills/genesis-program/SKILL.md +66 -0
  21. package/skills/genesis-program/agents/openai.yaml +4 -0
  22. package/skills/genesis-project/SKILL.md +53 -0
  23. package/skills/genesis-project/agents/openai.yaml +4 -0
  24. package/src/cli.js +276 -0
  25. package/src/index/agent-skills.js +425 -0
  26. package/src/index/assets.js +19 -0
  27. package/src/index/blueprint.js +38 -0
  28. package/src/index/check.js +102 -0
  29. package/src/index/code-index.js +283 -0
  30. package/src/index/code-indexers/ast-grep.js +414 -0
  31. package/src/index/codex-hooks.js +367 -0
  32. package/src/index/codex-plugin.js +73 -0
  33. package/src/index/context.js +137 -0
  34. package/src/index/environment-files.js +19 -0
  35. package/src/index/errors.js +26 -0
  36. package/src/index/git.js +26 -0
  37. package/src/index/init.js +48 -0
  38. package/src/index/launch.js +34 -0
  39. package/src/index/paths.js +10 -0
  40. package/src/index/process.js +89 -0
  41. package/src/index/program.js +181 -0
  42. package/src/index/project-files.js +24 -0
  43. package/src/index/project-state.js +87 -0
  44. package/src/index/prompt.js +347 -0
  45. package/src/index/stack-catalog.js +72 -0
  46. package/src/index/stack-command.js +65 -0
  47. package/src/index/stack-composition.js +38 -0
  48. package/src/index/stack-environment-files.js +83 -0
  49. package/src/index/stack-launch.js +428 -0
  50. package/src/index/stack-piece.js +283 -0
  51. package/src/index/stack-preflight.js +25 -0
  52. package/src/index/stack-process.js +25 -0
  53. package/src/index/stack-workspace-setup.js +129 -0
  54. package/src/index/stack.js +302 -0
  55. package/src/index/utils.js +85 -0
  56. package/src/index/verification.js +77 -0
  57. package/src/index/workspace-setup.js +55 -0
  58. package/src/index.js +102 -0
  59. package/stacks/pieces/cpp.md +22 -0
  60. package/stacks/pieces/csharp.md +22 -0
  61. package/stacks/pieces/go.md +22 -0
  62. package/stacks/pieces/java.md +22 -0
  63. package/stacks/pieces/jskit-mysql.md +37 -0
  64. package/stacks/pieces/jskit.md +70 -0
  65. package/stacks/pieces/kotlin.md +22 -0
  66. package/stacks/pieces/mysql.md +18 -0
  67. package/stacks/pieces/nodejs.md +25 -0
  68. package/stacks/pieces/php.md +23 -0
  69. package/stacks/pieces/python.md +23 -0
  70. package/stacks/pieces/ruby.md +22 -0
  71. package/stacks/pieces/rust.md +22 -0
  72. package/stacks/pieces/shell.md +23 -0
  73. package/stacks/pieces/vue.md +19 -0
@@ -0,0 +1,425 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createRequire } from 'node:module';
3
+ import {
4
+ cp,
5
+ lstat,
6
+ mkdir,
7
+ readFile,
8
+ readdir,
9
+ rename,
10
+ rm,
11
+ } from 'node:fs/promises';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ import { parse as parseYaml } from 'yaml';
16
+
17
+ import { GenesisError } from './errors.js';
18
+ import { sha256, stableJson, writeFileAtomic } from './utils.js';
19
+
20
+ const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
21
+ const require = createRequire(import.meta.url);
22
+ const SKILLS_ROOT = '.agents/skills';
23
+ const MANIFEST_PATH = `${SKILLS_ROOT}/.genesis-managed.json`;
24
+ const MANIFEST_VERSION = 1;
25
+ const HASH = /^sha256:[0-9a-f]{64}$/u;
26
+ const SKILL_FIELDS = new Set([
27
+ 'name',
28
+ 'description',
29
+ 'license',
30
+ 'compatibility',
31
+ 'metadata',
32
+ 'allowed-tools',
33
+ ]);
34
+
35
+ const CORE_SKILLS = [
36
+ 'genesis-project',
37
+ 'genesis-program',
38
+ 'genesis-deslop',
39
+ ].map((name) => ({
40
+ component: null,
41
+ package: null,
42
+ path: `skills/${name}`,
43
+ source: `genesis:skills/${name}`,
44
+ }));
45
+
46
+ function invalidSkill(message, details = {}) {
47
+ throw new GenesisError('AGENT_SKILL_INVALID', message, details);
48
+ }
49
+
50
+ function validSkillName(value) {
51
+ if (typeof value !== 'string') return false;
52
+ const name = value.normalize('NFKC');
53
+ return name.length > 0
54
+ && name.length <= 64
55
+ && name === name.toLowerCase()
56
+ && !name.startsWith('-')
57
+ && !name.endsWith('-')
58
+ && !name.includes('--')
59
+ && [...name].every((character) => character === '-' || /[\p{L}\p{N}]/u.test(character));
60
+ }
61
+
62
+ function frontmatter(source, location) {
63
+ const normalized = source.replace(/\r\n?/gu, '\n');
64
+ if (!normalized.startsWith('---\n')) invalidSkill(`Agent Skill is missing YAML frontmatter: ${location}.`);
65
+ const end = normalized.indexOf('\n---\n', 4);
66
+ if (end === -1) invalidSkill(`Agent Skill frontmatter is not closed: ${location}.`);
67
+ let metadata;
68
+ try {
69
+ metadata = parseYaml(normalized.slice(4, end));
70
+ } catch (error) {
71
+ invalidSkill(`Agent Skill frontmatter is invalid YAML: ${location}.`, { cause: error.message });
72
+ }
73
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
74
+ invalidSkill(`Agent Skill frontmatter must be a mapping: ${location}.`);
75
+ }
76
+ return metadata;
77
+ }
78
+
79
+ async function skillTree(directory, relative = '') {
80
+ const entries = await readdir(path.join(directory, relative), { withFileTypes: true });
81
+ const files = [];
82
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
83
+ const child = relative ? `${relative}/${entry.name}` : entry.name;
84
+ if (entry.isSymbolicLink()) invalidSkill(`Agent Skill contains a symbolic link: ${child}.`);
85
+ if (entry.isDirectory()) {
86
+ files.push(...await skillTree(directory, child));
87
+ continue;
88
+ }
89
+ if (!entry.isFile()) invalidSkill(`Agent Skill contains a non-file entry: ${child}.`);
90
+ const location = path.join(directory, child);
91
+ const [content, info] = await Promise.all([readFile(location), lstat(location)]);
92
+ files.push({
93
+ path: child,
94
+ hash: sha256(content),
95
+ mode: (info.mode & 0o111) === 0 ? 0o100644 : 0o100755,
96
+ });
97
+ }
98
+ return files;
99
+ }
100
+
101
+ async function describeSkill(directory, source) {
102
+ let info;
103
+ try { info = await lstat(directory); } catch (error) {
104
+ if (['ENOENT', 'ENOTDIR'].includes(error?.code)) return null;
105
+ throw error;
106
+ }
107
+ if (!info.isDirectory() || info.isSymbolicLink()) {
108
+ invalidSkill(`Agent Skill must be an ordinary directory: ${directory}.`);
109
+ }
110
+ const skillFile = path.join(directory, 'SKILL.md');
111
+ let skillSource;
112
+ try { skillSource = await readFile(skillFile, 'utf8'); } catch (error) {
113
+ invalidSkill(`Agent Skill is missing SKILL.md: ${directory}.`, { cause: error.code || error.message });
114
+ }
115
+ const metadata = frontmatter(skillSource, skillFile);
116
+ const unexpected = Object.keys(metadata).filter((field) => !SKILL_FIELDS.has(field));
117
+ if (unexpected.length > 0) {
118
+ invalidSkill(`Agent Skill frontmatter contains unsupported fields: ${unexpected.sort().join(', ')}.`);
119
+ }
120
+ const { name } = metadata;
121
+ const description = typeof metadata.description === 'string' ? metadata.description.trim() : '';
122
+ if (
123
+ !validSkillName(name)
124
+ || name.normalize('NFKC') !== path.basename(directory).normalize('NFKC')
125
+ ) invalidSkill(`Agent Skill name must match its directory and use the standard lowercase format: ${directory}.`);
126
+ if (!description || description.length > 1024) {
127
+ invalidSkill(`Agent Skill description must contain 1-1024 characters: ${skillFile}.`);
128
+ }
129
+ if (
130
+ metadata.compatibility !== undefined
131
+ && (typeof metadata.compatibility !== 'string' || metadata.compatibility.length > 500)
132
+ ) invalidSkill(`Agent Skill compatibility must be a string of at most 500 characters: ${skillFile}.`);
133
+ if (metadata.license !== undefined && typeof metadata.license !== 'string') {
134
+ invalidSkill(`Agent Skill license must be a string: ${skillFile}.`);
135
+ }
136
+ if (metadata['allowed-tools'] !== undefined && typeof metadata['allowed-tools'] !== 'string') {
137
+ invalidSkill(`Agent Skill allowed-tools must be a string: ${skillFile}.`);
138
+ }
139
+ if (
140
+ metadata.metadata !== undefined
141
+ && (
142
+ !metadata.metadata
143
+ || typeof metadata.metadata !== 'object'
144
+ || Array.isArray(metadata.metadata)
145
+ || Object.values(metadata.metadata).some((value) => typeof value !== 'string')
146
+ )
147
+ ) invalidSkill(`Agent Skill metadata must map strings to strings: ${skillFile}.`);
148
+ const files = await skillTree(directory);
149
+ return {
150
+ name,
151
+ description,
152
+ directory,
153
+ source,
154
+ files,
155
+ hash: sha256(stableJson(files)),
156
+ };
157
+ }
158
+
159
+ async function packageDirectory(packageName) {
160
+ for (const modulesRoot of require.resolve.paths(packageName) || []) {
161
+ const manifest = path.join(modulesRoot, packageName, 'package.json');
162
+ try {
163
+ const value = JSON.parse(await readFile(manifest, 'utf8'));
164
+ if (value?.name === packageName) return path.dirname(manifest);
165
+ } catch (error) {
166
+ if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) {
167
+ throw new GenesisError(
168
+ 'AGENT_SKILL_UNAVAILABLE',
169
+ `Cannot read selected Agent Skill package ${packageName}: ${error.message}.`,
170
+ { package: packageName },
171
+ );
172
+ }
173
+ }
174
+ }
175
+ throw new GenesisError(
176
+ 'AGENT_SKILL_UNAVAILABLE',
177
+ `Selected Agent Skill package is unavailable: ${packageName}.`,
178
+ { package: packageName },
179
+ );
180
+ }
181
+
182
+ async function resolveLocator(locator) {
183
+ const root = locator.package ? await packageDirectory(locator.package) : packageRoot;
184
+ const directory = path.join(root, locator.path);
185
+ const source = locator.source || (
186
+ locator.package
187
+ ? `npm:${locator.package}/${locator.path}`
188
+ : `genesis:${locator.path}`
189
+ );
190
+ const skill = await describeSkill(directory, source);
191
+ if (!skill) invalidSkill(`Selected Agent Skill is unavailable: ${directory}.`);
192
+ return { ...skill, component: locator.component };
193
+ }
194
+
195
+ /** Resolves the three Genesis workflow skills plus authoritative selected Stack skills. */
196
+ export async function projectSkillPlan(stack) {
197
+ const locators = [
198
+ ...CORE_SKILLS,
199
+ ...(stack?.components || []).flatMap((component) => (
200
+ component.skill ? [{ ...component.skill, component: component.id }] : []
201
+ )),
202
+ ];
203
+ const resolved = await Promise.all(locators.map(resolveLocator));
204
+ const skills = new Map();
205
+ for (const skill of resolved) {
206
+ const previous = skills.get(skill.name);
207
+ if (previous && previous.hash !== skill.hash) {
208
+ throw new GenesisError(
209
+ 'AGENT_SKILL_COLLISION',
210
+ `Selected Stack sources provide different Agent Skills named ${skill.name}.`,
211
+ { name: skill.name, sources: [previous.source, skill.source] },
212
+ );
213
+ }
214
+ if (!previous) skills.set(skill.name, skill);
215
+ }
216
+ return [...skills.values()].sort((left, right) => left.name.localeCompare(right.name));
217
+ }
218
+
219
+ async function readManifest(projectRoot) {
220
+ const location = path.join(projectRoot, MANIFEST_PATH);
221
+ let value;
222
+ try { value = JSON.parse(await readFile(location, 'utf8')); } catch (error) {
223
+ if (['ENOENT', 'ENOTDIR'].includes(error?.code)) {
224
+ return { location, value: { schemaVersion: MANIFEST_VERSION, skills: {} } };
225
+ }
226
+ throw new GenesisError('AGENT_SKILLS_MANIFEST_INVALID', `${MANIFEST_PATH} is invalid: ${error.message}.`);
227
+ }
228
+ const valid = value
229
+ && typeof value === 'object'
230
+ && !Array.isArray(value)
231
+ && value.schemaVersion === MANIFEST_VERSION
232
+ && value.skills
233
+ && typeof value.skills === 'object'
234
+ && !Array.isArray(value.skills)
235
+ && Object.entries(value.skills).every(([name, record]) => (
236
+ validSkillName(name)
237
+ && record
238
+ && typeof record === 'object'
239
+ && Object.keys(record).sort().join(',') === 'hash,source'
240
+ && HASH.test(record.hash)
241
+ && typeof record.source === 'string'
242
+ && record.source.length > 0
243
+ ));
244
+ if (!valid) throw new GenesisError('AGENT_SKILLS_MANIFEST_INVALID', `${MANIFEST_PATH} has an invalid shape.`);
245
+ return { location, value };
246
+ }
247
+
248
+ async function replaceSkill(source, target) {
249
+ const temporary = `${target}.${randomUUID()}.tmp`;
250
+ const backup = `${target}.${randomUUID()}.backup`;
251
+ await mkdir(path.dirname(target), { recursive: true });
252
+ let replaced = false;
253
+ try {
254
+ await cp(source, temporary, { recursive: true, errorOnExist: true });
255
+ try {
256
+ await rename(target, backup);
257
+ replaced = true;
258
+ } catch (error) {
259
+ if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error;
260
+ }
261
+ await rename(temporary, target);
262
+ } catch (error) {
263
+ if (replaced) {
264
+ await rm(target, { recursive: true, force: true }).catch(() => {});
265
+ await rename(backup, target).catch(() => {});
266
+ }
267
+ throw error;
268
+ } finally {
269
+ await rm(temporary, { recursive: true, force: true });
270
+ await rm(backup, { recursive: true, force: true });
271
+ }
272
+ }
273
+
274
+ function targetFiles(skill) {
275
+ return skill.files.map(({ path: file }) => `${SKILLS_ROOT}/${skill.name}/${file}`);
276
+ }
277
+
278
+ /** Installs only Genesis-owned or authoritative Stack-declared skills. */
279
+ export async function syncProjectSkills({ projectRoot, stack } = {}) {
280
+ const desired = await projectSkillPlan(stack);
281
+ const desiredNames = new Set(desired.map(({ name }) => name));
282
+ const manifest = await readManifest(projectRoot);
283
+ const changedFiles = [];
284
+ const diagnostics = [];
285
+
286
+ for (const skill of desired) {
287
+ const target = path.join(projectRoot, SKILLS_ROOT, skill.name);
288
+ const installed = await describeSkill(target, `project:${skill.name}`);
289
+ const managed = Object.hasOwn(manifest.value.skills, skill.name)
290
+ ? manifest.value.skills[skill.name]
291
+ : null;
292
+ if (!installed) {
293
+ await replaceSkill(skill.directory, target);
294
+ manifest.value.skills[skill.name] = { hash: skill.hash, source: skill.source };
295
+ changedFiles.push(...targetFiles(skill));
296
+ continue;
297
+ }
298
+ if (!managed) {
299
+ diagnostics.push({
300
+ code: 'AGENT_SKILL_EXTERNAL',
301
+ message: `Preserved existing project Agent Skill ${skill.name}; Genesis does not own it.`,
302
+ details: { name: skill.name, path: `${SKILLS_ROOT}/${skill.name}/SKILL.md` },
303
+ });
304
+ continue;
305
+ }
306
+ if (installed.hash !== managed.hash) {
307
+ diagnostics.push({
308
+ code: 'AGENT_SKILL_CUSTOMIZED',
309
+ message: `Preserved locally modified Agent Skill ${skill.name}.`,
310
+ details: { name: skill.name, path: `${SKILLS_ROOT}/${skill.name}/SKILL.md` },
311
+ });
312
+ continue;
313
+ }
314
+ if (installed.hash !== skill.hash || managed.source !== skill.source) {
315
+ await replaceSkill(skill.directory, target);
316
+ manifest.value.skills[skill.name] = { hash: skill.hash, source: skill.source };
317
+ changedFiles.push(...new Set([...targetFiles(installed), ...targetFiles(skill)]));
318
+ }
319
+ }
320
+
321
+ for (const [name, record] of Object.entries(manifest.value.skills)) {
322
+ if (desiredNames.has(name)) continue;
323
+ const target = path.join(projectRoot, SKILLS_ROOT, name);
324
+ const installed = await describeSkill(target, `project:${name}`);
325
+ if (installed?.hash === record.hash) {
326
+ await rm(target, { recursive: true, force: true });
327
+ changedFiles.push(...targetFiles(installed));
328
+ } else if (installed) {
329
+ diagnostics.push({
330
+ code: 'AGENT_SKILL_PRESERVED',
331
+ message: `Preserved modified deselected Agent Skill ${name} and released Genesis ownership.`,
332
+ details: { name, path: `${SKILLS_ROOT}/${name}/SKILL.md` },
333
+ });
334
+ }
335
+ delete manifest.value.skills[name];
336
+ }
337
+
338
+ const rendered = stableJson(manifest.value);
339
+ let previous = null;
340
+ try { previous = await readFile(manifest.location, 'utf8'); } catch (error) {
341
+ if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error;
342
+ }
343
+ if (rendered !== previous) {
344
+ await writeFileAtomic(manifest.location, rendered);
345
+ changedFiles.push(MANIFEST_PATH);
346
+ }
347
+ return {
348
+ status: changedFiles.length > 0 ? 'updated' : 'unchanged',
349
+ changedFiles: [...new Set(changedFiles)].sort(),
350
+ diagnostics,
351
+ skills: desired.map(({ name }) => name),
352
+ };
353
+ }
354
+
355
+ /** Inspects the exact project copies without changing them. */
356
+ export async function inspectProjectSkills({ projectRoot, stack } = {}) {
357
+ const desired = await projectSkillPlan(stack);
358
+ const manifest = await readManifest(projectRoot);
359
+ const diagnostics = [];
360
+ const skills = [];
361
+ let customized = false;
362
+ let invalid = false;
363
+ let missing = false;
364
+ for (const expected of desired) {
365
+ const target = path.join(projectRoot, SKILLS_ROOT, expected.name);
366
+ let installed;
367
+ try { installed = await describeSkill(target, `project:${expected.name}`); } catch (error) {
368
+ invalid = true;
369
+ diagnostics.push({ code: error.code || 'AGENT_SKILL_INVALID', message: error.message });
370
+ continue;
371
+ }
372
+ if (!installed) {
373
+ missing = true;
374
+ diagnostics.push({
375
+ code: 'AGENT_SKILL_MISSING',
376
+ message: `Project Agent Skill is missing: ${SKILLS_ROOT}/${expected.name}/SKILL.md.`,
377
+ details: { name: expected.name },
378
+ });
379
+ continue;
380
+ }
381
+ const managed = Object.hasOwn(manifest.value.skills, expected.name)
382
+ ? manifest.value.skills[expected.name]
383
+ : null;
384
+ if (!managed) {
385
+ customized = true;
386
+ diagnostics.push({
387
+ code: 'AGENT_SKILL_EXTERNAL',
388
+ message: `Project Agent Skill ${expected.name} is externally managed; Genesis preserved it.`,
389
+ details: { name: expected.name, path: `${SKILLS_ROOT}/${expected.name}/SKILL.md` },
390
+ });
391
+ } else if (installed.hash !== managed.hash) {
392
+ customized = true;
393
+ diagnostics.push({
394
+ code: 'AGENT_SKILL_CUSTOMIZED',
395
+ message: `Project Agent Skill ${expected.name} differs from its Genesis-managed source.`,
396
+ details: { name: expected.name, path: `${SKILLS_ROOT}/${expected.name}/SKILL.md` },
397
+ });
398
+ }
399
+ skills.push({
400
+ name: installed.name,
401
+ description: installed.description,
402
+ path: `${SKILLS_ROOT}/${installed.name}/SKILL.md`,
403
+ component: expected.component,
404
+ });
405
+ }
406
+ return {
407
+ status: invalid ? 'invalid' : missing ? 'missing' : customized ? 'customized' : 'current',
408
+ diagnostics,
409
+ skills,
410
+ };
411
+ }
412
+
413
+ export function renderAgentSkillCatalog(skills) {
414
+ if (!skills.length) return '';
415
+ return [
416
+ 'The following Agent Skills are available for progressive loading. Read a matching',
417
+ '`SKILL.md` completely before acting, and resolve its relative references from the',
418
+ 'skill directory. Load scripts, references, and assets only when needed.',
419
+ '',
420
+ ...skills.flatMap(({ name, description, path: skillPath }) => [
421
+ `- \`${name}\` — ${description}`,
422
+ ` Path: \`${skillPath}\``,
423
+ ]),
424
+ ].join('\n');
425
+ }
@@ -0,0 +1,19 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ const root = new URL('../../', import.meta.url);
4
+ const assets = {
5
+ start: new URL('prompts/start.txt', root),
6
+ work: new URL('prompts/work.txt', root),
7
+ describe: new URL('prompts/describe.txt', root),
8
+ reconcile: new URL('prompts/reconcile.txt', root),
9
+ deslop: new URL('prompts/deslop.txt', root),
10
+ program: new URL('prompts/program.txt', root),
11
+ blueprint: new URL('prompts/blueprint.txt', root),
12
+ review: new URL('prompts/review.txt', root),
13
+ };
14
+
15
+ export async function readInstalledAsset(name) {
16
+ const location = assets[name];
17
+ if (!location) throw new TypeError(`Unknown Genesis asset: ${name}.`);
18
+ return readFile(location, 'utf8');
19
+ }
@@ -0,0 +1,38 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { GenesisError } from './errors.js';
5
+ import { BLUEPRINT_PATH } from './paths.js';
6
+ import { normalizeSource } from './utils.js';
7
+
8
+ export const BLUEPRINT_SKELETON_SOURCE = '# Blueprint\n\n';
9
+
10
+ export function parseBlueprintSource(value, { requireDescription = false } = {}) {
11
+ const source = normalizeSource(value);
12
+ if (!/^# Blueprint[ \t]*$/mu.test(source)) {
13
+ throw new GenesisError('BLUEPRINT_INVALID', 'Blueprint needs one `# Blueprint` title.', {
14
+ path: BLUEPRINT_PATH,
15
+ });
16
+ }
17
+ const description = source.replace(/^# Blueprint[ \t]*\n?/mu, '').trim();
18
+ if (requireDescription && !description) {
19
+ throw new GenesisError('BLUEPRINT_INVALID', 'Blueprint needs a product description.', {
20
+ path: BLUEPRINT_PATH,
21
+ });
22
+ }
23
+ return { path: BLUEPRINT_PATH, source, description };
24
+ }
25
+
26
+ export async function readBlueprint(projectRoot, { required = false, requireDescription = false } = {}) {
27
+ try {
28
+ return parseBlueprintSource(await readFile(path.join(projectRoot, BLUEPRINT_PATH), 'utf8'), {
29
+ requireDescription,
30
+ });
31
+ } catch (error) {
32
+ if (['ENOENT', 'ENOTDIR'].includes(error?.code) && !required) return null;
33
+ if (['ENOENT', 'ENOTDIR'].includes(error?.code)) {
34
+ throw new GenesisError('BLUEPRINT_REQUIRED', `Blueprint does not exist: ${BLUEPRINT_PATH}.`);
35
+ }
36
+ throw error;
37
+ }
38
+ }
@@ -0,0 +1,102 @@
1
+ import { readBlueprint } from './blueprint.js';
2
+ import { inspectProjectSkills } from './agent-skills.js';
3
+ import { readStack } from './stack.js';
4
+ import { asDiagnostic } from './errors.js';
5
+ import { gitContext } from './git.js';
6
+ import { inspectProgram } from './program.js';
7
+ import { inspectVerification } from './project-state.js';
8
+ import { missingStackResources } from './stack-preflight.js';
9
+
10
+ function invalidResult(area, error) {
11
+ return {
12
+ status: 'invalid',
13
+ blueprint: area === 'blueprint' ? 'invalid' : 'valid',
14
+ stack: area === 'stack' ? 'invalid' : 'unknown',
15
+ skills: area === 'skills' ? 'invalid' : 'unknown',
16
+ program: 'unknown',
17
+ resources: 'unknown',
18
+ verification: 'unknown',
19
+ programFiles: [],
20
+ subsystems: [],
21
+ diagnostics: [asDiagnostic(error)],
22
+ guidance: error.message,
23
+ };
24
+ }
25
+
26
+ export async function checkProject({ environment = process.env, projectRoot } = {}) {
27
+ const root = (await gitContext(projectRoot)).repositoryRoot;
28
+ try {
29
+ await readBlueprint(root, { required: true, requireDescription: true });
30
+ } catch (error) {
31
+ return invalidResult('blueprint', error);
32
+ }
33
+
34
+ let stack;
35
+ try {
36
+ stack = await readStack(root);
37
+ } catch (error) {
38
+ return invalidResult('stack', error);
39
+ }
40
+
41
+ let program;
42
+ try {
43
+ program = await inspectProgram(root);
44
+ } catch (error) {
45
+ program = {
46
+ status: 'invalid',
47
+ files: [],
48
+ subsystems: [],
49
+ diagnostic: asDiagnostic(error),
50
+ };
51
+ }
52
+ let skills;
53
+ try {
54
+ skills = await inspectProjectSkills({ projectRoot: root, stack });
55
+ } catch (error) {
56
+ skills = { status: 'invalid', diagnostics: [asDiagnostic(error)], skills: [] };
57
+ }
58
+ const missingResources = missingStackResources({ environment, resources: stack.resources });
59
+ const verification = await inspectVerification({ projectRoot: root, stack });
60
+ const diagnostics = [
61
+ ...(program.diagnostic ? [program.diagnostic] : []),
62
+ ...skills.diagnostics,
63
+ ...missingResources,
64
+ ...(verification.status === 'invalid' ? [{
65
+ code: 'VERIFICATION_EVIDENCE_INVALID',
66
+ message: 'Saved verification evidence is malformed.',
67
+ }] : []),
68
+ ];
69
+
70
+ const guidance = [];
71
+ if (program.status === 'missing') guidance.push('Generate an explanatory Program prompt with genesis prompt --task program.');
72
+ if (program.status === 'invalid') guidance.push('Repair the Program with genesis prompt --task program.');
73
+ if (skills.status === 'missing') guidance.push('Run genesis init to install selected project Agent Skills.');
74
+ if (skills.status === 'invalid') guidance.push('Repair the reported Agent Skill or managed-skill manifest.');
75
+ if (missingResources.length > 0) {
76
+ guidance.push(`${missingResources.map(({ message }) => message).join(' ')} Prompt generation remains available.`);
77
+ }
78
+ if (['missing', 'stale'].includes(verification.status)) guidance.push('Run genesis verify to refresh concrete evidence.');
79
+ if (verification.status === 'unconfigured') guidance.push('Add project verification commands to genesis/stack.md.');
80
+ guidance.push('Use genesis prompt --task review for a semantic, evidence-based comparison.');
81
+
82
+ const needsAttention = program.status === 'missing'
83
+ || skills.status === 'missing'
84
+ || missingResources.length > 0
85
+ || ['missing', 'stale', 'unconfigured'].includes(verification.status);
86
+
87
+ return {
88
+ status: program.status === 'invalid' || skills.status === 'invalid' || verification.status === 'invalid'
89
+ ? 'invalid'
90
+ : needsAttention ? 'attention' : 'ok',
91
+ blueprint: 'valid',
92
+ stack: 'valid',
93
+ skills: skills.status,
94
+ program: program.status,
95
+ resources: missingResources.length > 0 ? 'missing' : 'inputs-present',
96
+ verification: verification.status,
97
+ programFiles: program.files,
98
+ subsystems: program.subsystems,
99
+ diagnostics,
100
+ guidance: guidance.join(' '),
101
+ };
102
+ }