genesis-compiler 1.0.0 → 1.1.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 +423 -0
- package/bin/genesis.js +15 -0
- package/docs/assurance-model.md +26 -0
- package/docs/prompt-integration.md +92 -0
- package/docs/stack-components.md +269 -0
- package/package.json +56 -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/work.txt +21 -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 +18 -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/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 +78 -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 +239 -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-launch.js +428 -0
- package/src/index/stack-piece.js +277 -0
- package/src/index/stack-preflight.js +25 -0
- package/src/index/stack-process.js +25 -0
- package/src/index/stack-workspace-setup.js +117 -0
- package/src/index/stack.js +272 -0
- package/src/index/utils.js +85 -0
- package/src/index/verification.js +77 -0
- package/src/index/workspace-setup.js +30 -0
- package/src/index.js +97 -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 +66 -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,34 @@
|
|
|
1
|
+
import { gitContext } from './git.js';
|
|
2
|
+
import { missingStackResources } from './stack-preflight.js';
|
|
3
|
+
import { readStack } from './stack.js';
|
|
4
|
+
import { uniqueSorted } from './utils.js';
|
|
5
|
+
|
|
6
|
+
/** Read the Stack's launch declaration without choosing or starting a runtime. */
|
|
7
|
+
export async function inspectProjectLaunch({
|
|
8
|
+
environment = process.env,
|
|
9
|
+
projectRoot,
|
|
10
|
+
} = {}) {
|
|
11
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
12
|
+
const stack = await readStack(root);
|
|
13
|
+
const diagnostics = missingStackResources({ environment, resources: stack.resources });
|
|
14
|
+
const disabledReason = diagnostics.length === 0
|
|
15
|
+
? null
|
|
16
|
+
: diagnostics.map(({ message }) => message).join(' ');
|
|
17
|
+
const targets = stack.launchTargets.map((target) => ({
|
|
18
|
+
...target,
|
|
19
|
+
available: diagnostics.length === 0,
|
|
20
|
+
disabledReason,
|
|
21
|
+
}));
|
|
22
|
+
let status = 'ready';
|
|
23
|
+
if (targets.length === 0) status = 'unconfigured';
|
|
24
|
+
else if (diagnostics.length > 0) status = 'blocked';
|
|
25
|
+
return {
|
|
26
|
+
status,
|
|
27
|
+
stackHash: stack.identityHash,
|
|
28
|
+
components: stack.components.map(({ id }) => id),
|
|
29
|
+
runtimeRequirements: uniqueSorted(targets.flatMap((target) => target.runtimeRequirements)),
|
|
30
|
+
resources: stack.resources,
|
|
31
|
+
targets,
|
|
32
|
+
diagnostics,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const BLUEPRINT_PATH = 'genesis/blueprint.md';
|
|
2
|
+
export const STACK_PATH = 'genesis/stack.md';
|
|
3
|
+
export const PROGRAM_ROOT = 'genesis/program';
|
|
4
|
+
export const VERIFICATION_PATH = '.genesis/verification.json';
|
|
5
|
+
|
|
6
|
+
export function isProjectContentPath(file) {
|
|
7
|
+
const first = file.split('/')[0];
|
|
8
|
+
if (file === '.agents/skills' || file.startsWith('.agents/skills/')) return false;
|
|
9
|
+
return !['.codex', '.genesis', 'genesis'].includes(first);
|
|
10
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
import { GenesisError } from './errors.js';
|
|
5
|
+
|
|
6
|
+
const MAX_DIAGNOSTIC_OUTPUT = 16_384;
|
|
7
|
+
const executeFile = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
function boundedDiagnosticOutput(buffer) {
|
|
10
|
+
const source = buffer.toString('utf8');
|
|
11
|
+
if (source.length <= MAX_DIAGNOSTIC_OUTPUT) return source;
|
|
12
|
+
const marker = '\n... diagnostic output omitted ...\n';
|
|
13
|
+
const available = MAX_DIAGNOSTIC_OUTPUT - marker.length;
|
|
14
|
+
const head = Math.floor(available / 2);
|
|
15
|
+
return `${source.slice(0, head)}${marker}${source.slice(-(available - head))}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cleanGitEnvironment(overrides = {}) {
|
|
19
|
+
const environment = {};
|
|
20
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
21
|
+
if (!key.startsWith('GIT_')) environment[key] = value;
|
|
22
|
+
}
|
|
23
|
+
return { ...environment, ...overrides };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function runProcess(command, args, {
|
|
27
|
+
cwd,
|
|
28
|
+
env = process.env,
|
|
29
|
+
maxBytes = 32 * 1024 * 1024,
|
|
30
|
+
code = 'PROCESS_EXEC_FAILED',
|
|
31
|
+
} = {}) {
|
|
32
|
+
try {
|
|
33
|
+
const { stdout, stderr } = await executeFile(command, args, {
|
|
34
|
+
cwd,
|
|
35
|
+
env,
|
|
36
|
+
encoding: 'buffer',
|
|
37
|
+
maxBuffer: maxBytes,
|
|
38
|
+
shell: false,
|
|
39
|
+
windowsHide: true,
|
|
40
|
+
});
|
|
41
|
+
return { status: 0, signal: null, stdout, stderr };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const stdout = Buffer.isBuffer(error.stdout) ? error.stdout : Buffer.from(error.stdout || '');
|
|
44
|
+
const stderr = Buffer.isBuffer(error.stderr) ? error.stderr : Buffer.from(error.stderr || '');
|
|
45
|
+
throw new GenesisError(code, `${command} failed: ${error.message}`, {
|
|
46
|
+
command,
|
|
47
|
+
args,
|
|
48
|
+
status: typeof error.code === 'number' ? error.code : null,
|
|
49
|
+
signal: error.signal || null,
|
|
50
|
+
cause: typeof error.code === 'string' ? error.code : undefined,
|
|
51
|
+
stdout: boundedDiagnosticOutput(stdout),
|
|
52
|
+
stderr: boundedDiagnosticOutput(stderr),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function runGit(cwd, args, options = {}) {
|
|
58
|
+
return runProcess('git', [
|
|
59
|
+
'-c', 'core.hooksPath=/dev/null',
|
|
60
|
+
'-c', 'commit.gpgSign=false',
|
|
61
|
+
'-c', 'core.fsmonitor=false',
|
|
62
|
+
...args,
|
|
63
|
+
], {
|
|
64
|
+
cwd,
|
|
65
|
+
env: cleanGitEnvironment({
|
|
66
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
67
|
+
GIT_CONFIG_GLOBAL: process.platform === 'win32' ? 'NUL' : '/dev/null',
|
|
68
|
+
...options.env,
|
|
69
|
+
}),
|
|
70
|
+
maxBytes: options.maxBytes,
|
|
71
|
+
code: options.code || 'GIT_EXEC_FAILED',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function runGitText(cwd, args, options = {}) {
|
|
76
|
+
const result = await runGit(cwd, args, options);
|
|
77
|
+
return result.stdout.toString('utf8').trimEnd();
|
|
78
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { GenesisError } from './errors.js';
|
|
5
|
+
import { isProjectContentPath, PROGRAM_ROOT } from './paths.js';
|
|
6
|
+
import { normalizeRelative } from './utils.js';
|
|
7
|
+
|
|
8
|
+
const SOURCE_LINE = /^- `([^`]+)`$/u;
|
|
9
|
+
const CONCEPT_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
10
|
+
|
|
11
|
+
async function markdownFiles(projectRoot) {
|
|
12
|
+
const root = path.join(projectRoot, PROGRAM_ROOT);
|
|
13
|
+
const files = [];
|
|
14
|
+
let found = true;
|
|
15
|
+
|
|
16
|
+
async function visit(directory, relative = '') {
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (['ENOENT', 'ENOTDIR'].includes(error?.code) && directory === root) {
|
|
22
|
+
found = false;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
28
|
+
const child = path.join(directory, entry.name);
|
|
29
|
+
const childRelative = relative ? `${relative}/${entry.name}` : entry.name;
|
|
30
|
+
if (entry.isDirectory()) await visit(child, childRelative);
|
|
31
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) files.push(`${PROGRAM_ROOT}/${childRelative}`);
|
|
32
|
+
else {
|
|
33
|
+
throw new GenesisError(
|
|
34
|
+
'PROGRAM_INVALID',
|
|
35
|
+
`Program may contain only directories and Markdown modules: ${PROGRAM_ROOT}/${childRelative}.`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await visit(root);
|
|
42
|
+
return { files, found };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function sourceExists(projectRoot, sourcePath) {
|
|
46
|
+
try {
|
|
47
|
+
return (await stat(path.join(projectRoot, sourcePath))).isFile();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (['ENOENT', 'ENOTDIR'].includes(error?.code)) return false;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function moduleIdentity(programPath) {
|
|
55
|
+
const relative = programPath.slice(`${PROGRAM_ROOT}/`.length, -3);
|
|
56
|
+
const normalized = normalizeRelative(relative);
|
|
57
|
+
const segments = normalized.split('/');
|
|
58
|
+
if (segments.length < 2 || segments.some((segment) => !CONCEPT_NAME.test(segment))) {
|
|
59
|
+
throw new GenesisError(
|
|
60
|
+
'PROGRAM_INVALID',
|
|
61
|
+
`Program module needs lowercase conceptual subsystem and operation names: ${programPath}.`,
|
|
62
|
+
{ path: programPath },
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
name: segments.at(-1),
|
|
67
|
+
subsystem: segments.slice(0, -1).join('/'),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function moduleContents(source, programPath) {
|
|
72
|
+
const lines = source.replace(/\r\n?/gu, '\n').split('\n');
|
|
73
|
+
const title = lines.find((line) => /^#\s+\S/u.test(line))?.replace(/^#\s+/u, '').trim() || '';
|
|
74
|
+
const headings = lines
|
|
75
|
+
.map((line, index) => ({ line, index }))
|
|
76
|
+
.filter(({ line }) => /^##\s+/u.test(line));
|
|
77
|
+
const sourcesHeading = headings.filter(({ line }) => line === '## Sources');
|
|
78
|
+
const contractHeading = headings.filter(({ line }) => line === '## Public contract');
|
|
79
|
+
if (
|
|
80
|
+
sourcesHeading.length !== 1
|
|
81
|
+
|| contractHeading.length !== 1
|
|
82
|
+
|| sourcesHeading[0].index >= contractHeading[0].index
|
|
83
|
+
) {
|
|
84
|
+
throw new GenesisError(
|
|
85
|
+
'PROGRAM_INVALID',
|
|
86
|
+
`Program module needs one Sources section before one Public contract: ${programPath}.`,
|
|
87
|
+
{ path: programPath },
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const sourceLines = lines
|
|
91
|
+
.slice(sourcesHeading[0].index + 1, contractHeading[0].index)
|
|
92
|
+
.filter((line) => line.trim());
|
|
93
|
+
const sources = sourceLines.map((line) => line.match(SOURCE_LINE)?.[1]);
|
|
94
|
+
const nextHeading = headings.find(({ index }) => index > contractHeading[0].index);
|
|
95
|
+
const contractLines = lines
|
|
96
|
+
.slice(contractHeading[0].index + 1, nextHeading?.index ?? lines.length)
|
|
97
|
+
.filter((line) => line.trim());
|
|
98
|
+
if (
|
|
99
|
+
sources.length === 0
|
|
100
|
+
|| sources.some((sourcePath) => sourcePath === undefined)
|
|
101
|
+
|| new Set(sources).size !== sources.length
|
|
102
|
+
|| contractLines.length === 0
|
|
103
|
+
) {
|
|
104
|
+
throw new GenesisError(
|
|
105
|
+
'PROGRAM_INVALID',
|
|
106
|
+
`Program module needs distinct backticked source bullets and a non-empty public contract: ${programPath}.`,
|
|
107
|
+
{ path: programPath },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const implementationHeading = headings.filter(({ line }) => line === '## Implementation map');
|
|
111
|
+
if (implementationHeading.length > 1) {
|
|
112
|
+
throw new GenesisError(
|
|
113
|
+
'PROGRAM_INVALID',
|
|
114
|
+
`Program module has duplicate Implementation maps: ${programPath}.`,
|
|
115
|
+
{ path: programPath },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const description = lines
|
|
119
|
+
.slice(lines.findIndex((line) => /^#\s+\S/u.test(line)) + 1, sourcesHeading[0].index)
|
|
120
|
+
.join('\n')
|
|
121
|
+
.trim();
|
|
122
|
+
let implementationMap = '';
|
|
123
|
+
if (implementationHeading.length === 1) {
|
|
124
|
+
const heading = implementationHeading[0];
|
|
125
|
+
const following = headings.find(({ index }) => index > heading.index);
|
|
126
|
+
implementationMap = lines.slice(heading.index + 1, following?.index ?? lines.length).join('\n').trim();
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
title,
|
|
130
|
+
description,
|
|
131
|
+
sources: sources.map((sourcePath) => normalizeRelative(sourcePath)),
|
|
132
|
+
publicContract: contractLines.join('\n').trim(),
|
|
133
|
+
implementationMap,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function inspectProgram(projectRoot) {
|
|
138
|
+
const { files, found } = await markdownFiles(projectRoot);
|
|
139
|
+
if (!found || files.length === 0) {
|
|
140
|
+
return { status: 'missing', files: [], modules: [], subsystems: [] };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const modules = [];
|
|
144
|
+
for (const programPath of files) {
|
|
145
|
+
const source = await readFile(path.join(projectRoot, programPath), 'utf8');
|
|
146
|
+
if ((source.match(/^#\s+\S.*$/gmu) || []).length !== 1) {
|
|
147
|
+
throw new GenesisError('PROGRAM_INVALID', `Program module has an invalid explanatory format: ${programPath}.`, {
|
|
148
|
+
path: programPath,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const identity = moduleIdentity(programPath);
|
|
152
|
+
const contents = moduleContents(source, programPath);
|
|
153
|
+
const { sources } = contents;
|
|
154
|
+
for (const sourcePath of sources) {
|
|
155
|
+
if (!sourcePath || !isProjectContentPath(sourcePath) || !await sourceExists(projectRoot, sourcePath)) {
|
|
156
|
+
throw new GenesisError(
|
|
157
|
+
'PROGRAM_SOURCE_MISSING',
|
|
158
|
+
`Program module cites a missing or ineligible source file: ${sourcePath}.`,
|
|
159
|
+
{ path: programPath, sourcePath },
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
modules.push({
|
|
164
|
+
path: programPath,
|
|
165
|
+
name: identity.name,
|
|
166
|
+
sources,
|
|
167
|
+
subsystem: identity.subsystem,
|
|
168
|
+
title: contents.title,
|
|
169
|
+
description: contents.description,
|
|
170
|
+
publicContract: contents.publicContract,
|
|
171
|
+
implementationMap: contents.implementationMap,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
status: 'valid',
|
|
177
|
+
files,
|
|
178
|
+
modules,
|
|
179
|
+
subsystems: [...new Set(modules.map(({ subsystem }) => subsystem))].sort(),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { runGit } from './process.js';
|
|
4
|
+
import { pathState } from './utils.js';
|
|
5
|
+
|
|
6
|
+
async function visiblePaths(projectRoot) {
|
|
7
|
+
const [visible, deleted] = await Promise.all([
|
|
8
|
+
runGit(projectRoot, ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.']),
|
|
9
|
+
runGit(projectRoot, ['ls-files', '-z', '--deleted', '--', '.']),
|
|
10
|
+
]);
|
|
11
|
+
const absent = new Set(deleted.stdout.toString('utf8').split('\0').filter(Boolean));
|
|
12
|
+
return [...new Set(visible.stdout.toString('utf8').split('\0').filter(Boolean))]
|
|
13
|
+
.filter((file) => !absent.has(file) && file !== '.git' && !file.startsWith('.git/'))
|
|
14
|
+
.sort();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Hashable states for ordinary Git-visible files, excluding ignored output. */
|
|
18
|
+
export async function gitVisibleFileStates(projectRoot, { includePath = () => true } = {}) {
|
|
19
|
+
const states = new Map();
|
|
20
|
+
for (const file of (await visiblePaths(projectRoot)).filter(includePath)) {
|
|
21
|
+
states.set(file, await pathState(path.join(projectRoot, file)));
|
|
22
|
+
}
|
|
23
|
+
return states;
|
|
24
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { isProjectContentPath, VERIFICATION_PATH } from './paths.js';
|
|
5
|
+
import { gitVisibleFileStates } from './project-files.js';
|
|
6
|
+
import { sha256, stableJson, writeFileAtomic } from './utils.js';
|
|
7
|
+
|
|
8
|
+
const SCHEMA_VERSION = 1;
|
|
9
|
+
const HASH = /^sha256:[0-9a-f]{64}$/u;
|
|
10
|
+
|
|
11
|
+
function commandRecords(stack) {
|
|
12
|
+
return stack.commands.map(({ label, argv }) => ({ label, argv }));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function codebaseHash(projectRoot) {
|
|
16
|
+
const states = await gitVisibleFileStates(projectRoot, { includePath: isProjectContentPath });
|
|
17
|
+
return sha256(stableJson([...states].map(([file, state]) => ({
|
|
18
|
+
path: file,
|
|
19
|
+
hash: state.hash,
|
|
20
|
+
mode: state.mode,
|
|
21
|
+
}))));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readEvidence(projectRoot) {
|
|
25
|
+
try {
|
|
26
|
+
const value = JSON.parse(await readFile(path.join(projectRoot, VERIFICATION_PATH), 'utf8'));
|
|
27
|
+
const exactFields = value && typeof value === 'object'
|
|
28
|
+
&& Object.keys(value).sort().join(',') === 'codeHash,commands,schemaVersion,stackHash';
|
|
29
|
+
const validCommands = Array.isArray(value?.commands) && value.commands.every((command) => (
|
|
30
|
+
command
|
|
31
|
+
&& typeof command === 'object'
|
|
32
|
+
&& Object.keys(command).sort().join(',') === 'argv,label'
|
|
33
|
+
&& typeof command.label === 'string'
|
|
34
|
+
&& command.label.length > 0
|
|
35
|
+
&& Array.isArray(command.argv)
|
|
36
|
+
&& command.argv.length > 0
|
|
37
|
+
&& command.argv.every((argument) => typeof argument === 'string' && argument.length > 0)
|
|
38
|
+
));
|
|
39
|
+
if (
|
|
40
|
+
!exactFields
|
|
41
|
+
|| value.schemaVersion !== SCHEMA_VERSION
|
|
42
|
+
|| !HASH.test(value.codeHash)
|
|
43
|
+
|| !HASH.test(value.stackHash)
|
|
44
|
+
|| !validCommands
|
|
45
|
+
) return { status: 'invalid', evidence: null };
|
|
46
|
+
return { status: 'recorded', evidence: value };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (['ENOENT', 'ENOTDIR'].includes(error?.code)) return { status: 'missing', evidence: null };
|
|
49
|
+
if (error instanceof SyntaxError) return { status: 'invalid', evidence: null };
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function inspectVerification({ projectRoot, stack }) {
|
|
55
|
+
if (stack.commands.length === 0) return { status: 'unconfigured', evidence: null };
|
|
56
|
+
const saved = await readEvidence(projectRoot);
|
|
57
|
+
if (saved.status !== 'recorded') return saved;
|
|
58
|
+
const expected = {
|
|
59
|
+
codeHash: await codebaseHash(projectRoot),
|
|
60
|
+
stackHash: stack.identityHash,
|
|
61
|
+
commands: commandRecords(stack),
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
status: stableJson({
|
|
65
|
+
codeHash: saved.evidence.codeHash,
|
|
66
|
+
stackHash: saved.evidence.stackHash,
|
|
67
|
+
commands: saved.evidence.commands,
|
|
68
|
+
}) === stableJson(expected) ? 'current' : 'stale',
|
|
69
|
+
evidence: saved.evidence,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function clearVerification(projectRoot) {
|
|
74
|
+
await rm(path.join(projectRoot, VERIFICATION_PATH), { force: true });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function writeVerification({ projectRoot, stack }) {
|
|
78
|
+
const location = path.join(projectRoot, VERIFICATION_PATH);
|
|
79
|
+
const evidence = {
|
|
80
|
+
schemaVersion: SCHEMA_VERSION,
|
|
81
|
+
codeHash: await codebaseHash(projectRoot),
|
|
82
|
+
stackHash: stack.identityHash,
|
|
83
|
+
commands: commandRecords(stack),
|
|
84
|
+
};
|
|
85
|
+
await writeFileAtomic(location, stableJson(evidence));
|
|
86
|
+
return evidence;
|
|
87
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { readInstalledAsset } from './assets.js';
|
|
2
|
+
import { inspectProjectSkills, renderAgentSkillCatalog } from './agent-skills.js';
|
|
3
|
+
import { buildProjectIndex, MACHINE_CITY_PATH, PROGRAM_CITY_PATH } from './code-index.js';
|
|
4
|
+
import { BLUEPRINT_SKELETON_SOURCE, readBlueprint } from './blueprint.js';
|
|
5
|
+
import { readStack, stackPromptContext } from './stack.js';
|
|
6
|
+
import { GenesisError } from './errors.js';
|
|
7
|
+
import { gitContext } from './git.js';
|
|
8
|
+
import { inspectProgram } from './program.js';
|
|
9
|
+
import { inspectVerification } from './project-state.js';
|
|
10
|
+
import { missingStackResources } from './stack-preflight.js';
|
|
11
|
+
import { stableJson } from './utils.js';
|
|
12
|
+
|
|
13
|
+
const TASKS = new Set(['work', 'deslop', 'program', 'blueprint', 'describe', 'review']);
|
|
14
|
+
const DEFAULT_REQUEST = {
|
|
15
|
+
work: 'Implement the product intent expressed by the current Blueprint.',
|
|
16
|
+
deslop: 'Simplify the current Git-visible work without making unrelated changes.',
|
|
17
|
+
program: 'Refresh the complete useful Program for the code that exists now.',
|
|
18
|
+
describe: 'Create or refresh the complete Blueprint and useful Program for the codebase that exists now.',
|
|
19
|
+
review: 'Review the complete useful relationship between Blueprint, code, Program, and tests.',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function requestText(value, task) {
|
|
23
|
+
const request = String(value ?? '').trim();
|
|
24
|
+
if (Buffer.byteLength(request, 'utf8') > 64 * 1024) {
|
|
25
|
+
throw new GenesisError('PROMPT_REQUEST_INVALID', 'Prompt request exceeds 64 KiB.');
|
|
26
|
+
}
|
|
27
|
+
if (task === 'blueprint' && !request) {
|
|
28
|
+
throw new GenesisError('PROMPT_REQUEST_REQUIRED', 'Blueprint prompt generation requires explicit user intent.');
|
|
29
|
+
}
|
|
30
|
+
return request || DEFAULT_REQUEST[task];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function observeProgram(projectRoot) {
|
|
34
|
+
try {
|
|
35
|
+
return await inspectProgram(projectRoot);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
status: 'invalid',
|
|
39
|
+
files: [],
|
|
40
|
+
modules: [],
|
|
41
|
+
subsystems: [],
|
|
42
|
+
diagnostic: { code: error.code || 'PROGRAM_INVALID', message: error.message },
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function programContext(program) {
|
|
48
|
+
return {
|
|
49
|
+
status: program.status,
|
|
50
|
+
files: program.files,
|
|
51
|
+
subsystems: program.subsystems,
|
|
52
|
+
modules: program.modules.map(({ path, name, sources, subsystem }) => ({
|
|
53
|
+
path,
|
|
54
|
+
name,
|
|
55
|
+
sources,
|
|
56
|
+
subsystem,
|
|
57
|
+
})),
|
|
58
|
+
...(program.diagnostic ? { diagnostic: program.diagnostic } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function codeIndexContext(index) {
|
|
63
|
+
return {
|
|
64
|
+
machineCity: {
|
|
65
|
+
path: MACHINE_CITY_PATH,
|
|
66
|
+
status: index.machine.status,
|
|
67
|
+
files: index.fileCount,
|
|
68
|
+
functions: index.functionCount,
|
|
69
|
+
},
|
|
70
|
+
programCity: {
|
|
71
|
+
path: PROGRAM_CITY_PATH,
|
|
72
|
+
status: index.program.status,
|
|
73
|
+
operations: index.operationCount,
|
|
74
|
+
},
|
|
75
|
+
query: 'genesis index <function-or-path...>',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function renderPrompt({
|
|
80
|
+
instructions,
|
|
81
|
+
request,
|
|
82
|
+
context,
|
|
83
|
+
skills = '',
|
|
84
|
+
guidance = '',
|
|
85
|
+
cleanup = '',
|
|
86
|
+
}) {
|
|
87
|
+
return [
|
|
88
|
+
instructions.trim(),
|
|
89
|
+
'',
|
|
90
|
+
'USER REQUEST',
|
|
91
|
+
'',
|
|
92
|
+
request,
|
|
93
|
+
'',
|
|
94
|
+
'GENESIS CONTEXT',
|
|
95
|
+
'',
|
|
96
|
+
'```json',
|
|
97
|
+
stableJson(context).trimEnd(),
|
|
98
|
+
'```',
|
|
99
|
+
...(guidance ? ['', 'SELECTED STACK GUIDANCE', '', guidance] : []),
|
|
100
|
+
...(skills ? ['', 'AVAILABLE AGENT SKILLS', '', skills] : []),
|
|
101
|
+
...(cleanup ? ['', 'SELECTED STACK CLEANUP GUIDANCE', '', cleanup] : []),
|
|
102
|
+
'',
|
|
103
|
+
].join('\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function generateExplanationPrompt({ instructions, program, request, root, task }) {
|
|
107
|
+
const blueprint = await readBlueprint(root, {
|
|
108
|
+
required: true,
|
|
109
|
+
requireDescription: task === 'program',
|
|
110
|
+
});
|
|
111
|
+
const [stack, index] = await Promise.all([
|
|
112
|
+
readStack(root),
|
|
113
|
+
buildProjectIndex({ projectRoot: root, write: false }),
|
|
114
|
+
]);
|
|
115
|
+
const projectSkills = await inspectProjectSkills({ projectRoot: root, stack });
|
|
116
|
+
const blueprintContext = task === 'describe'
|
|
117
|
+
? { path: blueprint.path, source: blueprint.source }
|
|
118
|
+
: { path: blueprint.path, description: blueprint.description };
|
|
119
|
+
return {
|
|
120
|
+
status: 'ready',
|
|
121
|
+
task,
|
|
122
|
+
prompt: renderPrompt({
|
|
123
|
+
instructions,
|
|
124
|
+
request,
|
|
125
|
+
context: {
|
|
126
|
+
task,
|
|
127
|
+
projectRoot: root,
|
|
128
|
+
blueprint: blueprintContext,
|
|
129
|
+
stack: stackPromptContext(stack),
|
|
130
|
+
program: programContext(program),
|
|
131
|
+
codeIndex: codeIndexContext(index),
|
|
132
|
+
},
|
|
133
|
+
guidance: stack.guidance,
|
|
134
|
+
skills: renderAgentSkillCatalog(projectSkills.skills),
|
|
135
|
+
}),
|
|
136
|
+
warnings: [
|
|
137
|
+
...(program.diagnostic ? [program.diagnostic] : []),
|
|
138
|
+
...projectSkills.diagnostics,
|
|
139
|
+
...index.diagnostics,
|
|
140
|
+
],
|
|
141
|
+
verificationCommands: [],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function generateProjectPrompt({
|
|
146
|
+
environment = process.env,
|
|
147
|
+
projectRoot,
|
|
148
|
+
request = '',
|
|
149
|
+
task = 'work',
|
|
150
|
+
} = {}) {
|
|
151
|
+
if (!TASKS.has(task)) {
|
|
152
|
+
throw new GenesisError('PROMPT_TASK_INVALID', `Prompt task must be one of: ${[...TASKS].join(', ')}.`);
|
|
153
|
+
}
|
|
154
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
155
|
+
const userRequest = requestText(request, task);
|
|
156
|
+
const instructions = await readInstalledAsset(task);
|
|
157
|
+
|
|
158
|
+
if (task === 'blueprint') {
|
|
159
|
+
const blueprint = await readBlueprint(root);
|
|
160
|
+
const projectSkills = await inspectProjectSkills({ projectRoot: root, stack: null });
|
|
161
|
+
const prompt = renderPrompt({
|
|
162
|
+
instructions,
|
|
163
|
+
request: userRequest,
|
|
164
|
+
context: {
|
|
165
|
+
task,
|
|
166
|
+
projectRoot: root,
|
|
167
|
+
blueprint: {
|
|
168
|
+
path: 'genesis/blueprint.md',
|
|
169
|
+
source: blueprint?.source || BLUEPRINT_SKELETON_SOURCE,
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
skills: renderAgentSkillCatalog(projectSkills.skills),
|
|
173
|
+
});
|
|
174
|
+
return {
|
|
175
|
+
status: 'ready',
|
|
176
|
+
task,
|
|
177
|
+
prompt,
|
|
178
|
+
warnings: projectSkills.diagnostics,
|
|
179
|
+
verificationCommands: [],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const program = await observeProgram(root);
|
|
184
|
+
if (['describe', 'program'].includes(task)) {
|
|
185
|
+
return generateExplanationPrompt({
|
|
186
|
+
instructions,
|
|
187
|
+
program,
|
|
188
|
+
request: userRequest,
|
|
189
|
+
root,
|
|
190
|
+
task,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const blueprint = await readBlueprint(root, { required: true, requireDescription: true });
|
|
195
|
+
const stack = await readStack(root);
|
|
196
|
+
const [index, projectSkills] = await Promise.all([
|
|
197
|
+
buildProjectIndex({ projectRoot: root, write: false }),
|
|
198
|
+
inspectProjectSkills({ projectRoot: root, stack }),
|
|
199
|
+
]);
|
|
200
|
+
const missing = missingStackResources({ environment, resources: stack.resources });
|
|
201
|
+
const warnings = [
|
|
202
|
+
...(program.diagnostic ? [program.diagnostic] : []),
|
|
203
|
+
...missing,
|
|
204
|
+
...projectSkills.diagnostics,
|
|
205
|
+
...index.diagnostics,
|
|
206
|
+
];
|
|
207
|
+
const context = {
|
|
208
|
+
task,
|
|
209
|
+
projectRoot: root,
|
|
210
|
+
blueprint: { path: blueprint.path, description: blueprint.description },
|
|
211
|
+
stack: stackPromptContext(stack),
|
|
212
|
+
program: programContext(program),
|
|
213
|
+
resourceInputs: missing.length > 0
|
|
214
|
+
? { status: 'missing', diagnostics: missing }
|
|
215
|
+
: { status: 'present' },
|
|
216
|
+
verificationCommands: stack.commands.map(({ label, argv }) => ({ label, argv })),
|
|
217
|
+
agentSkills: { status: projectSkills.status },
|
|
218
|
+
codeIndex: codeIndexContext(index),
|
|
219
|
+
};
|
|
220
|
+
if (task === 'review') {
|
|
221
|
+
const verification = await inspectVerification({ projectRoot: root, stack });
|
|
222
|
+
context.verificationEvidence = { status: verification.status };
|
|
223
|
+
}
|
|
224
|
+
const prompt = renderPrompt({
|
|
225
|
+
instructions,
|
|
226
|
+
request: userRequest,
|
|
227
|
+
context,
|
|
228
|
+
guidance: stack.guidance,
|
|
229
|
+
skills: renderAgentSkillCatalog(projectSkills.skills),
|
|
230
|
+
cleanup: task === 'deslop' ? stack.deslop : '',
|
|
231
|
+
});
|
|
232
|
+
return {
|
|
233
|
+
status: 'ready',
|
|
234
|
+
task,
|
|
235
|
+
prompt,
|
|
236
|
+
warnings,
|
|
237
|
+
verificationCommands: context.verificationCommands,
|
|
238
|
+
};
|
|
239
|
+
}
|