genesis-compiler 1.2.28 → 1.3.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.
@@ -0,0 +1,371 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { GENESIS_CONTRACTS } from './contracts.js';
5
+ import { GenesisError } from './errors.js';
6
+ import { gitContext } from './git.js';
7
+ import { COLLABORATION_PATH } from './paths.js';
8
+ import { normalizeSource, writeFileAtomic } from './utils.js';
9
+
10
+ const MAX_REQUIREMENTS_BYTES = 64 * 1024;
11
+
12
+ const DIMENSIONS = {
13
+ tone: {
14
+ encouraging: {
15
+ name: 'Encouraging',
16
+ guidance: 'Be warm, encouraging, and enthusiastic without empty praise.',
17
+ },
18
+ playful: {
19
+ name: 'Playful and cheeky',
20
+ guidance: 'Be warm, cheeky, and lightly funny when the situation allows it.',
21
+ },
22
+ direct: {
23
+ name: 'Direct',
24
+ guidance: 'Be direct, calm, and matter-of-fact.',
25
+ },
26
+ military: {
27
+ name: 'Crisp and military',
28
+ guidance: 'Use crisp, disciplined, command-style language while remaining respectful.',
29
+ },
30
+ },
31
+ responseLength: {
32
+ very_short: {
33
+ name: 'Very short',
34
+ guidance: 'Use very short sentences and the fewest words that still answer clearly.',
35
+ },
36
+ concise: {
37
+ name: 'Concise',
38
+ guidance: 'Keep responses concise while including the information needed to act.',
39
+ },
40
+ balanced: {
41
+ name: 'Balanced',
42
+ guidance: 'Use a balanced amount of detail.',
43
+ },
44
+ detailed: {
45
+ name: 'Detailed',
46
+ guidance: 'Give thorough, structured explanations when they help.',
47
+ },
48
+ },
49
+ experience: {
50
+ beginner: {
51
+ name: 'Beginner',
52
+ guidance: 'Assume the user is new to software development; explain necessary terms in plain language.',
53
+ },
54
+ comfortable: {
55
+ name: 'Comfortable',
56
+ guidance: 'Assume the user is comfortable with ordinary software concepts; explain only unfamiliar or consequential details.',
57
+ },
58
+ expert: {
59
+ name: 'Expert',
60
+ guidance: 'Assume the user is an expert; use precise technical language and omit introductory explanations.',
61
+ },
62
+ },
63
+ explanationStyle: {
64
+ conclusions: {
65
+ name: 'Conclusions only',
66
+ guidance: 'Lead with conclusions and omit rationale unless the user asks for it or it is needed for safety.',
67
+ },
68
+ concise: {
69
+ name: 'Concise rationale',
70
+ guidance: 'Give a brief, useful rationale for decisions and recommendations.',
71
+ },
72
+ teaching: {
73
+ name: 'Teaching detail',
74
+ guidance: 'Explain the practical reasoning behind decisions in a teaching-oriented way.',
75
+ },
76
+ },
77
+ };
78
+
79
+ const SECTION_FIELDS = Object.freeze({
80
+ 'Tone': 'tone',
81
+ 'Response length': 'responseLength',
82
+ 'Assumed experience': 'experience',
83
+ 'Explanation style': 'explanationStyle',
84
+ });
85
+
86
+ export const DEFAULT_COLLABORATION = Object.freeze({
87
+ tone: 'encouraging',
88
+ responseLength: 'concise',
89
+ experience: 'comfortable',
90
+ explanationStyle: 'concise',
91
+ requirements: '',
92
+ });
93
+
94
+ export const COLLABORATION_SKELETON_SOURCE = `# Collaboration approach
95
+
96
+ ## Tone
97
+
98
+ - \`${DEFAULT_COLLABORATION.tone}\`
99
+
100
+ ## Response length
101
+
102
+ - \`${DEFAULT_COLLABORATION.responseLength}\`
103
+
104
+ ## Assumed experience
105
+
106
+ - \`${DEFAULT_COLLABORATION.experience}\`
107
+
108
+ ## Explanation style
109
+
110
+ - \`${DEFAULT_COLLABORATION.explanationStyle}\`
111
+
112
+ ## Project requirements
113
+
114
+ - Nothing.
115
+ `;
116
+
117
+ function sections(source) {
118
+ const normalized = normalizeSource(source);
119
+ const title = '# Collaboration approach';
120
+ if (normalized.split('\n').filter((line) => line.trim() === title).length !== 1) {
121
+ throw new GenesisError(
122
+ 'COLLABORATION_INVALID',
123
+ `${COLLABORATION_PATH} needs exactly one \`${title}\` title.`,
124
+ { path: COLLABORATION_PATH },
125
+ );
126
+ }
127
+ const result = new Map();
128
+ let current = null;
129
+ for (const line of normalized.split('\n')) {
130
+ const heading = line.match(/^##\s+(.+?)\s*$/u);
131
+ if (heading) {
132
+ if (result.has(heading[1])) {
133
+ throw new GenesisError(
134
+ 'COLLABORATION_INVALID',
135
+ `Duplicate collaboration section: ${heading[1]}.`,
136
+ { path: COLLABORATION_PATH },
137
+ );
138
+ }
139
+ current = [];
140
+ result.set(heading[1], current);
141
+ } else if (current) {
142
+ current.push(line);
143
+ } else if (line.trim() && line.trim() !== title) {
144
+ throw new GenesisError(
145
+ 'COLLABORATION_INVALID',
146
+ `${COLLABORATION_PATH} contains content outside a collaboration section.`,
147
+ { path: COLLABORATION_PATH },
148
+ );
149
+ }
150
+ }
151
+ return { normalized, sections: result };
152
+ }
153
+
154
+ function sectionText(all, name) {
155
+ return (all.get(name) || []).join('\n').trim();
156
+ }
157
+
158
+ function dimensionValue(all, sectionName, field) {
159
+ const lines = (all.get(sectionName) || []).filter((line) => line.trim());
160
+ const match = lines.length === 1 ? lines[0].trim().match(/^- `([^`]+)`$/u) : null;
161
+ const value = match?.[1] || '';
162
+ if (!Object.hasOwn(DIMENSIONS[field], value)) {
163
+ throw new GenesisError(
164
+ 'COLLABORATION_INVALID',
165
+ `${sectionName} must be exactly one backticked choice: ${Object.keys(DIMENSIONS[field]).join(', ')}.`,
166
+ { path: COLLABORATION_PATH },
167
+ );
168
+ }
169
+ return value;
170
+ }
171
+
172
+ function normalizedRequirements(value) {
173
+ const requirements = normalizeSource(value).trim();
174
+ if (!requirements || requirements === '- Nothing.') return '';
175
+ if (Buffer.byteLength(requirements, 'utf8') > MAX_REQUIREMENTS_BYTES) {
176
+ throw new GenesisError(
177
+ 'COLLABORATION_INVALID',
178
+ 'Project collaboration requirements exceed 64 KiB.',
179
+ { path: COLLABORATION_PATH },
180
+ );
181
+ }
182
+ if (/^#{1,6}(?:\s|$)/mu.test(requirements)) {
183
+ throw new GenesisError(
184
+ 'COLLABORATION_INVALID',
185
+ 'Project collaboration requirements cannot contain Markdown headings.',
186
+ { path: COLLABORATION_PATH },
187
+ );
188
+ }
189
+ return requirements;
190
+ }
191
+
192
+ function collaborationCatalog() {
193
+ return Object.fromEntries(Object.entries(DIMENSIONS).map(([field, choices]) => [
194
+ field,
195
+ Object.entries(choices).map(([id, choice]) => ({ id, ...choice })),
196
+ ]));
197
+ }
198
+
199
+ function collaborationGuidance(value) {
200
+ return [
201
+ '## Tone',
202
+ '',
203
+ DIMENSIONS.tone[value.tone].guidance,
204
+ '',
205
+ '## Response length',
206
+ '',
207
+ DIMENSIONS.responseLength[value.responseLength].guidance,
208
+ '',
209
+ '## Assumed experience',
210
+ '',
211
+ DIMENSIONS.experience[value.experience].guidance,
212
+ '',
213
+ '## Explanation style',
214
+ '',
215
+ DIMENSIONS.explanationStyle[value.explanationStyle].guidance,
216
+ ...(value.requirements ? [
217
+ '',
218
+ '## Project requirements',
219
+ '',
220
+ 'Additional project-specific collaboration requirements:',
221
+ value.requirements,
222
+ ] : []),
223
+ ].join('\n');
224
+ }
225
+
226
+ export function parseCollaborationSource(source) {
227
+ const parsed = sections(source);
228
+ const expected = new Set([...Object.keys(SECTION_FIELDS), 'Project requirements']);
229
+ const unknown = [...parsed.sections.keys()].filter((name) => !expected.has(name));
230
+ const missing = [...expected].filter((name) => !parsed.sections.has(name));
231
+ if (unknown.length > 0 || missing.length > 0) {
232
+ throw new GenesisError(
233
+ 'COLLABORATION_INVALID',
234
+ `${COLLABORATION_PATH} needs only Tone, Response length, Assumed experience, Explanation style, and Project requirements sections.`,
235
+ { path: COLLABORATION_PATH },
236
+ );
237
+ }
238
+ const values = Object.fromEntries(Object.entries(SECTION_FIELDS).map(([sectionName, field]) => [
239
+ field,
240
+ dimensionValue(parsed.sections, sectionName, field),
241
+ ]));
242
+ const requirementsSource = sectionText(parsed.sections, 'Project requirements');
243
+ if (!requirementsSource) {
244
+ throw new GenesisError(
245
+ 'COLLABORATION_INVALID',
246
+ 'Project requirements must contain requirements or `- Nothing.`.',
247
+ { path: COLLABORATION_PATH },
248
+ );
249
+ }
250
+ return {
251
+ path: COLLABORATION_PATH,
252
+ ...values,
253
+ requirements: normalizedRequirements(requirementsSource),
254
+ source: parsed.normalized,
255
+ };
256
+ }
257
+
258
+ function collaborationResult(parsed, status = 'configured') {
259
+ return {
260
+ contract: GENESIS_CONTRACTS.collaboration,
261
+ status,
262
+ ...parsed,
263
+ choices: collaborationCatalog(),
264
+ guidance: collaborationGuidance(parsed),
265
+ };
266
+ }
267
+
268
+ export function defaultCollaboration() {
269
+ return collaborationResult(
270
+ parseCollaborationSource(COLLABORATION_SKELETON_SOURCE),
271
+ 'defaulted',
272
+ );
273
+ }
274
+
275
+ function renderCollaboration(value) {
276
+ return [
277
+ '# Collaboration approach',
278
+ '',
279
+ '## Tone',
280
+ '',
281
+ `- \`${value.tone}\``,
282
+ '',
283
+ '## Response length',
284
+ '',
285
+ `- \`${value.responseLength}\``,
286
+ '',
287
+ '## Assumed experience',
288
+ '',
289
+ `- \`${value.experience}\``,
290
+ '',
291
+ '## Explanation style',
292
+ '',
293
+ `- \`${value.explanationStyle}\``,
294
+ '',
295
+ '## Project requirements',
296
+ '',
297
+ value.requirements || '- Nothing.',
298
+ '',
299
+ ].join('\n');
300
+ }
301
+
302
+ export async function readCollaboration(projectRoot) {
303
+ let parsed;
304
+ try {
305
+ parsed = parseCollaborationSource(
306
+ await readFile(path.join(projectRoot, COLLABORATION_PATH), 'utf8'),
307
+ );
308
+ } catch (error) {
309
+ if (!['ENOENT', 'ENOTDIR'].includes(error?.code)) throw error;
310
+ throw new GenesisError(
311
+ 'COLLABORATION_REQUIRED',
312
+ `${COLLABORATION_PATH} is required for the current Genesis project format.`,
313
+ { path: COLLABORATION_PATH },
314
+ );
315
+ }
316
+ return collaborationResult(parsed);
317
+ }
318
+
319
+ export async function materializeDefaultCollaboration({ projectRoot } = {}) {
320
+ const location = path.join(projectRoot, COLLABORATION_PATH);
321
+ try {
322
+ await readCollaboration(projectRoot);
323
+ return { changedFiles: [] };
324
+ } catch (error) {
325
+ if (error?.code !== 'COLLABORATION_REQUIRED') throw error;
326
+ }
327
+ await writeFileAtomic(location, COLLABORATION_SKELETON_SOURCE);
328
+ return { changedFiles: [COLLABORATION_PATH] };
329
+ }
330
+
331
+ export async function inspectProjectCollaboration({ projectRoot } = {}) {
332
+ const root = (await gitContext(projectRoot)).repositoryRoot;
333
+ return readCollaboration(root);
334
+ }
335
+
336
+ export async function setProjectCollaboration({
337
+ experience,
338
+ explanationStyle,
339
+ projectRoot,
340
+ requirements,
341
+ responseLength,
342
+ tone,
343
+ } = {}) {
344
+ const root = (await gitContext(projectRoot)).repositoryRoot;
345
+ const current = await readCollaboration(root);
346
+ const next = parseCollaborationSource(renderCollaboration({
347
+ tone: tone ?? current.tone,
348
+ responseLength: responseLength ?? current.responseLength,
349
+ experience: experience ?? current.experience,
350
+ explanationStyle: explanationStyle ?? current.explanationStyle,
351
+ requirements: requirements ?? current.requirements,
352
+ }));
353
+ const rendered = renderCollaboration(next);
354
+ const changed = current.source !== rendered;
355
+ if (changed) await writeFileAtomic(path.join(root, COLLABORATION_PATH), rendered);
356
+ return {
357
+ contract: GENESIS_CONTRACTS.collaboration,
358
+ status: changed ? 'updated' : 'unchanged',
359
+ summary: changed
360
+ ? 'Updated the project collaboration approach.'
361
+ : 'The project collaboration approach is already selected.',
362
+ tone: next.tone,
363
+ responseLength: next.responseLength,
364
+ experience: next.experience,
365
+ explanationStyle: next.explanationStyle,
366
+ requirements: next.requirements,
367
+ choices: collaborationCatalog(),
368
+ guidance: collaborationGuidance(next),
369
+ changedFiles: changed ? [COLLABORATION_PATH] : [],
370
+ };
371
+ }
@@ -31,18 +31,13 @@ function stackSummary(stack) {
31
31
  }
32
32
 
33
33
  function stackGuidance(stack) {
34
- const pieces = stack.components.filter(({ guidance }) => guidance);
35
- if (pieces.length === 0) return [];
34
+ if (!stack.guidance) return [];
36
35
  return [
37
36
  '',
38
37
  '## Selected Stack guidance',
39
38
  '',
40
- ...pieces.flatMap(({ id, guidance }) => [
41
- `### ${id}`,
42
- '',
43
- guidance,
44
- '',
45
- ]),
39
+ stack.guidance,
40
+ '',
46
41
  ];
47
42
  }
48
43
 
@@ -1,7 +1,10 @@
1
1
  export const GENESIS_CONTRACTS = Object.freeze({
2
+ collaboration: 'genesis.collaboration.v1',
2
3
  derivedArtifacts: 'genesis.derived-artifacts.v1',
3
4
  engineering: 'genesis.engineering.v1',
4
5
  environment: 'genesis.environment.v2',
6
+ sessionContext: 'genesis.session-context.v1',
5
7
  stackSection: 'genesis.stack-section.v1',
8
+ turnContext: 'genesis.turn-context.v1',
6
9
  verification: 'genesis.verification.v1',
7
10
  });
@@ -0,0 +1,73 @@
1
+ import { execFile } from 'node:child_process';
2
+
3
+ import { GenesisError } from './errors.js';
4
+
5
+ export const HOST_CONTEXT_RESOLVER_ENV = 'GENESIS_HOST_CONTEXT_RESOLVER';
6
+ export const HOST_CONTEXT_RESOLVER_DATA_ENV = 'GENESIS_HOST_CONTEXT_RESOLVER_DATA';
7
+ export const HOST_CONTEXT_INPUT_ENV = 'GENESIS_HOST_CONTEXT_INPUT';
8
+ export const SESSION_CONTEXT_INSTALLED_ENV = 'GENESIS_SESSION_CONTEXT_INSTALLED';
9
+
10
+ const RESOLVER_OUTPUT_MAX_BYTES = 64 * 1024;
11
+ const RESOLVER_TIMEOUT_MS = 5_000;
12
+
13
+ function resolverError(message) {
14
+ return new GenesisError('HOST_CONTEXT_RESOLVER_FAILED', message);
15
+ }
16
+
17
+ function configuredResolver(environment) {
18
+ const command = String(environment[HOST_CONTEXT_RESOLVER_ENV] || '').trim();
19
+ return command || null;
20
+ }
21
+
22
+ function resolverData(environment) {
23
+ const source = String(environment[HOST_CONTEXT_RESOLVER_DATA_ENV] || '').trim();
24
+ if (!source) return null;
25
+ try {
26
+ return JSON.parse(source);
27
+ } catch {
28
+ throw resolverError(`${HOST_CONTEXT_RESOLVER_DATA_ENV} must contain JSON.`);
29
+ }
30
+ }
31
+
32
+ function runResolver({ command, input, projectRoot, environment }) {
33
+ const source = `${JSON.stringify(input)}\n`;
34
+ return new Promise((resolve, reject) => {
35
+ const child = execFile(command, [], {
36
+ cwd: projectRoot,
37
+ encoding: 'utf8',
38
+ env: environment,
39
+ maxBuffer: RESOLVER_OUTPUT_MAX_BYTES,
40
+ timeout: RESOLVER_TIMEOUT_MS,
41
+ windowsHide: true,
42
+ }, (error, stdout) => {
43
+ if (error) {
44
+ reject(resolverError(`The configured host context resolver failed: ${error.message}`));
45
+ return;
46
+ }
47
+ resolve(stdout);
48
+ });
49
+ child.stdin.on('error', () => {});
50
+ child.stdin.end(source);
51
+ });
52
+ }
53
+
54
+ export async function resolveConfiguredHostContext({
55
+ environment = process.env,
56
+ projectRoot,
57
+ providerSessionId = null,
58
+ scope = 'session',
59
+ } = {}) {
60
+ const command = configuredResolver(environment);
61
+ if (!command) return null;
62
+ const output = await runResolver({
63
+ command,
64
+ environment,
65
+ projectRoot,
66
+ input: {
67
+ scope,
68
+ providerSessionId: providerSessionId || null,
69
+ data: resolverData(environment),
70
+ },
71
+ });
72
+ return output;
73
+ }
package/src/index/init.js CHANGED
@@ -4,11 +4,13 @@ import path from 'node:path';
4
4
  import { syncProjectSkills } from './agent-skills.js';
5
5
  import { BLUEPRINT_SKELETON_SOURCE } from './blueprint.js';
6
6
  import { installCodexHooks } from './codex-hooks.js';
7
+ import { COLLABORATION_SKELETON_SOURCE } from './collaboration.js';
7
8
  import { ENGINEERING_SKELETON_SOURCE } from './engineering.js';
8
9
  import { gitContext } from './git.js';
9
10
  import { installOpenCodePlugin } from './opencode-plugin.js';
10
11
  import {
11
12
  BLUEPRINT_PATH,
13
+ COLLABORATION_PATH,
12
14
  ENGINEERING_PATH,
13
15
  PROGRAM_ROOT,
14
16
  PROJECT_VERSION_PATH,
@@ -39,6 +41,7 @@ export async function initializeProject({ projectRoot, stackPackages = [] } = {}
39
41
  requireCurrentProjectFormat(projectFormat, { allowUninitialized: true });
40
42
  const created = (await Promise.all([
41
43
  createIfMissing(root, BLUEPRINT_PATH, BLUEPRINT_SKELETON_SOURCE),
44
+ createIfMissing(root, COLLABORATION_PATH, COLLABORATION_SKELETON_SOURCE),
42
45
  createIfMissing(root, ENGINEERING_PATH, ENGINEERING_SKELETON_SOURCE),
43
46
  createIfMissing(root, STACK_PATH, EMPTY_STACK_SOURCE),
44
47
  ])).filter(Boolean);
@@ -73,6 +76,7 @@ export async function initializeProject({ projectRoot, stackPackages = [] } = {}
73
76
  'OpenCode loads the project guidance plugin automatically.',
74
77
  'Genesis workflow skills are available in .agents/skills/.',
75
78
  'Describe product intent in genesis/blueprint.md.',
79
+ 'Choose the project collaboration approach with genesis collaboration set.',
76
80
  'Choose the project engineering approach with genesis engineering set <profile>.',
77
81
  'Stack components are optional; add them with genesis stack add <piece...>.',
78
82
  ].join(' '),
@@ -1,4 +1,5 @@
1
1
  import { readBlueprint } from './blueprint.js';
2
+ import { materializeDefaultCollaboration } from './collaboration.js';
2
3
  import { readEngineering } from './engineering.js';
3
4
  import { asDiagnostic, GenesisError } from './errors.js';
4
5
  import { gitContext } from './git.js';
@@ -43,6 +44,7 @@ async function materializeProjectContracts({ projectRoot, stackPackages }) {
43
44
  const MIGRATIONS = new Map([
44
45
  [0, validateLegacyProject],
45
46
  [1, materializeProjectContracts],
47
+ [2, materializeDefaultCollaboration],
46
48
  ]);
47
49
 
48
50
  export async function migrateProject({ projectRoot, stackPackages = [] } = {}) {
@@ -2,9 +2,9 @@ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { gitContext } from './git.js';
5
+ import { OPENCODE_PLUGIN_PATH } from './paths.js';
5
6
  import { writeFileAtomic } from './utils.js';
6
7
 
7
- const OPENCODE_PLUGIN_PATH = '.opencode/plugins/genesis-project-guidance.js';
8
8
  const OPENCODE_PLUGIN_SOURCE = new URL('../../plugins/opencode/project-guidance.js', import.meta.url);
9
9
 
10
10
  async function existingSource(location) {
@@ -1,9 +1,11 @@
1
1
  export const BLUEPRINT_PATH = 'genesis/blueprint.md';
2
+ export const COLLABORATION_PATH = 'genesis/collaboration.md';
2
3
  export const ENGINEERING_PATH = 'genesis/engineering.md';
3
4
  export const STACK_PATH = 'genesis/stack.md';
4
5
  export const PROGRAM_ROOT = 'genesis/program';
5
6
  export const PROJECT_VERSION_PATH = 'genesis/version';
6
7
  export const VERIFICATION_PATH = '.genesis/verification.json';
8
+ export const OPENCODE_PLUGIN_PATH = '.opencode/plugins/genesis-project-guidance.js';
7
9
 
8
10
  export function isProjectContentPath(file) {
9
11
  const first = file.split('/')[0];
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
 
3
+ import { isProjectContentPath, OPENCODE_PLUGIN_PATH } from './paths.js';
3
4
  import { runGit } from './process.js';
4
5
  import { pathState } from './utils.js';
5
6
 
@@ -22,3 +23,14 @@ export async function gitVisibleFileStates(projectRoot, { includePath = () => tr
22
23
  }
23
24
  return states;
24
25
  }
26
+
27
+ /** Classifies opening behavior from cheap Git-visible paths and selected Stack state. */
28
+ export async function classifyProjectKind({ projectRoot, stackComponents = [] } = {}) {
29
+ if (stackComponents.length > 0) return 'existing';
30
+ const existing = (await visiblePaths(projectRoot)).some((file) => (
31
+ isProjectContentPath(file)
32
+ && file !== OPENCODE_PLUGIN_PATH
33
+ && !file.split('/').includes('node_modules')
34
+ ));
35
+ return existing ? 'existing' : 'new';
36
+ }
@@ -5,6 +5,7 @@ import { asDiagnostic, GenesisError } from './errors.js';
5
5
  import { gitContext } from './git.js';
6
6
  import {
7
7
  BLUEPRINT_PATH,
8
+ COLLABORATION_PATH,
8
9
  ENGINEERING_PATH,
9
10
  PROGRAM_ROOT,
10
11
  PROJECT_VERSION_PATH,
@@ -12,7 +13,7 @@ import {
12
13
  } from './paths.js';
13
14
  import { normalizeSource, pathState, writeFileAtomic } from './utils.js';
14
15
 
15
- export const CURRENT_PROJECT_FORMAT_VERSION = 2;
16
+ export const CURRENT_PROJECT_FORMAT_VERSION = 3;
16
17
  export const CURRENT_PROJECT_FORMAT_SOURCE = `${CURRENT_PROJECT_FORMAT_VERSION}\n`;
17
18
 
18
19
  const VERSION_PATTERN = /^(0|[1-9][0-9]*)\n?$/u;
@@ -45,6 +46,7 @@ export function parseProjectFormatVersion(source) {
45
46
  async function hasGenesisSource(projectRoot) {
46
47
  const states = await Promise.all([
47
48
  BLUEPRINT_PATH,
49
+ COLLABORATION_PATH,
48
50
  ENGINEERING_PATH,
49
51
  STACK_PATH,
50
52
  PROGRAM_ROOT,