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,302 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { syncProjectSkills } from './agent-skills.js';
5
+ import { GenesisError } from './errors.js';
6
+ import { readBuiltinStackCatalog } from './stack-catalog.js';
7
+ import { parseStackCommandLines } from './stack-command.js';
8
+ import { resolveStackPieces } from './stack-composition.js';
9
+ import {
10
+ composeStackEnvironmentFiles,
11
+ parseStackEnvironmentFileLines,
12
+ } from './stack-environment-files.js';
13
+ import { composeStackLaunchTargets, parseStackLaunchLines } from './stack-launch.js';
14
+ import {
15
+ composeStackWorkspaceSetup,
16
+ parseStackWorkspaceSetupLines,
17
+ } from './stack-workspace-setup.js';
18
+ import {
19
+ applyStackPieceCustomization,
20
+ normalizeStackPieceId,
21
+ parseStackPieceCustomizationSource,
22
+ } from './stack-piece.js';
23
+ import { STACK_PATH } from './paths.js';
24
+ import { sha256, stableJson, writeFileAtomic } from './utils.js';
25
+
26
+ const COMPONENT_LINE = /^- `([a-z0-9]+(?:-[a-z0-9]+)*)`$/u;
27
+ export const EMPTY_STACK_SOURCE = '# Stack\n\n## Components\n';
28
+ const STACK_SECTIONS = new Set([
29
+ 'Components',
30
+ 'Environment files',
31
+ 'Workspace setup',
32
+ 'Commands',
33
+ 'Launch',
34
+ ]);
35
+ const STACK_CUSTOMIZATION_ROOT = 'genesis/stack';
36
+
37
+ function parseSections(source) {
38
+ const lines = source.replace(/\r\n?/gu, '\n').split('\n');
39
+ if (lines.filter((line) => line.trim() === '# Stack').length !== 1) {
40
+ throw new GenesisError('STACK_INVALID', `${STACK_PATH} needs exactly one \`# Stack\` title.`);
41
+ }
42
+ const sections = new Map();
43
+ let current = null;
44
+ for (const line of lines) {
45
+ const heading = line.match(/^##\s+(.+?)\s*$/u);
46
+ if (heading) {
47
+ if (!STACK_SECTIONS.has(heading[1]) || sections.has(heading[1])) {
48
+ throw new GenesisError('STACK_INVALID', `Unknown or duplicate Stack section: ${heading[1]}.`);
49
+ }
50
+ current = [];
51
+ sections.set(heading[1], current);
52
+ } else if (current) current.push(line);
53
+ else if (line.trim() && line.trim() !== '# Stack') {
54
+ throw new GenesisError('STACK_INVALID', `${STACK_PATH} contains content outside a Stack section.`);
55
+ }
56
+ }
57
+ return sections;
58
+ }
59
+
60
+ function componentIds(sections) {
61
+ const componentLines = (sections.get('Components') || []).filter((line) => line.trim());
62
+ const ids = componentLines.map((line) => {
63
+ const match = line.trim().match(COMPONENT_LINE);
64
+ if (!match) {
65
+ throw new GenesisError(
66
+ 'STACK_INVALID',
67
+ 'Each Stack component must be exactly one bullet containing its backticked component id.',
68
+ { path: STACK_PATH, observed: line },
69
+ );
70
+ }
71
+ return normalizeStackPieceId(match[1]);
72
+ });
73
+ if (new Set(ids).size !== ids.length) {
74
+ throw new GenesisError('STACK_INVALID', 'Stack contains a duplicate component id.', { path: STACK_PATH });
75
+ }
76
+ return ids;
77
+ }
78
+
79
+ function knownPieces(resolution) {
80
+ if (resolution.unknown.length > 0) {
81
+ throw new GenesisError(
82
+ 'STACK_PIECE_UNKNOWN',
83
+ `No installed Stack piece exists for ${resolution.unknown.join(', ')}.`,
84
+ { components: resolution.unknown },
85
+ );
86
+ }
87
+ return resolution.pieces;
88
+ }
89
+
90
+ function withoutOuterBlankLines(lines = []) {
91
+ const result = [...lines];
92
+ while (result[0]?.trim() === '') result.shift();
93
+ while (result.at(-1)?.trim() === '') result.pop();
94
+ return result;
95
+ }
96
+
97
+ function renderStack({
98
+ commandLines = [],
99
+ componentIds: componentIdsValue,
100
+ environmentFileLines = null,
101
+ launchLines = null,
102
+ workspaceSetupLines = null,
103
+ }) {
104
+ return [
105
+ '# Stack',
106
+ '',
107
+ '## Components',
108
+ ...componentIdsValue.map((id) => `- \`${id}\``),
109
+ ...(environmentFileLines === null
110
+ ? []
111
+ : ['', '## Environment files', '', ...withoutOuterBlankLines(environmentFileLines)]),
112
+ ...(workspaceSetupLines === null
113
+ ? []
114
+ : ['', '## Workspace setup', '', ...withoutOuterBlankLines(workspaceSetupLines)]),
115
+ ...(commandLines.length > 0 ? ['', '## Commands', ...commandLines] : []),
116
+ ...(launchLines === null ? [] : ['', '## Launch', '', ...withoutOuterBlankLines(launchLines)]),
117
+ '',
118
+ ].join('\n');
119
+ }
120
+
121
+ export async function addStackPieces({ pieces, projectRoot }) {
122
+ if (!Array.isArray(pieces) || pieces.length === 0) {
123
+ throw new GenesisError('STACK_PIECE_REQUIRED', 'genesis stack add requires at least one component.');
124
+ }
125
+ const requested = [...new Set(pieces.map((piece) => normalizeStackPieceId(
126
+ piece,
127
+ { code: 'STACK_PIECE_UNKNOWN' },
128
+ )))];
129
+ const location = path.join(projectRoot, STACK_PATH);
130
+ let source;
131
+ try {
132
+ source = await readFile(location, 'utf8');
133
+ } catch (error) {
134
+ if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error;
135
+ source = EMPTY_STACK_SOURCE;
136
+ }
137
+ const sections = parseSections(source);
138
+ const existing = componentIds(sections);
139
+ const catalog = await readBuiltinStackCatalog();
140
+ const selected = (await Promise.all(
141
+ knownPieces(resolveStackPieces({ catalog, existing, requested }))
142
+ .map((piece) => customizePiece(projectRoot, piece)),
143
+ )).map(({ id }) => id);
144
+ const commandLines = parseStackCommandLines(sections.get('Commands') || [], { path: STACK_PATH })
145
+ .map(({ label, argv }) => `- Verify \`${label}\`: ${argv.map((value) => `\`${value}\``).join(' ')}`);
146
+ const environmentFileLines = sections.has('Environment files')
147
+ ? sections.get('Environment files')
148
+ : null;
149
+ parseStackEnvironmentFileLines(
150
+ environmentFileLines === null ? undefined : environmentFileLines,
151
+ { path: STACK_PATH },
152
+ );
153
+ const launchLines = sections.has('Launch') ? sections.get('Launch') : null;
154
+ parseStackLaunchLines(launchLines === null ? undefined : launchLines, { path: STACK_PATH });
155
+ const workspaceSetupLines = sections.has('Workspace setup')
156
+ ? sections.get('Workspace setup')
157
+ : null;
158
+ parseStackWorkspaceSetupLines(
159
+ workspaceSetupLines === null ? undefined : workspaceSetupLines,
160
+ { path: STACK_PATH },
161
+ );
162
+ const rendered = renderStack({
163
+ commandLines,
164
+ componentIds: selected,
165
+ environmentFileLines,
166
+ launchLines,
167
+ workspaceSetupLines,
168
+ });
169
+ const stackChanged = rendered !== source;
170
+ if (stackChanged) await writeFileAtomic(location, rendered);
171
+ const skills = await syncProjectSkills({ projectRoot, stack: await readStack(projectRoot) });
172
+ const changedFiles = [
173
+ ...(stackChanged ? [STACK_PATH] : []),
174
+ ...skills.changedFiles,
175
+ ].sort();
176
+ return {
177
+ status: changedFiles.length > 0 ? 'updated' : 'unchanged',
178
+ summary: stackChanged
179
+ ? `Selected Stack components: ${selected.join(', ')}.`
180
+ : 'Every requested Stack component and Agent Skill is already selected.',
181
+ components: selected,
182
+ changedFiles,
183
+ diagnostics: skills.diagnostics,
184
+ };
185
+ }
186
+
187
+ function distinctCommands(commands) {
188
+ const seen = new Set();
189
+ return commands.filter((command) => {
190
+ const key = JSON.stringify([command.label, command.argv]);
191
+ if (seen.has(key)) return false;
192
+ seen.add(key);
193
+ return true;
194
+ });
195
+ }
196
+
197
+ function resourceDeclarations(components) {
198
+ return components.flatMap((piece) => piece.resources.map((resource) => ({
199
+ component: piece.id,
200
+ resource,
201
+ })));
202
+ }
203
+
204
+ function composedProse(components, field) {
205
+ return components
206
+ .map((piece) => piece[field] || '')
207
+ .filter(Boolean)
208
+ .join('\n\n');
209
+ }
210
+
211
+ async function customizePiece(projectRoot, piece) {
212
+ const relative = `${STACK_CUSTOMIZATION_ROOT}/${piece.id}.md`;
213
+ let source;
214
+ try {
215
+ source = await readFile(path.join(projectRoot, relative), 'utf8');
216
+ } catch (error) {
217
+ if (['ENOENT', 'ENOTDIR'].includes(error?.code)) return piece;
218
+ throw error;
219
+ }
220
+ return applyStackPieceCustomization(piece, parseStackPieceCustomizationSource(source, {
221
+ expectedId: piece.id,
222
+ path: relative,
223
+ }));
224
+ }
225
+
226
+ export async function readStack(projectRoot) {
227
+ const location = path.join(projectRoot, STACK_PATH);
228
+ let source;
229
+ try {
230
+ source = await readFile(location, 'utf8');
231
+ } catch (error) {
232
+ if (['ENOENT', 'ENOTDIR'].includes(error?.code)) {
233
+ throw new GenesisError(
234
+ 'STACK_REQUIRED',
235
+ `Genesis requires ${STACK_PATH}. Run genesis init first.`,
236
+ );
237
+ }
238
+ throw error;
239
+ }
240
+ const sections = parseSections(source);
241
+ const catalog = await readBuiltinStackCatalog();
242
+ const components = await Promise.all(knownPieces(resolveStackPieces({
243
+ catalog,
244
+ requested: componentIds(sections),
245
+ })).map((piece) => customizePiece(projectRoot, piece)));
246
+ const projectCommands = parseStackCommandLines(
247
+ sections.get('Commands') || [],
248
+ { path: STACK_PATH },
249
+ ).map(({ line: _line, ...command }) => command);
250
+ const componentCommands = components.flatMap((piece) => piece.commands);
251
+ const commands = distinctCommands(projectCommands.length > 0 ? projectCommands : componentCommands);
252
+ const resources = resourceDeclarations(components);
253
+ const projectEnvironmentFiles = parseStackEnvironmentFileLines(
254
+ sections.has('Environment files') ? sections.get('Environment files') : undefined,
255
+ { path: STACK_PATH },
256
+ );
257
+ const environmentFiles = composeStackEnvironmentFiles(components, projectEnvironmentFiles);
258
+ const projectLaunch = parseStackLaunchLines(
259
+ sections.has('Launch') ? sections.get('Launch') : undefined,
260
+ { path: STACK_PATH },
261
+ );
262
+ const launchTargets = composeStackLaunchTargets(components, projectLaunch);
263
+ const projectWorkspaceSetup = parseStackWorkspaceSetupLines(
264
+ sections.has('Workspace setup') ? sections.get('Workspace setup') : undefined,
265
+ { path: STACK_PATH },
266
+ );
267
+ const workspaceSetup = composeStackWorkspaceSetup(components, projectWorkspaceSetup);
268
+ return {
269
+ path: STACK_PATH,
270
+ identityHash: sha256(stableJson({
271
+ components: components.map(({ id }) => id),
272
+ commands: commands.map(({ label, argv }) => ({ label, argv })),
273
+ environmentFiles,
274
+ launchTargets,
275
+ resources,
276
+ workspaceSetup,
277
+ })),
278
+ components,
279
+ commands,
280
+ environmentFiles,
281
+ launchTargets,
282
+ workspaceSetup,
283
+ resources,
284
+ guidance: composedProse(components, 'guidance'),
285
+ deslop: composedProse(components, 'deslop'),
286
+ };
287
+ }
288
+
289
+ export function stackPromptContext(stack) {
290
+ return {
291
+ components: stack.components.map((piece) => ({
292
+ id: piece.id,
293
+ description: piece.description,
294
+ ...(piece.guidance ? { guidance: piece.guidance } : {}),
295
+ requires: piece.requires,
296
+ })),
297
+ verifyCommands: stack.commands.map(({ label, argv }) => ({ label, argv })),
298
+ environmentFiles: stack.environmentFiles,
299
+ launchTargets: stack.launchTargets,
300
+ workspaceSetup: stack.workspaceSetup,
301
+ };
302
+ }
@@ -0,0 +1,85 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import { lstat, mkdir, open, readlink, rename, rm, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ import { fail } from './errors.js';
7
+
8
+ export function normalizeSource(source) {
9
+ return String(source).replace(/\r\n?/gu, '\n');
10
+ }
11
+
12
+ export function sha256(source) {
13
+ return `sha256:${createHash('sha256').update(source).digest('hex')}`;
14
+ }
15
+
16
+ function stable(value) {
17
+ if (Array.isArray(value)) return value.map(stable);
18
+ if (value && typeof value === 'object') {
19
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
20
+ }
21
+ return value;
22
+ }
23
+
24
+ export function stableJson(value) {
25
+ return `${JSON.stringify(stable(value), null, 2)}\n`;
26
+ }
27
+
28
+ export function uniqueSorted(values = []) {
29
+ return [...new Set(values)].sort();
30
+ }
31
+
32
+ export async function writeFileAtomic(filePath, source, { mode = 0o644 } = {}) {
33
+ const temporary = `${filePath}.${randomUUID()}.tmp`;
34
+ await mkdir(path.dirname(filePath), { recursive: true });
35
+ try {
36
+ await writeFile(temporary, source, { flag: 'wx', mode });
37
+ await rename(temporary, filePath);
38
+ } finally {
39
+ await rm(temporary, { force: true });
40
+ }
41
+ }
42
+
43
+ function toPosix(value) {
44
+ return value.split(path.sep).join('/');
45
+ }
46
+
47
+ export function normalizeRelative(value) {
48
+ const source = String(value);
49
+ if ((path.sep !== '\\' && source.includes('\\')) || source.includes('\0')) {
50
+ fail('PROJECT_PATH_OUTSIDE_ROOT', `Path is not canonical: ${value}`, { path: value });
51
+ }
52
+ const normalized = path.posix.normalize(toPosix(source).replace(/^\.\//u, ''));
53
+ if (
54
+ normalized === '..'
55
+ || normalized.startsWith('../')
56
+ || path.posix.isAbsolute(normalized)
57
+ ) {
58
+ fail('PROJECT_PATH_OUTSIDE_ROOT', `Path is outside the project root: ${value}`, { path: value });
59
+ }
60
+ return normalized === '.' ? '' : normalized;
61
+ }
62
+
63
+ export async function pathState(filePath) {
64
+ let handle;
65
+ try {
66
+ const info = await lstat(filePath);
67
+ if (info.isSymbolicLink()) {
68
+ return { exists: true, symlink: true, mode: 0o120000, hash: sha256(await readlink(filePath)) };
69
+ }
70
+ if (!info.isFile()) return { exists: true, special: true, mode: null, hash: null };
71
+ handle = await open(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0));
72
+ const content = await handle.readFile();
73
+ return {
74
+ exists: true,
75
+ hash: sha256(content),
76
+ mode: (info.mode & 0o111) === 0 ? 0o100644 : 0o100755,
77
+ };
78
+ } catch (error) {
79
+ if (error?.code === 'ELOOP') return { exists: true, symlink: true, mode: null, hash: null };
80
+ if (['ENOENT', 'ENOTDIR'].includes(error?.code)) return { exists: false, hash: null, mode: null };
81
+ throw error;
82
+ } finally {
83
+ await handle?.close().catch(() => {});
84
+ }
85
+ }
@@ -0,0 +1,77 @@
1
+ import { runProcess } from './process.js';
2
+ import { asDiagnostic } from './errors.js';
3
+ import { gitContext } from './git.js';
4
+ import { clearVerification, writeVerification } from './project-state.js';
5
+ import { missingStackResources } from './stack-preflight.js';
6
+ import { readStack } from './stack.js';
7
+
8
+ async function emit(onEvent, event) {
9
+ try { await onEvent?.(event); } catch { /* Progress observers do not control verification. */ }
10
+ }
11
+
12
+ export async function verifyProject({
13
+ environment = process.env,
14
+ onEvent,
15
+ processRunner = runProcess,
16
+ projectRoot,
17
+ } = {}) {
18
+ const root = (await gitContext(projectRoot)).repositoryRoot;
19
+ const stack = await readStack(root);
20
+ const missing = missingStackResources({ environment, resources: stack.resources });
21
+ if (missing.length > 0) {
22
+ return {
23
+ status: 'blocked',
24
+ summary: missing.map(({ message }) => message).join(' '),
25
+ commands: [],
26
+ diagnostics: missing,
27
+ };
28
+ }
29
+ if (stack.commands.length === 0) {
30
+ return {
31
+ status: 'unconfigured',
32
+ summary: 'The selected Stack declares no verification commands.',
33
+ commands: [],
34
+ diagnostics: [],
35
+ };
36
+ }
37
+
38
+ await clearVerification(root);
39
+ try {
40
+ const commands = [];
41
+ for (const command of stack.commands) {
42
+ await emit(onEvent, {
43
+ type: 'genesis.verification',
44
+ code: 'VERIFICATION_STARTED',
45
+ message: `Verifying: ${command.label}.`,
46
+ details: { label: command.label, argv: command.argv },
47
+ });
48
+ await processRunner(command.argv[0], command.argv.slice(1), {
49
+ cwd: root,
50
+ maxBytes: 32 * 1024 * 1024,
51
+ code: 'VERIFICATION_FAILED',
52
+ });
53
+ await emit(onEvent, {
54
+ type: 'genesis.verification',
55
+ code: 'VERIFICATION_COMPLETED',
56
+ message: `Verification passed: ${command.label}.`,
57
+ details: { label: command.label, argv: command.argv },
58
+ });
59
+ commands.push({ label: command.label, argv: command.argv });
60
+ }
61
+ const evidence = await writeVerification({ projectRoot: root, stack });
62
+ return {
63
+ status: 'passed',
64
+ summary: `Passed ${commands.length} declared verification command${commands.length === 1 ? '' : 's'}.`,
65
+ commands,
66
+ evidence,
67
+ diagnostics: [],
68
+ };
69
+ } catch (error) {
70
+ return {
71
+ status: 'failed',
72
+ summary: error.message,
73
+ commands: [],
74
+ diagnostics: [asDiagnostic(error)],
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,55 @@
1
+ import { access } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { gitContext } from './git.js';
5
+ import { readStack } from './stack.js';
6
+ import { sha256, stableJson, uniqueSorted } from './utils.js';
7
+
8
+ /** Read the Stack's workspace preparation recipe without executing it. */
9
+ export async function inspectProjectWorkspaceSetup({
10
+ projectRoot,
11
+ } = {}) {
12
+ const root = (await gitContext(projectRoot)).repositoryRoot;
13
+ const stack = await readStack(root);
14
+ const diagnostics = [...stack.workspaceSetup.diagnostics];
15
+ if (diagnostics.length === 0) {
16
+ const waitingFor = [];
17
+ for (const step of stack.workspaceSetup.steps) {
18
+ if (!step.readyWhen) continue;
19
+ try {
20
+ await access(path.join(root, step.readyWhen));
21
+ } catch (error) {
22
+ if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error;
23
+ waitingFor.push(step.readyWhen);
24
+ }
25
+ }
26
+ if (waitingFor.length > 0) {
27
+ const paths = uniqueSorted(waitingFor);
28
+ diagnostics.push({
29
+ code: 'STACK_WORKSPACE_SETUP_WAITING',
30
+ message: `Workspace setup is waiting for project ${paths.length === 1 ? 'path' : 'paths'}: ${paths.join(', ')}.`,
31
+ details: { paths },
32
+ });
33
+ }
34
+ }
35
+ const waiting = diagnostics.some(({ code }) => code === 'STACK_WORKSPACE_SETUP_WAITING');
36
+ const blocked = diagnostics.some(({ code }) => code !== 'STACK_WORKSPACE_SETUP_WAITING');
37
+ let status = 'unconfigured';
38
+ if (blocked) status = 'blocked';
39
+ else if (!waiting && stack.workspaceSetup.steps.length > 0) status = 'ready';
40
+ const recipeHash = status === 'ready'
41
+ ? sha256(stableJson({ version: 1, steps: stack.workspaceSetup.steps }))
42
+ : '';
43
+ return {
44
+ status,
45
+ stackHash: stack.identityHash,
46
+ recipeHash,
47
+ components: stack.components.map(({ id }) => id),
48
+ source: stack.workspaceSetup.source,
49
+ runtimeRequirements: uniqueSorted(
50
+ stack.workspaceSetup.steps.flatMap((step) => step.runtimeRequirements),
51
+ ),
52
+ steps: stack.workspaceSetup.steps,
53
+ diagnostics,
54
+ };
55
+ }
package/src/index.js ADDED
@@ -0,0 +1,102 @@
1
+ import path from 'node:path';
2
+
3
+ import { checkProject } from './index/check.js';
4
+ import { buildProjectIndex } from './index/code-index.js';
5
+ import { contextForProjectPaths } from './index/context.js';
6
+ import { generateProjectPrompt } from './index/prompt.js';
7
+ import { initializeProject } from './index/init.js';
8
+ import { installCodexPlugin } from './index/codex-plugin.js';
9
+ import { inspectProjectEnvironment } from './index/environment-files.js';
10
+ import { inspectProjectLaunch } from './index/launch.js';
11
+ import { listBuiltinStackPieces } from './index/stack-catalog.js';
12
+ import { addStackPieces } from './index/stack.js';
13
+ import { verifyProject } from './index/verification.js';
14
+ import { inspectProjectWorkspaceSetup } from './index/workspace-setup.js';
15
+
16
+ function withIndexResult(result, index) {
17
+ const changedFiles = [...new Set([...result.changedFiles, ...index.changedFiles])].sort();
18
+ const refreshedOnly = result.status === 'unchanged' && index.changedFiles.length > 0;
19
+ return {
20
+ ...result,
21
+ status: changedFiles.length > 0 ? 'updated' : result.status,
22
+ summary: refreshedOnly ? `${result.summary} Refreshed the City indexes.` : result.summary,
23
+ changedFiles,
24
+ diagnostics: [...(result.diagnostics || []), ...index.diagnostics],
25
+ };
26
+ }
27
+
28
+ async function initializeWithIndex(projectRoot) {
29
+ const initialized = await initializeProject({ projectRoot });
30
+ const index = await buildProjectIndex({ projectRoot });
31
+ return withIndexResult(initialized, index);
32
+ }
33
+
34
+ export function initialize({ projectRoot = process.cwd() } = {}) {
35
+ return initializeWithIndex(projectRoot);
36
+ }
37
+
38
+ export async function adoptProject({ projectRoot = process.cwd(), request = '' } = {}) {
39
+ const initialized = await initializeWithIndex(projectRoot);
40
+ const description = await generateProjectPrompt({
41
+ projectRoot,
42
+ request,
43
+ task: 'describe',
44
+ });
45
+ return {
46
+ status: 'ready',
47
+ summary: initialized.status === 'updated'
48
+ ? 'Adopted Genesis in this existing project.'
49
+ : 'Genesis is already initialized for this project.',
50
+ changedFiles: initialized.changedFiles,
51
+ prompt: description.prompt,
52
+ warnings: description.warnings,
53
+ guidance: 'Follow the generated description prompt now. Genesis does not start another agent.',
54
+ };
55
+ }
56
+
57
+ export function installCodex({ environment = process.env } = {}) {
58
+ return installCodexPlugin({ environment });
59
+ }
60
+
61
+ export async function addStack({ pieces, projectRoot = process.cwd() } = {}) {
62
+ const root = path.resolve(projectRoot);
63
+ const selected = await addStackPieces({ pieces, projectRoot: root });
64
+ const index = await buildProjectIndex({ projectRoot: root });
65
+ return withIndexResult(selected, index);
66
+ }
67
+
68
+ export function listStackPieces() {
69
+ return listBuiltinStackPieces();
70
+ }
71
+
72
+ export function inspectLaunch(options) {
73
+ return inspectProjectLaunch(options);
74
+ }
75
+
76
+ export function inspectEnvironment(options) {
77
+ return inspectProjectEnvironment(options);
78
+ }
79
+
80
+ export function inspectWorkspaceSetup(options) {
81
+ return inspectProjectWorkspaceSetup(options);
82
+ }
83
+
84
+ export function generatePrompt(options) {
85
+ return generateProjectPrompt(options);
86
+ }
87
+
88
+ export function getContext({ paths, projectRoot = process.cwd() } = {}) {
89
+ return contextForProjectPaths({ paths, projectRoot });
90
+ }
91
+
92
+ export function indexCodebase({ projectRoot = process.cwd(), queries = [], write = true } = {}) {
93
+ return buildProjectIndex({ projectRoot, queries, write });
94
+ }
95
+
96
+ export function verify(options) {
97
+ return verifyProject(options);
98
+ }
99
+
100
+ export function check(options) {
101
+ return checkProject(options);
102
+ }
@@ -0,0 +1,22 @@
1
+ # Stack piece: cpp
2
+
3
+ ## Description
4
+
5
+ C and C++ translation-unit, header, callable, ownership, build, and test conventions.
6
+
7
+ ## Requires
8
+
9
+ - Nothing.
10
+
11
+ ## Indexers
12
+
13
+ - `cpp`
14
+
15
+ ## Deslop
16
+
17
+ - Preserve public header boundaries, ABI expectations, build configuration,
18
+ const correctness, lifetime ownership, and the codebase's C or C++ level.
19
+ - Keep file-local helpers internal and near their callers. Consolidate repeated
20
+ allocation, cleanup, conversion, and error paths with explicit ownership.
21
+ - Remove unused wrappers, speculative templates, duplicate overload plumbing,
22
+ and abstractions that obscure control flow without protecting a real boundary.
@@ -0,0 +1,22 @@
1
+ # Stack piece: csharp
2
+
3
+ ## Description
4
+
5
+ C# namespace, public type, dependency, lifecycle, and test conventions.
6
+
7
+ ## Requires
8
+
9
+ - Nothing.
10
+
11
+ ## Indexers
12
+
13
+ - `csharp`
14
+
15
+ ## Deslop
16
+
17
+ - Preserve established namespaces, public contracts, dependency injection,
18
+ async ownership, disposal, nullable annotations, and project conventions.
19
+ - Keep private helpers with their owning type. Consolidate repeated mapping,
20
+ validation, and error translation at an existing application boundary.
21
+ - Remove redundant service wrappers, interfaces with no useful substitution
22
+ boundary, and extension methods that only rename direct framework behavior.
@@ -0,0 +1,22 @@
1
+ # Stack piece: go
2
+
3
+ ## Description
4
+
5
+ Go package, exported callable, interface, concurrency, resource, and test conventions.
6
+
7
+ ## Requires
8
+
9
+ - Nothing.
10
+
11
+ ## Indexers
12
+
13
+ - `go`
14
+
15
+ ## Deslop
16
+
17
+ - Preserve package ownership, exported APIs, context propagation, error
18
+ wrapping, goroutine lifetime, and standard Go tooling conventions.
19
+ - Keep unexported helpers close to their caller. Consolidate repeated setup,
20
+ validation, cleanup, and conversion only at a clear package boundary.
21
+ - Remove unnecessary interfaces, constructor wrappers, channels, and generic
22
+ helpers that make direct control flow harder to follow.