genesis-compiler 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -20
- package/docs/assurance-model.md +16 -1
- package/docs/preview-identity-command.md +113 -0
- package/docs/prompt-integration.md +22 -14
- package/docs/stack-components.md +112 -22
- package/package.json +4 -2
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/prompts/adopt.txt +62 -0
- package/prompts/start.txt +17 -4
- package/prompts/work.txt +1 -1
- package/skills/genesis-project/SKILL.md +9 -1
- package/src/cli.js +76 -3
- package/src/index/agent-skills.js +25 -4
- package/src/index/assets.js +1 -0
- package/src/index/city-presentation.js +83 -0
- package/src/index/code-index.js +24 -3
- package/src/index/codex-hooks.js +3 -2
- package/src/index/init.js +1 -1
- package/src/index/prompt.js +58 -1
- package/src/index/stack-catalog.js +3 -0
- package/src/index/stack-city-presentation.js +178 -0
- package/src/index/stack-environment-defaults.js +2 -0
- package/src/index/stack-launch.js +5 -6
- package/src/index/stack-piece.js +35 -11
- package/src/index/stack.js +47 -2
- package/src/index/utils.js +1 -1
- package/src/index/workspace-setup.js +76 -0
- package/src/index.js +10 -3
- package/stacks/pieces/cpp.md +12 -0
- package/stacks/pieces/jskit-mysql.md +10 -0
- package/stacks/pieces/jskit-postgresql.md +10 -0
- package/stacks/pieces/jskit.md +34 -0
- package/stacks/pieces/mysql.md +11 -0
- package/stacks/pieces/nodejs.md +11 -0
- package/stacks/pieces/postgresql.md +12 -0
- package/stacks/pieces/vue.md +9 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { GenesisError } from './errors.js';
|
|
2
|
+
|
|
3
|
+
const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
4
|
+
const IGNORE_LINE = /^- Ignore `([^`]+)`[ \t]*$/u;
|
|
5
|
+
const MATCH_LINE = /^- Match `([^`]+)` as `([^`]+)`: `([^`]+)\/\*\*`[ \t]*$/u;
|
|
6
|
+
const FALLBACK_LINE = /^- Fallback `([^`]+)` as `([^`]+)`[ \t]*$/u;
|
|
7
|
+
const REGEXP_SPECIAL = /[.+^${}()|[\]\\]/u;
|
|
8
|
+
const compiledGlobPatterns = new Map();
|
|
9
|
+
|
|
10
|
+
function invalid(piecePath, message) {
|
|
11
|
+
throw new GenesisError('STACK_PIECE_INVALID', message, { path: piecePath });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizedPrefix(value, piecePath) {
|
|
15
|
+
const prefix = String(value || '').trim();
|
|
16
|
+
if (
|
|
17
|
+
!prefix
|
|
18
|
+
|| prefix.startsWith('/')
|
|
19
|
+
|| prefix.includes('\\')
|
|
20
|
+
|| /[*?\[\]{}!]/u.test(prefix)
|
|
21
|
+
|| prefix.split('/').some((segment) => !segment || ['.', '..'].includes(segment))
|
|
22
|
+
) {
|
|
23
|
+
invalid(piecePath, '## City regions match paths must be canonical project-relative prefixes.');
|
|
24
|
+
}
|
|
25
|
+
return prefix;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizedGlob(value, piecePath) {
|
|
29
|
+
const pattern = String(value || '').trim();
|
|
30
|
+
if (
|
|
31
|
+
!pattern
|
|
32
|
+
|| pattern.startsWith('/')
|
|
33
|
+
|| pattern.includes('\\')
|
|
34
|
+
|| pattern.includes('***')
|
|
35
|
+
|| /[?\[\]{}!]/u.test(pattern)
|
|
36
|
+
|| pattern.split('/').some((segment) => !segment || ['.', '..'].includes(segment))
|
|
37
|
+
) {
|
|
38
|
+
invalid(
|
|
39
|
+
piecePath,
|
|
40
|
+
'## City regions ignore patterns must be canonical project-relative globs using only * and ** wildcards.',
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return pattern;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function regionIdentity(id, title, piecePath) {
|
|
47
|
+
if (!ID_PATTERN.test(id)) {
|
|
48
|
+
invalid(piecePath, '## City regions ids must use lowercase letters, digits, and single hyphens.');
|
|
49
|
+
}
|
|
50
|
+
if (!title.trim()) invalid(piecePath, '## City regions titles must not be empty.');
|
|
51
|
+
return { id, title: title.trim() };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function globExpression(pattern) {
|
|
55
|
+
if (compiledGlobPatterns.has(pattern)) return compiledGlobPatterns.get(pattern);
|
|
56
|
+
let expression = '^';
|
|
57
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
58
|
+
const character = pattern[index];
|
|
59
|
+
if (character === '*' && pattern[index + 1] === '*') {
|
|
60
|
+
if (pattern[index + 2] === '/') {
|
|
61
|
+
expression += '(?:[^/]+/)*';
|
|
62
|
+
index += 2;
|
|
63
|
+
} else {
|
|
64
|
+
expression += '.*';
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
} else if (character === '*') {
|
|
68
|
+
expression += '[^/]*';
|
|
69
|
+
} else {
|
|
70
|
+
expression += REGEXP_SPECIAL.test(character) ? `\\${character}` : character;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const compiled = new RegExp(`${expression}$`, 'u');
|
|
74
|
+
compiledGlobPatterns.set(pattern, compiled);
|
|
75
|
+
return compiled;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function cityPathExcluded(filePath, exclusions = []) {
|
|
79
|
+
return exclusions.some((pattern) => globExpression(pattern).test(filePath));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A City presentation declaration is one complete, ordered contract. Ignore
|
|
84
|
+
* globs remove non-product files before Machine City is emitted. Matching
|
|
85
|
+
* prefixes are explicit Stack facts; one final fallback owns every remaining
|
|
86
|
+
* path. Genesis resolves both region and campus placement, so renderers never
|
|
87
|
+
* classify or filter repository paths.
|
|
88
|
+
*/
|
|
89
|
+
export function parseStackCityPresentationLines(lines, { path = 'stack piece' } = {}) {
|
|
90
|
+
if (lines === undefined) return { exclusions: [], regions: [] };
|
|
91
|
+
const content = lines.filter((line) => line.trim());
|
|
92
|
+
if (content.length === 0 || (content.length === 1 && content[0].trim() === '- Nothing.')) {
|
|
93
|
+
return { exclusions: [], regions: [] };
|
|
94
|
+
}
|
|
95
|
+
const exclusions = [];
|
|
96
|
+
const regions = [];
|
|
97
|
+
let regionDeclarationStarted = false;
|
|
98
|
+
content.forEach((line, index) => {
|
|
99
|
+
const ignore = line.match(IGNORE_LINE);
|
|
100
|
+
if (ignore) {
|
|
101
|
+
if (regionDeclarationStarted) {
|
|
102
|
+
invalid(path, '## City regions Ignore entries must precede Match and Fallback entries.');
|
|
103
|
+
}
|
|
104
|
+
exclusions.push(normalizedGlob(ignore[1], path));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
regionDeclarationStarted = true;
|
|
108
|
+
const match = line.match(MATCH_LINE);
|
|
109
|
+
if (match) {
|
|
110
|
+
const identity = regionIdentity(match[1], match[2], path);
|
|
111
|
+
regions.push({
|
|
112
|
+
...identity,
|
|
113
|
+
fallback: false,
|
|
114
|
+
pathPrefix: normalizedPrefix(match[3], path),
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const fallback = line.match(FALLBACK_LINE);
|
|
119
|
+
if (fallback) {
|
|
120
|
+
if (index !== content.length - 1) {
|
|
121
|
+
invalid(path, '## City regions fallback must be the final declaration.');
|
|
122
|
+
}
|
|
123
|
+
regions.push({
|
|
124
|
+
...regionIdentity(fallback[1], fallback[2], path),
|
|
125
|
+
fallback: true,
|
|
126
|
+
pathPrefix: null,
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
invalid(
|
|
131
|
+
path,
|
|
132
|
+
'## City regions entries must use Ignore with a glob, Match with an id, title, and path/**, or one final Fallback.',
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
if (new Set(exclusions).size !== exclusions.length) {
|
|
136
|
+
invalid(path, '## City regions contains a duplicate ignore pattern.');
|
|
137
|
+
}
|
|
138
|
+
if (new Set(regions.map(({ id }) => id)).size !== regions.length) {
|
|
139
|
+
invalid(path, '## City regions contains a duplicate region id.');
|
|
140
|
+
}
|
|
141
|
+
const matches = regions.filter(({ fallback }) => !fallback);
|
|
142
|
+
if (new Set(matches.map(({ pathPrefix }) => pathPrefix)).size !== matches.length) {
|
|
143
|
+
invalid(path, '## City regions contains a duplicate path prefix.');
|
|
144
|
+
}
|
|
145
|
+
if (regions.filter(({ fallback }) => fallback).length !== 1) {
|
|
146
|
+
invalid(path, '## City regions requires exactly one final fallback.');
|
|
147
|
+
}
|
|
148
|
+
for (const [index, left] of matches.entries()) {
|
|
149
|
+
if (matches.slice(index + 1).some((right) => (
|
|
150
|
+
left.pathPrefix.startsWith(`${right.pathPrefix}/`)
|
|
151
|
+
|| right.pathPrefix.startsWith(`${left.pathPrefix}/`)
|
|
152
|
+
))) {
|
|
153
|
+
invalid(path, '## City regions match prefixes must not overlap.');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { exclusions, regions };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function composeStackCityPresentation(components = []) {
|
|
160
|
+
const owners = components.filter((component) => (
|
|
161
|
+
component.cityRegions.length > 0 || component.cityExclusions.length > 0
|
|
162
|
+
));
|
|
163
|
+
if (owners.length === 0) return { exclusions: [], regions: [] };
|
|
164
|
+
if (owners.length > 1) {
|
|
165
|
+
throw new GenesisError(
|
|
166
|
+
'STACK_CITY_PRESENTATION_AMBIGUOUS',
|
|
167
|
+
`Stack components ${owners.map(({ id }) => id).join(', ')} declare competing City presentation contracts.`,
|
|
168
|
+
{ components: owners.map(({ id }) => id) },
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
exclusions: [...owners[0].cityExclusions],
|
|
173
|
+
regions: owners[0].cityRegions.map((region) => ({
|
|
174
|
+
...region,
|
|
175
|
+
component: owners[0].id,
|
|
176
|
+
})),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -14,6 +14,8 @@ export function parseStackEnvironmentDefaultLines(lines, {
|
|
|
14
14
|
path: defaultsPath = 'stack environment defaults',
|
|
15
15
|
} = {}) {
|
|
16
16
|
if (lines === undefined) return [];
|
|
17
|
+
const present = lines.map((line) => line.trim()).filter(Boolean);
|
|
18
|
+
if (present.length === 1 && present[0] === '- Nothing.') return [];
|
|
17
19
|
const defaults = [];
|
|
18
20
|
for (let index = 0; index < lines.length; index += 1) {
|
|
19
21
|
const source = lines[index].trim();
|
|
@@ -7,9 +7,8 @@ import {
|
|
|
7
7
|
|
|
8
8
|
const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
9
9
|
const PLACEHOLDER = /\{([a-z][a-z0-9-]*)\}/gu;
|
|
10
|
-
const PREVIEW_IDENTITY_COMMAND_PATH = /^\.vibe64\/bin\/[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
11
10
|
const PREVIEW_IDENTITY_ENVIRONMENT_NAME = /^[A-Z_][A-Z0-9_]*$/u;
|
|
12
|
-
const PREVIEW_IDENTITY_PROTOCOL = '
|
|
11
|
+
const PREVIEW_IDENTITY_PROTOCOL = 'genesis.preview-identity.command.v1';
|
|
13
12
|
const PREVIEW_IDENTITY_RESERVED_ENVIRONMENT_NAMES = new Set([
|
|
14
13
|
'HOME',
|
|
15
14
|
'LOGNAME',
|
|
@@ -17,8 +16,6 @@ const PREVIEW_IDENTITY_RESERVED_ENVIRONMENT_NAMES = new Set([
|
|
|
17
16
|
'PATH',
|
|
18
17
|
'TMPDIR',
|
|
19
18
|
'USER',
|
|
20
|
-
'VIBE64_PREVIEW_IDENTITY_ENABLED',
|
|
21
|
-
'VIBE64_PREVIEW_IDENTITY_SECRET',
|
|
22
19
|
]);
|
|
23
20
|
const PREVIEW_IDENTITY_TYPES = new Set(['email', 'login', 'user-id']);
|
|
24
21
|
const PREVIEW_FIELDS = [
|
|
@@ -291,12 +288,14 @@ function normalizePreviewIdentity(draft, launchPath, targetId) {
|
|
|
291
288
|
if (
|
|
292
289
|
command.length > 64
|
|
293
290
|
|| command.some((entry) => !entry.trim() || entry.length > 4096 || /[\r\n]/u.test(entry))
|
|
294
|
-
|| !
|
|
291
|
+
|| !command[0].includes('/')
|
|
292
|
+
|| command[0].includes('\\')
|
|
293
|
+
|| command[0].split('/').some((part) => !part || ['.', '..'].includes(part))
|
|
295
294
|
|| command.some((entry) => /\{(?:host|port)\}/u.test(entry))
|
|
296
295
|
) {
|
|
297
296
|
invalid(
|
|
298
297
|
launchPath,
|
|
299
|
-
'Preview identity command must use
|
|
298
|
+
'Preview identity command must use a committed app-owned project-relative executable and contain at most 64 literal arguments.',
|
|
300
299
|
draft.line,
|
|
301
300
|
);
|
|
302
301
|
}
|
package/src/index/stack-piece.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GenesisError } from './errors.js';
|
|
2
2
|
import { parseStackCommandLines } from './stack-command.js';
|
|
3
|
+
import { parseStackCityPresentationLines } from './stack-city-presentation.js';
|
|
3
4
|
import { parseStackDeploymentLines } from './stack-deployment.js';
|
|
4
5
|
import { parseStackEnvironmentDefaultLines } from './stack-environment-defaults.js';
|
|
5
6
|
import { parseStackEnvironmentFileLines } from './stack-environment-files.js';
|
|
@@ -9,7 +10,7 @@ import { normalizeSource } from './utils.js';
|
|
|
9
10
|
|
|
10
11
|
const STACK_PIECE_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
11
12
|
const STACK_CUSTOMIZATION_TITLE = /^# Stack customization: ([a-z0-9]+(?:-[a-z0-9]+)*)\s*$/u;
|
|
12
|
-
const STACK_CUSTOMIZATION_FIELDS = new Set(['Description', 'Guidance', 'Deslop']);
|
|
13
|
+
const STACK_CUSTOMIZATION_FIELDS = new Set(['Description', 'Guidance', 'Adoption', 'Deslop']);
|
|
13
14
|
|
|
14
15
|
export function normalizeStackPieceId(value, {
|
|
15
16
|
code = 'STACK_PIECE_INVALID',
|
|
@@ -92,12 +93,12 @@ function customizationSections(source, piecePath) {
|
|
|
92
93
|
if (/^#{1,3}\s+/u.test(line)) invalid(piecePath, `Unknown customization heading: ${line.trim()}.`);
|
|
93
94
|
if (field) content.push(line);
|
|
94
95
|
else if (line.trim()) {
|
|
95
|
-
invalid(piecePath, 'Customization content must be below Description, Guidance, or Deslop.');
|
|
96
|
+
invalid(piecePath, 'Customization content must be below Description, Guidance, Adoption, or Deslop.');
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
finishField();
|
|
99
100
|
if (Object.values(result).every((values) => Object.keys(values).length === 0)) {
|
|
100
|
-
invalid(piecePath, 'Stack customization must add or override Description, Guidance, or Deslop.');
|
|
101
|
+
invalid(piecePath, 'Stack customization must add or override Description, Guidance, Adoption, or Deslop.');
|
|
101
102
|
}
|
|
102
103
|
return result;
|
|
103
104
|
}
|
|
@@ -150,18 +151,22 @@ function skill(all, piecePath) {
|
|
|
150
151
|
};
|
|
151
152
|
}
|
|
152
153
|
|
|
153
|
-
function
|
|
154
|
-
|
|
154
|
+
export function parseStackResourceLines(lines, {
|
|
155
|
+
path: resourcePath = 'stack resources',
|
|
156
|
+
} = {}) {
|
|
157
|
+
if (lines === undefined) return [];
|
|
158
|
+
const source = lines.join('\n').trim();
|
|
155
159
|
if (!source) return [];
|
|
160
|
+
if (source === '- Nothing.') return [];
|
|
156
161
|
const blocks = [...source.matchAll(/```json genesis-resource\s*\n([\s\S]*?)\n```/gu)];
|
|
157
162
|
const outside = source.replace(/```json genesis-resource\s*\n[\s\S]*?\n```/gu, '').trim();
|
|
158
163
|
if (outside || blocks.length === 0) {
|
|
159
|
-
invalid(
|
|
164
|
+
invalid(resourcePath, '## Resources may contain only `json genesis-resource` fenced objects.');
|
|
160
165
|
}
|
|
161
166
|
const parsed = blocks.map((match) => {
|
|
162
167
|
let resource;
|
|
163
168
|
try { resource = JSON.parse(match[1]); } catch (error) {
|
|
164
|
-
invalid(
|
|
169
|
+
invalid(resourcePath, `A Stack resource is invalid JSON: ${error.message}`);
|
|
165
170
|
}
|
|
166
171
|
if (
|
|
167
172
|
!resource
|
|
@@ -172,7 +177,7 @@ function resources(all, piecePath) {
|
|
|
172
177
|
|| !Array.isArray(resource.environmentAlternatives)
|
|
173
178
|
|| resource.environmentAlternatives.length === 0
|
|
174
179
|
) {
|
|
175
|
-
invalid(
|
|
180
|
+
invalid(resourcePath, 'A Stack resource needs an id, kind, and at least one environment alternative.');
|
|
176
181
|
}
|
|
177
182
|
const environmentAlternatives = resource.environmentAlternatives.map((alternative) => {
|
|
178
183
|
const required = alternative?.required;
|
|
@@ -181,7 +186,7 @@ function resources(all, piecePath) {
|
|
|
181
186
|
!Array.isArray(required)
|
|
182
187
|
|| !Array.isArray(allowEmpty)
|
|
183
188
|
) {
|
|
184
|
-
invalid(
|
|
189
|
+
invalid(resourcePath, `Stack resource ${resource.id} has an invalid environment alternative.`);
|
|
185
190
|
}
|
|
186
191
|
const names = [...required, ...allowEmpty];
|
|
187
192
|
if (
|
|
@@ -190,17 +195,23 @@ function resources(all, piecePath) {
|
|
|
190
195
|
|| allowEmpty.some((name) => !required.includes(name))
|
|
191
196
|
|| new Set(required).size !== required.length
|
|
192
197
|
|| new Set(allowEmpty).size !== allowEmpty.length
|
|
193
|
-
) invalid(
|
|
198
|
+
) invalid(resourcePath, `Stack resource ${resource.id} has an invalid environment alternative.`);
|
|
194
199
|
return { required: [...required], allowEmpty: [...allowEmpty] };
|
|
195
200
|
});
|
|
196
201
|
return { id: resource.id, kind: resource.kind, environmentAlternatives };
|
|
197
202
|
});
|
|
198
203
|
if (new Set(parsed.map(({ id }) => id)).size !== parsed.length) {
|
|
199
|
-
invalid(
|
|
204
|
+
invalid(resourcePath, '## Resources contains a duplicate resource id.');
|
|
200
205
|
}
|
|
201
206
|
return parsed;
|
|
202
207
|
}
|
|
203
208
|
|
|
209
|
+
function resources(all, piecePath) {
|
|
210
|
+
return parseStackResourceLines(all.has('Resources') ? all.get('Resources') : undefined, {
|
|
211
|
+
path: piecePath,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
204
215
|
/**
|
|
205
216
|
* Stack pieces are guidance, not a second programming language. Genesis reads
|
|
206
217
|
* only the few sections it uses and deliberately ignores explanatory sections.
|
|
@@ -217,9 +228,14 @@ export function parseStackPieceSource(value, {
|
|
|
217
228
|
const all = sections(source);
|
|
218
229
|
const description = textSection(all, 'Description');
|
|
219
230
|
const guidance = textSection(all, 'Guidance');
|
|
231
|
+
const adoption = textSection(all, 'Adoption');
|
|
220
232
|
const deslop = textSection(all, 'Deslop');
|
|
221
233
|
if (!description) invalid(piecePath, 'Stack piece needs Description.');
|
|
222
234
|
const commands = parseStackCommandLines(all.get('Commands') || [], { path: piecePath });
|
|
235
|
+
const cityPresentation = parseStackCityPresentationLines(
|
|
236
|
+
all.has('City regions') ? all.get('City regions') : undefined,
|
|
237
|
+
{ path: piecePath },
|
|
238
|
+
);
|
|
223
239
|
const environmentDefaults = parseStackEnvironmentDefaultLines(
|
|
224
240
|
all.has('Environment defaults') ? all.get('Environment defaults') : undefined,
|
|
225
241
|
{ path: piecePath },
|
|
@@ -243,12 +259,15 @@ export function parseStackPieceSource(value, {
|
|
|
243
259
|
id,
|
|
244
260
|
description,
|
|
245
261
|
guidance,
|
|
262
|
+
adoption,
|
|
246
263
|
requires: idList(all, 'Requires', piecePath, { required: true }),
|
|
247
264
|
conflicts: idList(all, 'Conflicts', piecePath),
|
|
248
265
|
indexers: idList(all, 'Indexers', piecePath),
|
|
249
266
|
skill: skill(all, piecePath),
|
|
250
267
|
resources: resources(all, piecePath),
|
|
251
268
|
commands: commands.map(({ line: _line, ...command }) => command),
|
|
269
|
+
cityExclusions: cityPresentation.exclusions,
|
|
270
|
+
cityRegions: cityPresentation.regions,
|
|
252
271
|
environmentDefaults,
|
|
253
272
|
environmentFiles: environmentFiles || [],
|
|
254
273
|
launchTargets: launch?.targets || [],
|
|
@@ -292,6 +311,11 @@ export function applyStackPieceCustomization(piece, customization) {
|
|
|
292
311
|
customization.override.guidance,
|
|
293
312
|
customization.add.guidance,
|
|
294
313
|
),
|
|
314
|
+
adoption: customizedText(
|
|
315
|
+
piece.adoption,
|
|
316
|
+
customization.override.adoption,
|
|
317
|
+
customization.add.adoption,
|
|
318
|
+
),
|
|
295
319
|
deslop: customizedText(piece.deslop, customization.override.deslop, customization.add.deslop),
|
|
296
320
|
};
|
|
297
321
|
}
|
package/src/index/stack.js
CHANGED
|
@@ -5,10 +5,12 @@ import { syncProjectSkills } from './agent-skills.js';
|
|
|
5
5
|
import { GenesisError } from './errors.js';
|
|
6
6
|
import { readBuiltinStackCatalog } from './stack-catalog.js';
|
|
7
7
|
import { parseStackCommandLines } from './stack-command.js';
|
|
8
|
+
import { composeStackCityPresentation } from './stack-city-presentation.js';
|
|
8
9
|
import { composeStackDeployment, parseStackDeploymentLines } from './stack-deployment.js';
|
|
9
10
|
import { resolveStackPieces } from './stack-composition.js';
|
|
10
11
|
import {
|
|
11
12
|
composeStackEnvironmentDefaults,
|
|
13
|
+
parseStackEnvironmentDefaultLines,
|
|
12
14
|
} from './stack-environment-defaults.js';
|
|
13
15
|
import {
|
|
14
16
|
composeStackEnvironmentFiles,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
applyStackPieceCustomization,
|
|
24
26
|
normalizeStackPieceId,
|
|
25
27
|
parseStackPieceCustomizationSource,
|
|
28
|
+
parseStackResourceLines,
|
|
26
29
|
} from './stack-piece.js';
|
|
27
30
|
import { STACK_PATH } from './paths.js';
|
|
28
31
|
import { sha256, stableJson, writeFileAtomic } from './utils.js';
|
|
@@ -31,6 +34,8 @@ const COMPONENT_LINE = /^- `([a-z0-9]+(?:-[a-z0-9]+)*)`$/u;
|
|
|
31
34
|
export const EMPTY_STACK_SOURCE = '# Stack\n\n## Components\n';
|
|
32
35
|
const STACK_SECTIONS = new Set([
|
|
33
36
|
'Components',
|
|
37
|
+
'Resources',
|
|
38
|
+
'Environment defaults',
|
|
34
39
|
'Environment files',
|
|
35
40
|
'Workspace setup',
|
|
36
41
|
'Commands',
|
|
@@ -103,8 +108,10 @@ function renderStack({
|
|
|
103
108
|
commandLines = [],
|
|
104
109
|
componentIds: componentIdsValue,
|
|
105
110
|
deploymentLines = null,
|
|
111
|
+
environmentDefaultLines = null,
|
|
106
112
|
environmentFileLines = null,
|
|
107
113
|
launchLines = null,
|
|
114
|
+
resourceLines = null,
|
|
108
115
|
workspaceSetupLines = null,
|
|
109
116
|
}) {
|
|
110
117
|
return [
|
|
@@ -112,6 +119,12 @@ function renderStack({
|
|
|
112
119
|
'',
|
|
113
120
|
'## Components',
|
|
114
121
|
...componentIdsValue.map((id) => `- \`${id}\``),
|
|
122
|
+
...(resourceLines === null
|
|
123
|
+
? []
|
|
124
|
+
: ['', '## Resources', '', ...withoutOuterBlankLines(resourceLines)]),
|
|
125
|
+
...(environmentDefaultLines === null
|
|
126
|
+
? []
|
|
127
|
+
: ['', '## Environment defaults', '', ...withoutOuterBlankLines(environmentDefaultLines)]),
|
|
115
128
|
...(environmentFileLines === null
|
|
116
129
|
? []
|
|
117
130
|
: ['', '## Environment files', '', ...withoutOuterBlankLines(environmentFileLines)]),
|
|
@@ -152,6 +165,15 @@ export async function addStackPieces({ pieces, projectRoot }) {
|
|
|
152
165
|
)).map(({ id }) => id);
|
|
153
166
|
const commandLines = parseStackCommandLines(sections.get('Commands') || [], { path: STACK_PATH })
|
|
154
167
|
.map(({ label, argv }) => `- Verify \`${label}\`: ${argv.map((value) => `\`${value}\``).join(' ')}`);
|
|
168
|
+
const resourceLines = sections.has('Resources') ? sections.get('Resources') : null;
|
|
169
|
+
parseStackResourceLines(resourceLines === null ? undefined : resourceLines, { path: STACK_PATH });
|
|
170
|
+
const environmentDefaultLines = sections.has('Environment defaults')
|
|
171
|
+
? sections.get('Environment defaults')
|
|
172
|
+
: null;
|
|
173
|
+
parseStackEnvironmentDefaultLines(
|
|
174
|
+
environmentDefaultLines === null ? undefined : environmentDefaultLines,
|
|
175
|
+
{ path: STACK_PATH },
|
|
176
|
+
);
|
|
155
177
|
const environmentFileLines = sections.has('Environment files')
|
|
156
178
|
? sections.get('Environment files')
|
|
157
179
|
: null;
|
|
@@ -177,8 +199,10 @@ export async function addStackPieces({ pieces, projectRoot }) {
|
|
|
177
199
|
commandLines,
|
|
178
200
|
componentIds: selected,
|
|
179
201
|
deploymentLines,
|
|
202
|
+
environmentDefaultLines,
|
|
180
203
|
environmentFileLines,
|
|
181
204
|
launchLines,
|
|
205
|
+
resourceLines,
|
|
182
206
|
workspaceSetupLines,
|
|
183
207
|
});
|
|
184
208
|
const stackChanged = rendered !== source;
|
|
@@ -264,8 +288,20 @@ export async function readStack(projectRoot) {
|
|
|
264
288
|
).map(({ line: _line, ...command }) => command);
|
|
265
289
|
const componentCommands = components.flatMap((piece) => piece.commands);
|
|
266
290
|
const commands = distinctCommands(projectCommands.length > 0 ? projectCommands : componentCommands);
|
|
267
|
-
const
|
|
268
|
-
const
|
|
291
|
+
const cityPresentation = composeStackCityPresentation(components);
|
|
292
|
+
const projectResources = sections.has('Resources')
|
|
293
|
+
? parseStackResourceLines(sections.get('Resources'), { path: STACK_PATH })
|
|
294
|
+
: null;
|
|
295
|
+
const resources = projectResources === null
|
|
296
|
+
? resourceDeclarations(components)
|
|
297
|
+
: projectResources.map((resource) => ({ component: 'project', resource }));
|
|
298
|
+
const projectEnvironmentDefaults = parseStackEnvironmentDefaultLines(
|
|
299
|
+
sections.has('Environment defaults') ? sections.get('Environment defaults') : undefined,
|
|
300
|
+
{ path: STACK_PATH },
|
|
301
|
+
);
|
|
302
|
+
const environmentDefaults = sections.has('Environment defaults')
|
|
303
|
+
? projectEnvironmentDefaults.map((entry) => ({ ...entry, sources: ['project'] }))
|
|
304
|
+
: composeStackEnvironmentDefaults(components);
|
|
269
305
|
const projectEnvironmentFiles = parseStackEnvironmentFileLines(
|
|
270
306
|
sections.has('Environment files') ? sections.get('Environment files') : undefined,
|
|
271
307
|
{ path: STACK_PATH },
|
|
@@ -290,6 +326,8 @@ export async function readStack(projectRoot) {
|
|
|
290
326
|
path: STACK_PATH,
|
|
291
327
|
identityHash: sha256(stableJson({
|
|
292
328
|
components: components.map(({ id }) => id),
|
|
329
|
+
cityExclusions: cityPresentation.exclusions,
|
|
330
|
+
cityRegions: cityPresentation.regions,
|
|
293
331
|
commands: commands.map(({ label, argv }) => ({ label, argv })),
|
|
294
332
|
environmentDefaults,
|
|
295
333
|
environmentFiles,
|
|
@@ -299,6 +337,8 @@ export async function readStack(projectRoot) {
|
|
|
299
337
|
workspaceSetup,
|
|
300
338
|
})),
|
|
301
339
|
components,
|
|
340
|
+
cityExclusions: cityPresentation.exclusions,
|
|
341
|
+
cityRegions: cityPresentation.regions,
|
|
302
342
|
commands,
|
|
303
343
|
environmentDefaults,
|
|
304
344
|
environmentFiles,
|
|
@@ -307,6 +347,7 @@ export async function readStack(projectRoot) {
|
|
|
307
347
|
workspaceSetup,
|
|
308
348
|
resources,
|
|
309
349
|
guidance: composedProse(components, 'guidance'),
|
|
350
|
+
adoption: composedProse(components, 'adoption'),
|
|
310
351
|
deslop: composedProse(components, 'deslop'),
|
|
311
352
|
};
|
|
312
353
|
}
|
|
@@ -317,10 +358,14 @@ export function stackPromptContext(stack) {
|
|
|
317
358
|
id: piece.id,
|
|
318
359
|
description: piece.description,
|
|
319
360
|
...(piece.guidance ? { guidance: piece.guidance } : {}),
|
|
361
|
+
...(piece.adoption ? { adoption: piece.adoption } : {}),
|
|
320
362
|
requires: piece.requires,
|
|
321
363
|
})),
|
|
364
|
+
cityExclusions: stack.cityExclusions,
|
|
365
|
+
cityRegions: stack.cityRegions,
|
|
322
366
|
verifyCommands: stack.commands.map(({ label, argv }) => ({ label, argv })),
|
|
323
367
|
environmentDefaults: stack.environmentDefaults,
|
|
368
|
+
resources: stack.resources,
|
|
324
369
|
environmentFiles: stack.environmentFiles,
|
|
325
370
|
launchTargets: stack.launchTargets,
|
|
326
371
|
deployment: stack.deployment,
|
package/src/index/utils.js
CHANGED
|
@@ -29,7 +29,7 @@ export function uniqueSorted(values = []) {
|
|
|
29
29
|
return [...new Set(values)].sort();
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export async function writeFileAtomic(filePath, source, { mode =
|
|
32
|
+
export async function writeFileAtomic(filePath, source, { mode = 0o666 } = {}) {
|
|
33
33
|
const temporary = `${filePath}.${randomUUID()}.tmp`;
|
|
34
34
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
35
35
|
try {
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { access } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
import { asDiagnostic } from './errors.js';
|
|
4
5
|
import { gitContext } from './git.js';
|
|
6
|
+
import { runProcess } from './process.js';
|
|
5
7
|
import { readStack } from './stack.js';
|
|
8
|
+
import { withStackEnvironmentDefaults } from './stack-environment-defaults.js';
|
|
6
9
|
import { sha256, stableJson, uniqueSorted } from './utils.js';
|
|
7
10
|
|
|
11
|
+
async function emit(onEvent, event) {
|
|
12
|
+
try { await onEvent?.(event); } catch { /* Progress observers do not control preparation. */ }
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
export async function inspectWorkspaceSetupForStack({ projectRoot: root, stack }) {
|
|
9
16
|
const diagnostics = [...stack.workspaceSetup.diagnostics];
|
|
10
17
|
const applicableSteps = [];
|
|
@@ -68,3 +75,72 @@ export async function inspectProjectWorkspaceSetup({
|
|
|
68
75
|
const stack = await readStack(root);
|
|
69
76
|
return inspectWorkspaceSetupForStack({ projectRoot: root, stack });
|
|
70
77
|
}
|
|
78
|
+
|
|
79
|
+
/** Execute the selected Stack's exact workspace recipe with the caller's environment. */
|
|
80
|
+
export async function prepareProjectWorkspace({
|
|
81
|
+
environment = process.env,
|
|
82
|
+
onEvent,
|
|
83
|
+
processRunner = runProcess,
|
|
84
|
+
projectRoot,
|
|
85
|
+
} = {}) {
|
|
86
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
87
|
+
const stack = await readStack(root);
|
|
88
|
+
const setup = await inspectWorkspaceSetupForStack({ projectRoot: root, stack });
|
|
89
|
+
if (setup.status !== 'ready') {
|
|
90
|
+
const summary = setup.diagnostics.map(({ message }) => message).join(' ')
|
|
91
|
+
|| 'The selected Stack declares no applicable workspace preparation commands.';
|
|
92
|
+
return {
|
|
93
|
+
...setup,
|
|
94
|
+
summary,
|
|
95
|
+
commands: [],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const resolvedEnvironment = withStackEnvironmentDefaults(
|
|
100
|
+
environment,
|
|
101
|
+
stack.environmentDefaults,
|
|
102
|
+
);
|
|
103
|
+
const commands = [];
|
|
104
|
+
try {
|
|
105
|
+
for (const step of setup.steps) {
|
|
106
|
+
await emit(onEvent, {
|
|
107
|
+
type: 'genesis.workspace-preparation',
|
|
108
|
+
code: 'WORKSPACE_PREPARATION_STARTED',
|
|
109
|
+
message: `Preparing: ${step.label}.`,
|
|
110
|
+
details: { label: step.label, argv: step.argv, workdir: step.workdir },
|
|
111
|
+
});
|
|
112
|
+
await processRunner(step.argv[0], step.argv.slice(1), {
|
|
113
|
+
cwd: path.resolve(root, step.workdir),
|
|
114
|
+
env: resolvedEnvironment,
|
|
115
|
+
maxBytes: 32 * 1024 * 1024,
|
|
116
|
+
code: 'WORKSPACE_PREPARATION_FAILED',
|
|
117
|
+
});
|
|
118
|
+
commands.push({
|
|
119
|
+
label: step.label,
|
|
120
|
+
argv: step.argv,
|
|
121
|
+
workdir: step.workdir,
|
|
122
|
+
});
|
|
123
|
+
await emit(onEvent, {
|
|
124
|
+
type: 'genesis.workspace-preparation',
|
|
125
|
+
code: 'WORKSPACE_PREPARATION_COMPLETED',
|
|
126
|
+
message: `Prepared: ${step.label}.`,
|
|
127
|
+
details: { label: step.label, argv: step.argv, workdir: step.workdir },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
...setup,
|
|
132
|
+
status: 'passed',
|
|
133
|
+
summary: `Prepared the workspace with ${commands.length} command${commands.length === 1 ? '' : 's'}.`,
|
|
134
|
+
commands,
|
|
135
|
+
diagnostics: [],
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
...setup,
|
|
140
|
+
status: 'failed',
|
|
141
|
+
summary: error.message,
|
|
142
|
+
commands,
|
|
143
|
+
diagnostics: [asDiagnostic(error)],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/index.js
CHANGED
|
@@ -12,7 +12,10 @@ import { inspectProjectLaunch } from './index/launch.js';
|
|
|
12
12
|
import { listBuiltinStackPieces } from './index/stack-catalog.js';
|
|
13
13
|
import { addStackPieces } from './index/stack.js';
|
|
14
14
|
import { verifyProject } from './index/verification.js';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
inspectProjectWorkspaceSetup,
|
|
17
|
+
prepareProjectWorkspace,
|
|
18
|
+
} from './index/workspace-setup.js';
|
|
16
19
|
|
|
17
20
|
function withIndexResult(result, index) {
|
|
18
21
|
const changedFiles = [...new Set([...result.changedFiles, ...index.changedFiles])].sort();
|
|
@@ -41,7 +44,7 @@ export async function adoptProject({ projectRoot = process.cwd(), request = '' }
|
|
|
41
44
|
const description = await generateProjectPrompt({
|
|
42
45
|
projectRoot,
|
|
43
46
|
request,
|
|
44
|
-
task: '
|
|
47
|
+
task: 'adopt',
|
|
45
48
|
});
|
|
46
49
|
return {
|
|
47
50
|
status: 'ready',
|
|
@@ -51,7 +54,7 @@ export async function adoptProject({ projectRoot = process.cwd(), request = '' }
|
|
|
51
54
|
changedFiles: initialized.changedFiles,
|
|
52
55
|
prompt: description.prompt,
|
|
53
56
|
warnings: description.warnings,
|
|
54
|
-
guidance: 'Follow the generated
|
|
57
|
+
guidance: 'Follow the generated adoption prompt now. Genesis does not start another agent.',
|
|
55
58
|
};
|
|
56
59
|
}
|
|
57
60
|
|
|
@@ -86,6 +89,10 @@ export function inspectWorkspaceSetup(options) {
|
|
|
86
89
|
return inspectProjectWorkspaceSetup(options);
|
|
87
90
|
}
|
|
88
91
|
|
|
92
|
+
export function prepareWorkspace(options) {
|
|
93
|
+
return prepareProjectWorkspace(options);
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
export function generatePrompt(options) {
|
|
90
97
|
return generateProjectPrompt(options);
|
|
91
98
|
}
|
package/stacks/pieces/cpp.md
CHANGED
|
@@ -12,6 +12,18 @@ C and C++ translation-unit, header, callable, ownership, build, and test convent
|
|
|
12
12
|
|
|
13
13
|
- `cpp`
|
|
14
14
|
|
|
15
|
+
## Adoption
|
|
16
|
+
|
|
17
|
+
- Identify the actual build system and presets, compiler and language level,
|
|
18
|
+
package/runtime dependencies, generated-source steps, and supported build
|
|
19
|
+
configurations without substituting a preferred CMake or Meson layout.
|
|
20
|
+
- Record exact configure/build/test commands and the executable argv, workdir,
|
|
21
|
+
host/port handling, HTTP readiness predicate, and signal/shutdown contract for
|
|
22
|
+
every web-service target.
|
|
23
|
+
- Preserve public headers, ABI and platform assumptions. Treat databases,
|
|
24
|
+
queues, TLS material, and external service credentials as explicit Resources,
|
|
25
|
+
not implicit properties of the native binary.
|
|
26
|
+
|
|
15
27
|
## Deslop
|
|
16
28
|
|
|
17
29
|
- Preserve public header boundaries, ABI expectations, build configuration,
|