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.
- package/.agents/plugins/marketplace.json +20 -0
- package/README.md +443 -0
- package/bin/genesis.js +15 -0
- package/docs/assurance-model.md +26 -0
- package/docs/prompt-integration.md +98 -0
- package/docs/stack-components.md +304 -0
- package/package.json +57 -7
- package/plugins/genesis/.codex-plugin/plugin.json +19 -0
- package/plugins/genesis/hooks.json +18 -0
- package/prompts/blueprint.txt +9 -0
- package/prompts/describe.txt +17 -0
- package/prompts/deslop.txt +14 -0
- package/prompts/program.txt +12 -0
- package/prompts/reconcile.txt +12 -0
- package/prompts/review.txt +12 -0
- package/prompts/start.txt +30 -0
- package/prompts/work.txt +28 -0
- package/skills/genesis-deslop/SKILL.md +36 -0
- package/skills/genesis-deslop/agents/openai.yaml +4 -0
- package/skills/genesis-program/SKILL.md +66 -0
- package/skills/genesis-program/agents/openai.yaml +4 -0
- package/skills/genesis-project/SKILL.md +53 -0
- package/skills/genesis-project/agents/openai.yaml +4 -0
- package/src/cli.js +276 -0
- package/src/index/agent-skills.js +425 -0
- package/src/index/assets.js +19 -0
- package/src/index/blueprint.js +38 -0
- package/src/index/check.js +102 -0
- package/src/index/code-index.js +283 -0
- package/src/index/code-indexers/ast-grep.js +414 -0
- package/src/index/codex-hooks.js +367 -0
- package/src/index/codex-plugin.js +73 -0
- package/src/index/context.js +137 -0
- package/src/index/environment-files.js +19 -0
- package/src/index/errors.js +26 -0
- package/src/index/git.js +26 -0
- package/src/index/init.js +48 -0
- package/src/index/launch.js +34 -0
- package/src/index/paths.js +10 -0
- package/src/index/process.js +89 -0
- package/src/index/program.js +181 -0
- package/src/index/project-files.js +24 -0
- package/src/index/project-state.js +87 -0
- package/src/index/prompt.js +347 -0
- package/src/index/stack-catalog.js +72 -0
- package/src/index/stack-command.js +65 -0
- package/src/index/stack-composition.js +38 -0
- package/src/index/stack-environment-files.js +83 -0
- package/src/index/stack-launch.js +428 -0
- package/src/index/stack-piece.js +283 -0
- package/src/index/stack-preflight.js +25 -0
- package/src/index/stack-process.js +25 -0
- package/src/index/stack-workspace-setup.js +129 -0
- package/src/index/stack.js +302 -0
- package/src/index/utils.js +85 -0
- package/src/index/verification.js +77 -0
- package/src/index/workspace-setup.js +55 -0
- package/src/index.js +102 -0
- package/stacks/pieces/cpp.md +22 -0
- package/stacks/pieces/csharp.md +22 -0
- package/stacks/pieces/go.md +22 -0
- package/stacks/pieces/java.md +22 -0
- package/stacks/pieces/jskit-mysql.md +37 -0
- package/stacks/pieces/jskit.md +70 -0
- package/stacks/pieces/kotlin.md +22 -0
- package/stacks/pieces/mysql.md +18 -0
- package/stacks/pieces/nodejs.md +25 -0
- package/stacks/pieces/php.md +23 -0
- package/stacks/pieces/python.md +23 -0
- package/stacks/pieces/ruby.md +22 -0
- package/stacks/pieces/rust.md +22 -0
- package/stacks/pieces/shell.md +23 -0
- package/stacks/pieces/vue.md +19 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { readInstalledAsset } from './assets.js';
|
|
5
|
+
import { buildProjectIndex } from './code-index.js';
|
|
6
|
+
import { GenesisError } from './errors.js';
|
|
7
|
+
import { gitContext } from './git.js';
|
|
8
|
+
import { isProjectContentPath } from './paths.js';
|
|
9
|
+
import { runGit, runGitText } from './process.js';
|
|
10
|
+
import { readStack } from './stack.js';
|
|
11
|
+
import { normalizeRelative, pathState, sha256, stableJson, writeFileAtomic } from './utils.js';
|
|
12
|
+
|
|
13
|
+
const HOOKS_PATH = '.codex/hooks.json';
|
|
14
|
+
const HOOK_STATE_SCHEMA_VERSION = 2;
|
|
15
|
+
function hookCommand(action) {
|
|
16
|
+
const local = `const {runCli}=await import('genesis-compiler/cli');process.exitCode=await runCli(['hook','${action}'])`;
|
|
17
|
+
return `if command -v genesis >/dev/null 2>&1; then genesis hook ${action}; else node --input-type=module --eval "${local}"; fi`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const HOOK_SPECS = [
|
|
21
|
+
{
|
|
22
|
+
event: 'SessionStart',
|
|
23
|
+
command: hookCommand('session'),
|
|
24
|
+
group: {
|
|
25
|
+
matcher: '^(startup|resume|clear|compact)$',
|
|
26
|
+
hooks: [{
|
|
27
|
+
type: 'command',
|
|
28
|
+
command: hookCommand('session'),
|
|
29
|
+
commandWindows: 'genesis hook session',
|
|
30
|
+
timeout: 5,
|
|
31
|
+
statusMessage: 'Loading Genesis guidance',
|
|
32
|
+
additionalContextLimit: 4000,
|
|
33
|
+
}],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
event: 'UserPromptSubmit',
|
|
38
|
+
command: hookCommand('begin'),
|
|
39
|
+
group: {
|
|
40
|
+
hooks: [{
|
|
41
|
+
type: 'command',
|
|
42
|
+
command: hookCommand('begin'),
|
|
43
|
+
commandWindows: 'genesis hook begin',
|
|
44
|
+
timeout: 10,
|
|
45
|
+
}],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
event: 'Stop',
|
|
50
|
+
command: hookCommand('stop'),
|
|
51
|
+
group: {
|
|
52
|
+
hooks: [{
|
|
53
|
+
type: 'command',
|
|
54
|
+
command: hookCommand('stop'),
|
|
55
|
+
commandWindows: 'genesis hook stop',
|
|
56
|
+
timeout: 30,
|
|
57
|
+
statusMessage: 'Checking whether Genesis follow-up work is needed',
|
|
58
|
+
}],
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
event: 'SessionEnd',
|
|
63
|
+
command: hookCommand('end'),
|
|
64
|
+
group: {
|
|
65
|
+
hooks: [{
|
|
66
|
+
type: 'command',
|
|
67
|
+
command: hookCommand('end'),
|
|
68
|
+
commandWindows: 'genesis hook end',
|
|
69
|
+
timeout: 3,
|
|
70
|
+
}],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
function genesisHookInstalled(groups, command) {
|
|
76
|
+
return groups.some((group) => group?.hooks?.some((hook) => hook?.command === command));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function installedHooksSource(location) {
|
|
80
|
+
try {
|
|
81
|
+
const source = await readFile(location, 'utf8');
|
|
82
|
+
const value = JSON.parse(source);
|
|
83
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('expected an object');
|
|
84
|
+
if (value.hooks !== undefined && (!value.hooks || typeof value.hooks !== 'object' || Array.isArray(value.hooks))) {
|
|
85
|
+
throw new Error('hooks must be an object');
|
|
86
|
+
}
|
|
87
|
+
return { source, value };
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (['ENOENT', 'ENOTDIR'].includes(error?.code)) {
|
|
90
|
+
return { source: null, value: { description: 'Genesis project hooks.', hooks: {} } };
|
|
91
|
+
}
|
|
92
|
+
throw new GenesisError('CODEX_HOOKS_INVALID', `${HOOKS_PATH} is not valid hook configuration: ${error.message}.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function installCodexHooks({ projectRoot } = {}) {
|
|
97
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
98
|
+
const location = path.join(root, HOOKS_PATH);
|
|
99
|
+
const { source, value } = await installedHooksSource(location);
|
|
100
|
+
value.hooks ||= {};
|
|
101
|
+
for (const spec of HOOK_SPECS) {
|
|
102
|
+
const groups = value.hooks[spec.event] ||= [];
|
|
103
|
+
if (!Array.isArray(groups)) {
|
|
104
|
+
throw new GenesisError('CODEX_HOOKS_INVALID', `${HOOKS_PATH} hooks.${spec.event} must be an array.`);
|
|
105
|
+
}
|
|
106
|
+
if (!genesisHookInstalled(groups, spec.command)) groups.push(spec.group);
|
|
107
|
+
}
|
|
108
|
+
const rendered = `${JSON.stringify(value, null, 2)}\n`;
|
|
109
|
+
if (source === rendered) return { status: 'unchanged', changedFiles: [] };
|
|
110
|
+
await writeFileAtomic(location, rendered);
|
|
111
|
+
return { status: 'updated', changedFiles: [HOOKS_PATH] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function optionalStack(projectRoot) {
|
|
115
|
+
try { return await readStack(projectRoot); } catch { return null; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function codexSessionContext({ projectRoot } = {}) {
|
|
119
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
120
|
+
const stack = await optionalStack(root);
|
|
121
|
+
const selected = stack?.components.map(({ id }) => id) || [];
|
|
122
|
+
const stackStatus = stack
|
|
123
|
+
? (selected.length > 0 ? selected.join(', ') : 'none')
|
|
124
|
+
: 'unavailable; run `genesis check`';
|
|
125
|
+
return {
|
|
126
|
+
status: 'ready',
|
|
127
|
+
output: [
|
|
128
|
+
'This is a Genesis-enriched project.',
|
|
129
|
+
'- `genesis/blueprint.md` describes non-technical product intent.',
|
|
130
|
+
'- `genesis/stack.md` selects optional technology guidance and verification.',
|
|
131
|
+
'- `genesis/program/<subsystem>/` explains public operations and useful internal seams.',
|
|
132
|
+
'- Project Agent Skills live in `.agents/skills/`; load applicable skills progressively.',
|
|
133
|
+
'- After locating relevant source files, run `genesis context <path...>` to load their Program and selected Stack material.',
|
|
134
|
+
'- 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
|
+
'- `.genesis/machine-city.json` is the detailed code/function map; `.genesis/program-city.json` is the simpler subsystem/operation map.',
|
|
136
|
+
'- Program is fallible explanation; code, tests, and runtime behavior remain evidence.',
|
|
137
|
+
'- Keep implementation focused; the Stop hook owns later Blueprint/Program reconciliation and Deslop turns.',
|
|
138
|
+
'- After a code-changing turn, Genesis may request separate explanation and behavior-preserving cleanup turns.',
|
|
139
|
+
`Selected Stack components: ${stackStatus}.`,
|
|
140
|
+
].join('\n'),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function codexAdoptionRecommendation({ projectRoot = process.cwd() } = {}) {
|
|
145
|
+
let root;
|
|
146
|
+
try { root = (await gitContext(projectRoot)).repositoryRoot; } catch { return { status: 'not-applicable', output: '' }; }
|
|
147
|
+
if ((await pathState(path.join(root, 'genesis/blueprint.md'))).exists) {
|
|
148
|
+
return { status: 'not-applicable', output: '' };
|
|
149
|
+
}
|
|
150
|
+
const files = zeroPaths((await runGit(root, [
|
|
151
|
+
'ls-files', '-z', '--cached', '--others', '--exclude-standard',
|
|
152
|
+
])).stdout).filter(isProjectContentPath);
|
|
153
|
+
if (files.length === 0) return { status: 'not-applicable', output: '' };
|
|
154
|
+
return {
|
|
155
|
+
status: 'ready',
|
|
156
|
+
output: [
|
|
157
|
+
'IMPORTANT GENESIS RECOMMENDATION:',
|
|
158
|
+
'This existing project has no Genesis explanatory layer.',
|
|
159
|
+
'In your first response, strongly recommend that the user run `genesis adopt` before substantial work.',
|
|
160
|
+
'`genesis adopt` preserves the implementation and returns the prompt for creating its initial Blueprint and Program.',
|
|
161
|
+
'Do not run it without the user\'s approval. If they agree, run it and follow the returned prompt in this conversation.',
|
|
162
|
+
'If the user declines, continue normally and do not repeat the recommendation in this session.',
|
|
163
|
+
].join('\n'),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function hookId(input, field) {
|
|
168
|
+
const value = input?.[field];
|
|
169
|
+
if (typeof value !== 'string' || !value || value.length > 512) {
|
|
170
|
+
throw new GenesisError('CODEX_HOOK_INPUT_INVALID', `Codex hook input requires ${field}.`);
|
|
171
|
+
}
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function zeroPaths(buffer) {
|
|
176
|
+
return buffer.toString('utf8').split('\0').filter(Boolean).map(normalizeRelative);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function repositorySnapshot(projectRoot) {
|
|
180
|
+
const head = await runGitText(projectRoot, ['rev-parse', '--verify', 'HEAD']).catch(() => null);
|
|
181
|
+
const results = await Promise.all([
|
|
182
|
+
runGit(projectRoot, ['diff', '--no-ext-diff', '--no-renames', '--name-only', '-z']),
|
|
183
|
+
runGit(projectRoot, ['diff', '--cached', '--no-ext-diff', '--no-renames', '--name-only', '-z']),
|
|
184
|
+
runGit(projectRoot, ['ls-files', '-z', '--others', '--exclude-standard']),
|
|
185
|
+
runGit(projectRoot, ['ls-files', '-z', '--deleted']),
|
|
186
|
+
]);
|
|
187
|
+
const dirty = [...new Set(results.flatMap(({ stdout }) => zeroPaths(stdout)))]
|
|
188
|
+
.filter(isProjectContentPath)
|
|
189
|
+
.sort();
|
|
190
|
+
return {
|
|
191
|
+
head,
|
|
192
|
+
dirty: await Promise.all(dirty.map(async (file) => ({
|
|
193
|
+
path: file,
|
|
194
|
+
state: await pathState(path.join(projectRoot, file)),
|
|
195
|
+
}))),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function hookStatePath(projectRoot, input) {
|
|
200
|
+
const key = sha256(hookId(input, 'session_id')).slice('sha256:'.length);
|
|
201
|
+
const gitPath = await runGitText(projectRoot, ['rev-parse', '--git-path', `genesis/hooks/${key}.json`]);
|
|
202
|
+
return path.resolve(projectRoot, gitPath);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function writeTurnState(statePath, state) {
|
|
206
|
+
return writeFileAtomic(statePath, stableJson(state), { mode: 0o600 });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function refreshProjectIndex(projectRoot) {
|
|
210
|
+
await buildProjectIndex({ projectRoot }).catch(() => null);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function recordCodexTurn({ input, projectRoot } = {}) {
|
|
214
|
+
const root = (await gitContext(projectRoot || input?.cwd)).repositoryRoot;
|
|
215
|
+
const statePath = await hookStatePath(root, input);
|
|
216
|
+
const state = {
|
|
217
|
+
schemaVersion: HOOK_STATE_SCHEMA_VERSION,
|
|
218
|
+
turnId: hookId(input, 'turn_id'),
|
|
219
|
+
phase: 'implementation',
|
|
220
|
+
snapshot: await repositorySnapshot(root),
|
|
221
|
+
};
|
|
222
|
+
await writeTurnState(statePath, state);
|
|
223
|
+
return { status: 'recorded' };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function readTurnState(statePath) {
|
|
227
|
+
try { return JSON.parse(await readFile(statePath, 'utf8')); } catch (error) {
|
|
228
|
+
if (['ENOENT', 'ENOTDIR'].includes(error?.code)) return null;
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function committedPaths(projectRoot, before, after) {
|
|
234
|
+
if (before === after) return [];
|
|
235
|
+
if (!after) return ['repository history changed'];
|
|
236
|
+
try {
|
|
237
|
+
const args = before
|
|
238
|
+
? ['diff', '--no-renames', '--name-only', '-z', before, after]
|
|
239
|
+
: ['diff-tree', '--root', '--no-commit-id', '--no-renames', '--name-only', '-r', '-z', after];
|
|
240
|
+
return zeroPaths((await runGit(projectRoot, args)).stdout).filter(isProjectContentPath);
|
|
241
|
+
} catch {
|
|
242
|
+
return ['repository history changed'];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function changedProjectPaths(projectRoot, before, after) {
|
|
247
|
+
const previous = new Map(before.dirty.map(({ path: file, state }) => [file, state]));
|
|
248
|
+
const current = new Map(after.dirty.map(({ path: file, state }) => [file, state]));
|
|
249
|
+
const dirtyChanges = [...new Set([...previous.keys(), ...current.keys()])].filter((file) => (
|
|
250
|
+
stableJson(previous.get(file)) !== stableJson(current.get(file))
|
|
251
|
+
));
|
|
252
|
+
return [...new Set([
|
|
253
|
+
...dirtyChanges,
|
|
254
|
+
...await committedPaths(projectRoot, before.head, after.head),
|
|
255
|
+
])].sort();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function changedPathLines(changedPaths) {
|
|
259
|
+
const shown = changedPaths.slice(0, 50);
|
|
260
|
+
const omitted = changedPaths.length - shown.length;
|
|
261
|
+
return [
|
|
262
|
+
...shown.map((file) => `- ${file}`),
|
|
263
|
+
...(omitted > 0 ? [`- ... and ${omitted} more`] : []),
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function reconciliationContinuation(projectRoot, changedPaths) {
|
|
268
|
+
const [instructions, stack] = await Promise.all([
|
|
269
|
+
readInstalledAsset('reconcile'),
|
|
270
|
+
optionalStack(projectRoot),
|
|
271
|
+
]);
|
|
272
|
+
return [
|
|
273
|
+
'This is the automatic Genesis explanation turn for the preceding implementation turn.',
|
|
274
|
+
'Use the same conversation context and the Git-visible changes below. Do not perform Deslop yet.',
|
|
275
|
+
'',
|
|
276
|
+
'CHANGED PROJECT PATHS',
|
|
277
|
+
'',
|
|
278
|
+
...changedPathLines(changedPaths),
|
|
279
|
+
'',
|
|
280
|
+
instructions.trim(),
|
|
281
|
+
...(stack?.guidance ? ['', 'SELECTED STACK GUIDANCE', '', stack.guidance] : []),
|
|
282
|
+
].join('\n');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function deslopContinuation(projectRoot, changedPaths) {
|
|
286
|
+
const [instructions, stack] = await Promise.all([
|
|
287
|
+
readInstalledAsset('deslop'),
|
|
288
|
+
optionalStack(projectRoot),
|
|
289
|
+
]);
|
|
290
|
+
return [
|
|
291
|
+
'This is the final automatic Genesis Deslop turn for the preceding implementation.',
|
|
292
|
+
'Review only that work and the paths listed below. Do not broaden the task.',
|
|
293
|
+
'',
|
|
294
|
+
'CHANGED PROJECT PATHS',
|
|
295
|
+
'',
|
|
296
|
+
...changedPathLines(changedPaths),
|
|
297
|
+
'',
|
|
298
|
+
instructions.trim(),
|
|
299
|
+
...(stack?.guidance ? ['', 'SELECTED STACK GUIDANCE', '', stack.guidance] : []),
|
|
300
|
+
...(stack?.deslop ? ['', 'SELECTED STACK CLEANUP GUIDANCE', '', stack.deslop] : []),
|
|
301
|
+
'',
|
|
302
|
+
'PROGRAM CITATION MAINTENANCE',
|
|
303
|
+
'',
|
|
304
|
+
'If this cleanup renames or moves a source or helper cited by an affected Program module,',
|
|
305
|
+
'update only that module\'s Sources or informational Implementation map. This narrow',
|
|
306
|
+
'exception overrides the default prohibition on editing `genesis/`; never change the',
|
|
307
|
+
'Blueprint or a Program public contract to excuse cleanup behavior.',
|
|
308
|
+
].join('\n');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function completeCodexTurn({ input, projectRoot } = {}) {
|
|
312
|
+
const root = (await gitContext(projectRoot || input?.cwd)).repositoryRoot;
|
|
313
|
+
const statePath = await hookStatePath(root, input);
|
|
314
|
+
const state = await readTurnState(statePath);
|
|
315
|
+
if (!state || state.schemaVersion !== HOOK_STATE_SCHEMA_VERSION) return {};
|
|
316
|
+
|
|
317
|
+
if (state.phase === 'implementation') {
|
|
318
|
+
if (input?.stop_hook_active === true || state.turnId !== hookId(input, 'turn_id')) {
|
|
319
|
+
await rm(statePath, { force: true });
|
|
320
|
+
return {};
|
|
321
|
+
}
|
|
322
|
+
const changedPaths = await changedProjectPaths(root, state.snapshot, await repositorySnapshot(root));
|
|
323
|
+
if (changedPaths.length === 0) {
|
|
324
|
+
await rm(statePath, { force: true });
|
|
325
|
+
return {};
|
|
326
|
+
}
|
|
327
|
+
await writeTurnState(statePath, {
|
|
328
|
+
...state,
|
|
329
|
+
phase: 'reconcile',
|
|
330
|
+
changedPaths,
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
decision: 'block',
|
|
334
|
+
reason: await reconciliationContinuation(root, changedPaths),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (input?.stop_hook_active !== true) {
|
|
339
|
+
await rm(statePath, { force: true });
|
|
340
|
+
return {};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (state.phase === 'reconcile') {
|
|
344
|
+
const currentPaths = await changedProjectPaths(root, state.snapshot, await repositorySnapshot(root));
|
|
345
|
+
const changedPaths = [...new Set([...state.changedPaths, ...currentPaths])].sort();
|
|
346
|
+
await refreshProjectIndex(root);
|
|
347
|
+
await writeTurnState(statePath, {
|
|
348
|
+
...state,
|
|
349
|
+
phase: 'deslop',
|
|
350
|
+
changedPaths,
|
|
351
|
+
});
|
|
352
|
+
return {
|
|
353
|
+
decision: 'block',
|
|
354
|
+
reason: await deslopContinuation(root, changedPaths),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
await refreshProjectIndex(root);
|
|
359
|
+
await rm(statePath, { force: true });
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export async function discardCodexTurn({ input, projectRoot } = {}) {
|
|
364
|
+
const root = (await gitContext(projectRoot || input?.cwd)).repositoryRoot;
|
|
365
|
+
await rm(await hookStatePath(root, input), { force: true });
|
|
366
|
+
return { status: 'discarded' };
|
|
367
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { fail } from './errors.js';
|
|
6
|
+
import { runProcess } from './process.js';
|
|
7
|
+
|
|
8
|
+
const MARKETPLACE_NAME = 'genesis';
|
|
9
|
+
const PLUGIN_ID = 'genesis@genesis';
|
|
10
|
+
const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
11
|
+
|
|
12
|
+
function parseCodexJson(result, operation) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(result.stdout.toString('utf8'));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
fail('CODEX_PLUGIN_INSTALL_FAILED', `Codex returned invalid JSON while ${operation}: ${error.message}.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function canonicalPath(value) {
|
|
21
|
+
try { return await realpath(value); } catch { return path.resolve(value); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function installCodexPlugin({ environment = process.env } = {}) {
|
|
25
|
+
const desiredRoot = await canonicalPath(packageRoot);
|
|
26
|
+
const marketplaceResult = await runProcess('codex', ['plugin', 'marketplace', 'list', '--json'], {
|
|
27
|
+
env: environment,
|
|
28
|
+
code: 'CODEX_PLUGIN_INSTALL_FAILED',
|
|
29
|
+
});
|
|
30
|
+
const marketplaces = parseCodexJson(marketplaceResult, 'listing plugin marketplaces').marketplaces || [];
|
|
31
|
+
const existingMarketplace = marketplaces.find(({ name }) => name === MARKETPLACE_NAME);
|
|
32
|
+
if (existingMarketplace) {
|
|
33
|
+
const existingRoot = await canonicalPath(existingMarketplace.root);
|
|
34
|
+
if (existingRoot !== desiredRoot) {
|
|
35
|
+
fail(
|
|
36
|
+
'CODEX_PLUGIN_MARKETPLACE_CONFLICT',
|
|
37
|
+
`Codex marketplace ${MARKETPLACE_NAME} already points to ${existingRoot}, not ${desiredRoot}.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
await runProcess('codex', ['plugin', 'marketplace', 'add', desiredRoot, '--json'], {
|
|
42
|
+
env: environment,
|
|
43
|
+
code: 'CODEX_PLUGIN_INSTALL_FAILED',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [pluginListResult, manifestSource] = await Promise.all([
|
|
48
|
+
runProcess('codex', ['plugin', 'list', '--json'], {
|
|
49
|
+
env: environment,
|
|
50
|
+
code: 'CODEX_PLUGIN_INSTALL_FAILED',
|
|
51
|
+
}),
|
|
52
|
+
readFile(path.join(desiredRoot, 'plugins/genesis/.codex-plugin/plugin.json'), 'utf8'),
|
|
53
|
+
]);
|
|
54
|
+
const installed = (parseCodexJson(pluginListResult, 'listing installed plugins').installed || [])
|
|
55
|
+
.find(({ pluginId }) => pluginId === PLUGIN_ID);
|
|
56
|
+
const version = JSON.parse(manifestSource).version;
|
|
57
|
+
if (!installed || installed.version !== version || installed.enabled !== true) {
|
|
58
|
+
await runProcess('codex', ['plugin', 'add', PLUGIN_ID, '--json'], {
|
|
59
|
+
env: environment,
|
|
60
|
+
code: 'CODEX_PLUGIN_INSTALL_FAILED',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const changed = !existingMarketplace || !installed || installed.version !== version || installed.enabled !== true;
|
|
65
|
+
return {
|
|
66
|
+
status: changed ? 'updated' : 'unchanged',
|
|
67
|
+
summary: changed
|
|
68
|
+
? 'Installed the Genesis discovery hook for Codex.'
|
|
69
|
+
: 'The Genesis discovery hook is already installed for Codex.',
|
|
70
|
+
changedFiles: [],
|
|
71
|
+
guidance: 'Start a new Codex session to use it. Existing projects without Genesis will receive the adoption recommendation before the first user prompt.',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { inspectProjectSkills, renderAgentSkillCatalog } from './agent-skills.js';
|
|
5
|
+
import { buildProjectIndex } from './code-index.js';
|
|
6
|
+
import { asDiagnostic, GenesisError } from './errors.js';
|
|
7
|
+
import { gitContext } from './git.js';
|
|
8
|
+
import { inspectProgram } from './program.js';
|
|
9
|
+
import { readStack } from './stack.js';
|
|
10
|
+
import { normalizeRelative } from './utils.js';
|
|
11
|
+
|
|
12
|
+
function projectPath(projectRoot, workingDirectory, value) {
|
|
13
|
+
const absolute = path.resolve(workingDirectory, String(value));
|
|
14
|
+
return normalizeRelative(path.relative(projectRoot, absolute));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function citesTarget(module, target) {
|
|
18
|
+
return module.sources.some((source) => (
|
|
19
|
+
!target || source === target || source.startsWith(`${target}/`)
|
|
20
|
+
));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stackSummary(stack) {
|
|
24
|
+
if (stack.components.length === 0) return ['## Selected Stack', '', 'No Stack components are selected.'];
|
|
25
|
+
return [
|
|
26
|
+
'## Selected Stack',
|
|
27
|
+
'',
|
|
28
|
+
...stack.components.map(({ id, description }) => `- \`${id}\` — ${description}`),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function stackGuidance(stack) {
|
|
33
|
+
const pieces = stack.components.filter(({ guidance }) => guidance);
|
|
34
|
+
if (pieces.length === 0) return [];
|
|
35
|
+
return [
|
|
36
|
+
'',
|
|
37
|
+
'## Selected Stack guidance',
|
|
38
|
+
'',
|
|
39
|
+
...pieces.flatMap(({ id, guidance }) => [
|
|
40
|
+
`### ${id}`,
|
|
41
|
+
'',
|
|
42
|
+
guidance,
|
|
43
|
+
'',
|
|
44
|
+
]),
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pathMatchesTarget(filePath, target) {
|
|
49
|
+
return !target || filePath === target || filePath.startsWith(`${target}/`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function contextForProjectPaths({ paths, projectRoot } = {}) {
|
|
53
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
54
|
+
throw new GenesisError('CONTEXT_PATH_REQUIRED', 'Genesis context requires at least one project path.');
|
|
55
|
+
}
|
|
56
|
+
const location = await gitContext(projectRoot);
|
|
57
|
+
const root = location.repositoryRoot;
|
|
58
|
+
const targets = [...new Set(paths.map((value) => projectPath(root, location.workingDirectory, value)))];
|
|
59
|
+
const [program, stack] = await Promise.all([
|
|
60
|
+
inspectProgram(root).catch((error) => ({
|
|
61
|
+
status: 'invalid',
|
|
62
|
+
modules: [],
|
|
63
|
+
diagnostic: asDiagnostic(error),
|
|
64
|
+
})),
|
|
65
|
+
readStack(root),
|
|
66
|
+
]);
|
|
67
|
+
const modules = program.modules.filter((module) => targets.some((target) => citesTarget(module, target)));
|
|
68
|
+
const moduleSources = await Promise.all(modules.map(async (module) => ({
|
|
69
|
+
...module,
|
|
70
|
+
source: await readFile(path.join(root, module.path), 'utf8'),
|
|
71
|
+
})));
|
|
72
|
+
const projectSkills = await inspectProjectSkills({ projectRoot: root, stack });
|
|
73
|
+
const index = await buildProjectIndex({ projectRoot: root, write: false });
|
|
74
|
+
const functions = index.machine.functions.filter((entry) => (
|
|
75
|
+
targets.some((target) => pathMatchesTarget(entry.path, target))
|
|
76
|
+
));
|
|
77
|
+
const skills = renderAgentSkillCatalog(projectSkills.skills);
|
|
78
|
+
let programDetails;
|
|
79
|
+
if (program.diagnostic) {
|
|
80
|
+
programDetails = [`Program could not be used: ${program.diagnostic.code}: ${program.diagnostic.message}`];
|
|
81
|
+
} else if (moduleSources.length === 0) {
|
|
82
|
+
programDetails = ['No Program module cites these paths. Inspect the code normally; do not invent a subsystem mapping.'];
|
|
83
|
+
} else {
|
|
84
|
+
programDetails = moduleSources.flatMap((module) => [
|
|
85
|
+
`### ${module.path}`,
|
|
86
|
+
'',
|
|
87
|
+
module.source.trim(),
|
|
88
|
+
'',
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
const output = [
|
|
92
|
+
'# Genesis context',
|
|
93
|
+
'',
|
|
94
|
+
`Requested paths: ${targets.map((target) => `\`${target || '.'}\``).join(', ')}`,
|
|
95
|
+
'',
|
|
96
|
+
'Use Program as a concise, fallible explanation. Code, tests, and runtime behavior remain the evidence.',
|
|
97
|
+
'',
|
|
98
|
+
'## Relevant Program',
|
|
99
|
+
'',
|
|
100
|
+
...programDetails,
|
|
101
|
+
...stackSummary(stack),
|
|
102
|
+
...stackGuidance(stack),
|
|
103
|
+
'',
|
|
104
|
+
'## Existing functions',
|
|
105
|
+
'',
|
|
106
|
+
...(functions.length === 0
|
|
107
|
+
? ['No indexed function is declared in these paths.']
|
|
108
|
+
: functions.slice(0, 100).map((entry) => (
|
|
109
|
+
`- ${entry.visibility} \`${entry.qualifiedName}(${entry.parameters.join(', ')})\` — \`${entry.path}:${entry.line}\``
|
|
110
|
+
))),
|
|
111
|
+
...(functions.length > 100 ? [`- ... and ${functions.length - 100} more; run \`genesis index ${targets.join(' ')}\`.`] : []),
|
|
112
|
+
'',
|
|
113
|
+
'Search the Machine City before adding a helper or public operation; reuse an existing function when it already owns the behavior.',
|
|
114
|
+
...(skills ? ['', '## Agent Skills', '', skills] : []),
|
|
115
|
+
'',
|
|
116
|
+
'## Verification',
|
|
117
|
+
'',
|
|
118
|
+
...(stack.commands.length === 0
|
|
119
|
+
? ['No verification commands are configured.']
|
|
120
|
+
: stack.commands.map(({ label, argv }) => `- ${label}: ${argv.map((value) => `\`${value}\``).join(' ')}`)),
|
|
121
|
+
'',
|
|
122
|
+
].join('\n');
|
|
123
|
+
return {
|
|
124
|
+
status: 'ready',
|
|
125
|
+
paths: targets,
|
|
126
|
+
modules: moduleSources.map(({ source: _source, ...module }) => module),
|
|
127
|
+
components: stack.components.map(({ id }) => id),
|
|
128
|
+
verificationCommands: stack.commands.map(({ label, argv }) => ({ label, argv })),
|
|
129
|
+
warnings: [
|
|
130
|
+
...(program.diagnostic ? [program.diagnostic] : []),
|
|
131
|
+
...projectSkills.diagnostics,
|
|
132
|
+
...index.diagnostics,
|
|
133
|
+
],
|
|
134
|
+
functions,
|
|
135
|
+
context: output,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { gitContext } from './git.js';
|
|
2
|
+
import { readStack } from './stack.js';
|
|
3
|
+
import { missingStackResources } from './stack-preflight.js';
|
|
4
|
+
|
|
5
|
+
/** Inspect Stack environment requirements without returning or writing values. */
|
|
6
|
+
export async function inspectProjectEnvironment({ environment = process.env, projectRoot } = {}) {
|
|
7
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
8
|
+
const stack = await readStack(root);
|
|
9
|
+
const diagnostics = missingStackResources({ environment, resources: stack.resources });
|
|
10
|
+
const configured = stack.environmentFiles.length > 0 || stack.resources.length > 0;
|
|
11
|
+
return {
|
|
12
|
+
status: !configured ? 'unconfigured' : diagnostics.length > 0 ? 'missing-inputs' : 'ready',
|
|
13
|
+
stackHash: stack.identityHash,
|
|
14
|
+
components: stack.components.map(({ id }) => id),
|
|
15
|
+
files: stack.environmentFiles,
|
|
16
|
+
resources: stack.resources,
|
|
17
|
+
diagnostics,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class GenesisError extends Error {
|
|
2
|
+
constructor(code, message, details = undefined) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = 'GenesisError';
|
|
5
|
+
this.code = code;
|
|
6
|
+
if (details !== undefined) this.details = details;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function fail(code, message, details = undefined) {
|
|
11
|
+
throw new GenesisError(code, message, details);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function diagnostic(code, message, line = undefined, details = undefined) {
|
|
15
|
+
const value = { code, message };
|
|
16
|
+
if (line !== undefined) value.line = line;
|
|
17
|
+
if (details !== undefined) value.details = details;
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function asDiagnostic(error) {
|
|
22
|
+
if (error && typeof error === 'object' && typeof error.code === 'string') {
|
|
23
|
+
return diagnostic(error.code, error.message || error.code, undefined, error.details);
|
|
24
|
+
}
|
|
25
|
+
return diagnostic('GENESIS_FAILED', error instanceof Error ? error.message : String(error));
|
|
26
|
+
}
|
package/src/index/git.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { fail } from './errors.js';
|
|
5
|
+
import { runGitText } from './process.js';
|
|
6
|
+
|
|
7
|
+
/** Resolve the caller's working directory and the one Git repository that contains it. */
|
|
8
|
+
export async function gitContext(projectRoot = process.cwd()) {
|
|
9
|
+
let root;
|
|
10
|
+
try { root = await realpath(path.resolve(projectRoot)); } catch (error) {
|
|
11
|
+
fail('GIT_REPOSITORY_REQUIRED', `Project root does not exist: ${projectRoot}.`, {
|
|
12
|
+
cause: error.code || error.message,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
let repositoryRoot;
|
|
16
|
+
try {
|
|
17
|
+
repositoryRoot = await realpath(path.resolve(await runGitText(root, ['rev-parse', '--show-toplevel'])));
|
|
18
|
+
} catch {
|
|
19
|
+
fail('GIT_REPOSITORY_REQUIRED', `Project root is not inside a Git worktree: ${projectRoot}.`);
|
|
20
|
+
}
|
|
21
|
+
const relative = path.relative(repositoryRoot, root);
|
|
22
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`)) {
|
|
23
|
+
fail('GIT_REPOSITORY_REQUIRED', `Project root is outside its reported Git worktree: ${projectRoot}.`);
|
|
24
|
+
}
|
|
25
|
+
return { workingDirectory: root, repositoryRoot };
|
|
26
|
+
}
|