genesis-compiler 1.10.10 → 1.10.12

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,193 @@
1
+ import process from 'node:process';
2
+
3
+ import {
4
+ addStack,
5
+ adoptProject,
6
+ applyTemplate,
7
+ check,
8
+ generatePrompt,
9
+ getContext,
10
+ indexCodebase,
11
+ initialize,
12
+ inspectCollaboration,
13
+ inspectEngineering,
14
+ inspectEnvironment,
15
+ inspectStackSection,
16
+ inspectSubsystems,
17
+ inspectProject,
18
+ inspectSkills,
19
+ installCodex,
20
+ listEngineeringProfiles,
21
+ listStackPieces,
22
+ listTemplates,
23
+ listParsers,
24
+ installParsers,
25
+ verifyParsers,
26
+ migrate,
27
+ setCollaboration,
28
+ setEngineeringProfile,
29
+ syncSkills,
30
+ verify,
31
+ } from './index.js';
32
+ import {
33
+ codexAdoptionRecommendation,
34
+ } from './index/codex-hooks.js';
35
+ import { engineeringProfile, readEngineeringBaseline } from './index/engineering.js';
36
+ import { fail } from './index/errors.js';
37
+ import { projectSessionContext } from './index/session-context.js';
38
+ import { installedFirstPartyStackPackages } from './index/stack-catalog.js';
39
+ import { readStack } from './index/stack.js';
40
+ import { withParserCancellation } from './index/code-indexers/parsers.js';
41
+
42
+ // The CLI's commands, which load every project operation. cli.js imports this module for every command except
43
+ // `genesis hook turn`, which runs on every agent turn and composes only the host's context.
44
+
45
+ async function cliStackPackages(projectRoot, supplied) {
46
+ if (supplied.length > 0) return supplied;
47
+ try {
48
+ const stack = await readStack(projectRoot);
49
+ if (stack.stackPackages.length > 0) return [];
50
+ } catch (error) {
51
+ if (error?.code !== 'STACK_REQUIRED') return [];
52
+ }
53
+ return installedFirstPartyStackPackages({ projectRoot });
54
+ }
55
+
56
+ async function execute({ command, operands, options }, { signal, hostContext = null, progress }) {
57
+ if (command === 'parsers') {
58
+ const parserOptions = {
59
+ languages: operands.slice(1), all: options.all, directory: options.directory, signal,
60
+ onProgress: progress,
61
+ };
62
+ return { list: listParsers, install: installParsers, verify: verifyParsers }[operands[0]](parserOptions);
63
+ }
64
+ const projectRoot = options.projectRoot || process.cwd();
65
+ const stackPackages = await cliStackPackages(projectRoot, options.stackPackages || []);
66
+ if (command === 'init') return initialize({ projectRoot, stackPackages });
67
+ if (command === 'templates') {
68
+ const templateOptions = { projectRoot, stackPackages, templateSources: options.templateSources };
69
+ return operands[0] === 'list' ? listTemplates(templateOptions) : applyTemplate({ ...templateOptions, templateId: operands[1] });
70
+ }
71
+ if (command === 'migrate') return migrate({ projectRoot, stackPackages });
72
+ if (command === 'skills') return syncSkills({ projectRoot, stackPackages });
73
+ if (command === 'adopt') {
74
+ return adoptProject({ projectRoot, request: operands.join(' '), stackPackages });
75
+ }
76
+ if (command === 'codex') return installCodex();
77
+ if (command === 'collaboration') {
78
+ if (operands[0] === 'show') {
79
+ return { action: 'show', ...await inspectCollaboration({ projectRoot }) };
80
+ }
81
+ return {
82
+ action: 'set',
83
+ ...await setCollaboration({
84
+ projectRoot,
85
+ tone: operands[1],
86
+ responseLength: operands[2],
87
+ experience: operands[3],
88
+ explanationStyle: operands[4],
89
+ longChecks: operands[5],
90
+ ...(operands.length > 6 ? { requirements: operands.slice(6).join(' ') } : {}),
91
+ }),
92
+ };
93
+ }
94
+ if (command === 'engineering') {
95
+ if (operands[0] === 'list') {
96
+ return { action: 'list', status: 'ok', profiles: await listEngineeringProfiles() };
97
+ }
98
+ if (operands[0] === 'show') {
99
+ if (!operands[1]) return { action: 'show', ...await inspectEngineering({ projectRoot }) };
100
+ const [baseline, profile] = await Promise.all([
101
+ readEngineeringBaseline(),
102
+ engineeringProfile(operands[1]),
103
+ ]);
104
+ return {
105
+ action: 'show',
106
+ status: 'ready',
107
+ profile,
108
+ guidance: [
109
+ '## Universal complexity gate',
110
+ '',
111
+ baseline,
112
+ '',
113
+ '## Profile guidance',
114
+ '',
115
+ profile.guidance,
116
+ ].join('\n'),
117
+ };
118
+ }
119
+ return {
120
+ action: 'set',
121
+ ...await setEngineeringProfile({ profile: operands[1], projectRoot }),
122
+ };
123
+ }
124
+ if (command === 'stack') {
125
+ if (operands[0] === 'list') {
126
+ return { status: 'ok', pieces: await listStackPieces({ projectRoot, stackPackages }) };
127
+ }
128
+ return addStack({ pieces: operands.slice(1), projectRoot, stackPackages });
129
+ }
130
+ if (command === 'prompt') {
131
+ return generatePrompt({
132
+ projectRoot,
133
+ task: options.task || 'work',
134
+ request: operands.join(' '),
135
+ stackPackages,
136
+ });
137
+ }
138
+ if (command === 'context') return getContext({ paths: operands, projectRoot, stackPackages });
139
+ if (command === 'index') return indexCodebase({ projectRoot, queries: operands, stackPackages, callWeb: options.callWeb });
140
+ if (command === 'inspect') {
141
+ if (operands[0] === 'section') {
142
+ return {
143
+ inspection: 'section',
144
+ ...await inspectStackSection({
145
+ name: operands.slice(1).join(' '),
146
+ projectRoot,
147
+ stackPackages,
148
+ }),
149
+ };
150
+ }
151
+ const inspections = { subsystems: inspectSubsystems, environment: inspectEnvironment, project: inspectProject, skills: inspectSkills };
152
+ return {
153
+ inspection: operands[0],
154
+ ...await inspections[operands[0]]({ projectRoot, stackPackages }),
155
+ };
156
+ }
157
+ if (command === 'hook') {
158
+ if (operands[0] === 'discover') {
159
+ return { kind: 'discover', ...await codexAdoptionRecommendation({ projectRoot }) };
160
+ }
161
+ return {
162
+ kind: 'session',
163
+ ...await projectSessionContext({
164
+ ...(hostContext === null ? {} : {
165
+ hostDriver: () => hostContext.text,
166
+ hostDriverInput: { scope: 'session' },
167
+ }),
168
+ projectRoot,
169
+ stackPackages,
170
+ }),
171
+ };
172
+ }
173
+ if (command === 'verify') {
174
+ return verify({
175
+ projectRoot,
176
+ signal,
177
+ stackPackages,
178
+ onEvent: (event) => {
179
+ if (!options.json) progress(event.message);
180
+ },
181
+ });
182
+ }
183
+ if (command === 'check') return check({ projectRoot, stackPackages });
184
+ fail('CLI_UNKNOWN_COMMAND', `Unknown command: ${command}`);
185
+ }
186
+
187
+ /**
188
+ * Runs a parsed CLI command other than the turn hook. `progress` writes one line of progress for the person.
189
+ * @returns {Promise<object>} The command's result.
190
+ */
191
+ export function runCommand(parsed, { signal, hostContext = null, progress }) {
192
+ return withParserCancellation(signal, () => execute(parsed, { signal, hostContext, progress }));
193
+ }
package/src/cli.js CHANGED
@@ -1,40 +1,8 @@
1
1
  import process from 'node:process';
2
2
  import { parseArgs } from 'node:util';
3
3
 
4
- import {
5
- addStack,
6
- adoptProject,
7
- applyTemplate,
8
- check,
9
- generatePrompt,
10
- getContext,
11
- indexCodebase,
12
- initialize,
13
- inspectCollaboration,
14
- inspectEngineering,
15
- inspectEnvironment,
16
- inspectStackSection,
17
- inspectSubsystems,
18
- inspectProject,
19
- inspectSkills,
20
- installCodex,
21
- listEngineeringProfiles,
22
- listStackPieces,
23
- listTemplates,
24
- listParsers,
25
- installParsers,
26
- verifyParsers,
27
- migrate,
28
- setCollaboration,
29
- setEngineeringProfile,
30
- syncSkills,
31
- verify,
32
- } from './index.js';
33
- import {
34
- codexAdoptionRecommendation,
35
- } from './index/codex-hooks.js';
36
- import { engineeringProfile, readEngineeringBaseline } from './index/engineering.js';
37
4
  import { asDiagnostic, fail } from './index/errors.js';
5
+ import { projectTurnContext } from './index/host-context.js';
38
6
  import {
39
7
  HOST_CONTEXT_INPUT_ENV,
40
8
  HOST_CONTEXT_RESOLVER_ENV,
@@ -45,10 +13,6 @@ import {
45
13
  inspectProjectFormat,
46
14
  projectFormatDiagnostic,
47
15
  } from './index/project-format.js';
48
- import { projectSessionContext, projectTurnContext } from './index/session-context.js';
49
- import { installedFirstPartyStackPackages } from './index/stack-catalog.js';
50
- import { readStack } from './index/stack.js';
51
- import { withParserCancellation } from './index/code-indexers/parsers.js';
52
16
 
53
17
  const USAGE = `Usage:
54
18
  genesis init
@@ -473,158 +437,22 @@ async function hookSessionInput(environment = process.env) {
473
437
  };
474
438
  }
475
439
 
476
- async function cliStackPackages(projectRoot, supplied) {
477
- if (supplied.length > 0) return supplied;
478
- try {
479
- const stack = await readStack(projectRoot);
480
- if (stack.stackPackages.length > 0) return [];
481
- } catch (error) {
482
- if (error?.code !== 'STACK_REQUIRED') return [];
483
- }
484
- return installedFirstPartyStackPackages({ projectRoot });
485
- }
486
-
487
- async function execute({ command, operands, options }, { signal, hostContext = null } = {}) {
488
- if (command === 'parsers') {
489
- const parserOptions = {
490
- languages: operands.slice(1), all: options.all, directory: options.directory, signal,
491
- onProgress: (message) => line(process.stderr, message),
492
- };
493
- return { list: listParsers, install: installParsers, verify: verifyParsers }[operands[0]](parserOptions);
494
- }
495
- const projectRoot = options.projectRoot || process.cwd();
496
- const stackPackages = await cliStackPackages(projectRoot, options.stackPackages || []);
497
- if (command === 'init') return initialize({ projectRoot, stackPackages });
498
- if (command === 'templates') {
499
- const templateOptions = { projectRoot, stackPackages, templateSources: options.templateSources };
500
- return operands[0] === 'list' ? listTemplates(templateOptions) : applyTemplate({ ...templateOptions, templateId: operands[1] });
501
- }
502
- if (command === 'migrate') return migrate({ projectRoot, stackPackages });
503
- if (command === 'skills') return syncSkills({ projectRoot, stackPackages });
504
- if (command === 'adopt') {
505
- return adoptProject({ projectRoot, request: operands.join(' '), stackPackages });
506
- }
507
- if (command === 'codex') return installCodex();
508
- if (command === 'collaboration') {
509
- if (operands[0] === 'show') {
510
- return { action: 'show', ...await inspectCollaboration({ projectRoot }) };
511
- }
512
- return {
513
- action: 'set',
514
- ...await setCollaboration({
515
- projectRoot,
516
- tone: operands[1],
517
- responseLength: operands[2],
518
- experience: operands[3],
519
- explanationStyle: operands[4],
520
- longChecks: operands[5],
521
- ...(operands.length > 6 ? { requirements: operands.slice(6).join(' ') } : {}),
522
- }),
523
- };
524
- }
525
- if (command === 'engineering') {
526
- if (operands[0] === 'list') {
527
- return { action: 'list', status: 'ok', profiles: await listEngineeringProfiles() };
528
- }
529
- if (operands[0] === 'show') {
530
- if (!operands[1]) return { action: 'show', ...await inspectEngineering({ projectRoot }) };
531
- const [baseline, profile] = await Promise.all([
532
- readEngineeringBaseline(),
533
- engineeringProfile(operands[1]),
534
- ]);
535
- return {
536
- action: 'show',
537
- status: 'ready',
538
- profile,
539
- guidance: [
540
- '## Universal complexity gate',
541
- '',
542
- baseline,
543
- '',
544
- '## Profile guidance',
545
- '',
546
- profile.guidance,
547
- ].join('\n'),
548
- };
549
- }
550
- return {
551
- action: 'set',
552
- ...await setEngineeringProfile({ profile: operands[1], projectRoot }),
553
- };
554
- }
555
- if (command === 'stack') {
556
- if (operands[0] === 'list') {
557
- return { status: 'ok', pieces: await listStackPieces({ projectRoot, stackPackages }) };
558
- }
559
- return addStack({ pieces: operands.slice(1), projectRoot, stackPackages });
560
- }
561
- if (command === 'prompt') {
562
- return generatePrompt({
563
- projectRoot,
564
- task: options.task || 'work',
565
- request: operands.join(' '),
566
- stackPackages,
567
- });
568
- }
569
- if (command === 'context') return getContext({ paths: operands, projectRoot, stackPackages });
570
- if (command === 'index') return indexCodebase({ projectRoot, queries: operands, stackPackages, callWeb: options.callWeb });
571
- if (command === 'inspect') {
572
- if (operands[0] === 'section') {
573
- return {
574
- inspection: 'section',
575
- ...await inspectStackSection({
576
- name: operands.slice(1).join(' '),
577
- projectRoot,
578
- stackPackages,
579
- }),
580
- };
581
- }
582
- const inspections = { subsystems: inspectSubsystems, environment: inspectEnvironment, project: inspectProject, skills: inspectSkills };
583
- return {
584
- inspection: operands[0],
585
- ...await inspections[operands[0]]({ projectRoot, stackPackages }),
586
- };
587
- }
588
- if (command === 'hook') {
589
- if (operands[0] === 'discover') {
590
- return { kind: 'discover', ...await codexAdoptionRecommendation({ projectRoot }) };
591
- }
592
- const scope = operands[0];
593
- if (scope === 'turn') {
594
- return hostContext === null
595
- ? { kind: 'turn', status: 'ready', output: '' }
596
- : {
597
- kind: 'turn',
598
- ...await projectTurnContext({
599
- hostDriver: () => hostContext.text,
600
- hostDriverInput: { scope: 'turn' },
601
- }),
602
- };
603
- }
604
- return {
605
- kind: 'session',
606
- ...await projectSessionContext({
607
- ...(hostContext === null ? {} : {
608
- hostDriver: () => hostContext.text,
609
- hostDriverInput: { scope: 'session' },
610
- }),
611
- projectRoot,
612
- stackPackages,
613
- }),
614
- };
615
- }
616
- if (command === 'verify') {
617
- return verify({
618
- projectRoot,
619
- signal,
620
- stackPackages,
621
- onEvent: (event) => {
622
- if (!options.json) line(process.stderr, event.message);
623
- },
624
- });
625
- }
626
- if (command === 'check') return check({ projectRoot, stackPackages });
627
- fail('CLI_UNKNOWN_COMMAND', `Unknown command: ${command}`);
440
+ // `genesis hook turn` runs on every agent turn and composes only the host's context, so it loads none of the project
441
+ // operations; every other command loads them with cli-commands.js when it runs.
442
+ async function executeCommand(parsed, { signal, hostContext }) {
443
+ if (parsed.command === 'hook' && parsed.operands[0] === 'turn') {
444
+ return hostContext === null
445
+ ? { kind: 'turn', status: 'ready', output: '' }
446
+ : {
447
+ kind: 'turn',
448
+ ...await projectTurnContext({
449
+ hostDriver: () => hostContext.text,
450
+ hostDriverInput: { scope: 'turn' },
451
+ }),
452
+ };
453
+ }
454
+ const { runCommand } = await import('./cli-commands.js');
455
+ return runCommand(parsed, { signal, hostContext, progress: (message) => line(process.stderr, message) });
628
456
  }
629
457
 
630
458
  async function optionalProjectFormat(projectRoot) {
@@ -695,7 +523,7 @@ export async function runCli(argv = process.argv.slice(2)) {
695
523
  process.once('SIGINT', abort);
696
524
  process.once('SIGTERM', abort);
697
525
  const result = withProjectFormatWarning(
698
- hookResult ?? await withParserCancellation(controller.signal, () => execute(parsed, { signal: controller.signal, hostContext })),
526
+ hookResult ?? await executeCommand(parsed, { signal: controller.signal, hostContext }),
699
527
  projectFormat,
700
528
  formatDiagnostic,
701
529
  );
@@ -85,11 +85,32 @@ function markdownDestinations(tree) {
85
85
  while (nodes.length > 0) {
86
86
  const node = nodes.pop();
87
87
  if (['definition', 'image', 'link'].includes(node?.type) && typeof node.url === 'string') {
88
- destinations.push({ line: node.position?.start?.line, url: node.url });
88
+ destinations.push(Object.freeze({ line: node.position?.start?.line, url: node.url }));
89
89
  }
90
90
  if (Array.isArray(node?.children)) nodes.push(...node.children);
91
91
  }
92
- return destinations.sort((left, right) => (left.line || 0) - (right.line || 0));
92
+ return Object.freeze(destinations.sort((left, right) => (left.line || 0) - (right.line || 0)));
93
+ }
94
+
95
+ // Link destinations by Markdown content hash. A skill plan validates every Markdown file of every selected skill,
96
+ // and one operation builds several plans (synchronization, inspection, context, prompts), so the same skill text was
97
+ // parsed again each time; the full CommonMark parse was 40% of the test suite's CPU. The destinations depend only on
98
+ // the text, so each distinct text is parsed once per process. Whether each local target exists is still checked on
99
+ // every validation.
100
+ const MAX_PARSED_MARKDOWN = 256;
101
+ const parsedMarkdownDestinations = new Map();
102
+
103
+ function markdownDestinationsOf(source) {
104
+ const key = sha256(source);
105
+ let destinations = parsedMarkdownDestinations.get(key);
106
+ if (!destinations) {
107
+ destinations = markdownDestinations(fromMarkdown(source));
108
+ if (parsedMarkdownDestinations.size >= MAX_PARSED_MARKDOWN) {
109
+ parsedMarkdownDestinations.delete(parsedMarkdownDestinations.keys().next().value);
110
+ }
111
+ parsedMarkdownDestinations.set(key, destinations);
112
+ }
113
+ return destinations;
93
114
  }
94
115
 
95
116
  function localReferencePath(url) {
@@ -121,16 +142,16 @@ async function validateMarkdownReferences(directory, files) {
121
142
  const problems = [];
122
143
  for (const file of files.filter(({ path: filePath }) => filePath.endsWith('.md'))) {
123
144
  const location = path.join(root, file.path);
124
- let tree;
145
+ let destinations;
125
146
  try {
126
- tree = fromMarkdown(await readFile(location, 'utf8'));
147
+ destinations = markdownDestinationsOf(await readFile(location, 'utf8'));
127
148
  } catch (error) {
128
149
  invalidSkill(`Agent Skill Markdown is invalid: ${location}.`, {
129
150
  cause: error.message,
130
151
  path: file.path,
131
152
  });
132
153
  }
133
- for (const destination of markdownDestinations(tree)) {
154
+ for (const destination of destinations) {
134
155
  const local = localReferencePath(destination.url);
135
156
  if (!local) continue;
136
157
  const problem = {
@@ -1,4 +1,4 @@
1
- import { gitContext } from './git.js';
1
+ import { inspectionRoot } from './git.js';
2
2
  import { readStack } from './stack.js';
3
3
  import { withStackEnvironmentDefaults } from './stack-environment-defaults.js';
4
4
  import { missingStackResources } from './stack-preflight.js';
@@ -8,9 +8,10 @@ import { GENESIS_CONTRACTS } from './contracts.js';
8
8
  export async function inspectProjectEnvironment({
9
9
  environment = process.env,
10
10
  projectRoot,
11
+ rootMode = 'repository',
11
12
  stackPackages = [],
12
13
  } = {}) {
13
- const root = (await gitContext(projectRoot)).repositoryRoot;
14
+ const root = await inspectionRoot({ projectRoot, rootMode, subject: 'Environment' });
14
15
  const stack = await readStack(root, { stackPackages });
15
16
  const diagnostics = missingStackResources({
16
17
  environment: withStackEnvironmentDefaults(environment, stack.environmentDefaults),
package/src/index/git.js CHANGED
@@ -1,4 +1,4 @@
1
- import { realpath } from 'node:fs/promises';
1
+ import { realpath, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { fail } from './errors.js';
@@ -51,3 +51,19 @@ export async function gitContext(projectRoot = process.cwd()) {
51
51
  }
52
52
  return { workingDirectory: root, repositoryRoot };
53
53
  }
54
+
55
+ /**
56
+ * The root a read-only inspection reads. The default `repository` mode discovers the enclosing Git worktree with
57
+ * its trust checks. `directory` reads exactly an explicit absolute directory without starting Git: a source
58
+ * snapshot without Git metadata, or a host that already knows the project root. There is no fallback between them.
59
+ */
60
+ export async function inspectionRoot({ projectRoot, rootMode = 'repository', subject }) {
61
+ if (rootMode === 'repository') return (await gitContext(projectRoot)).repositoryRoot;
62
+ if (rootMode !== 'directory') throw new TypeError(`${subject} rootMode must be repository or directory.`);
63
+ if (typeof projectRoot !== 'string' || !path.isAbsolute(projectRoot)) {
64
+ throw new TypeError('Directory inspection requires an explicit absolute projectRoot.');
65
+ }
66
+ const root = await realpath(projectRoot);
67
+ if (!(await stat(root)).isDirectory()) throw new TypeError('Directory inspection requires a directory.');
68
+ return root;
69
+ }
@@ -0,0 +1,70 @@
1
+ import { GENESIS_CONTRACTS } from './contracts.js';
2
+ import { GenesisError } from './errors.js';
3
+ import { normalizeSource, sha256, stableJson } from './utils.js';
4
+
5
+ // The host's contribution to the session and turn context lanes, and the turn lane itself. The turn lane composes only
6
+ // that contribution, and `genesis hook turn` runs it on every agent turn, so this module imports nothing that reads the
7
+ // project; the session lane (session-context.js) adds the project's guidance.
8
+ const HOST_CONTEXT_MAX_BYTES = Object.freeze({
9
+ session: 24 * 1024,
10
+ turn: 512,
11
+ });
12
+
13
+ export async function hostContextContribution({ hostDriver, hostDriverInput, scope }) {
14
+ if (hostDriver === undefined || hostDriver === null) {
15
+ if (hostDriverInput !== undefined) {
16
+ throw new GenesisError(
17
+ 'HOST_CONTEXT_INVALID',
18
+ 'Host context input requires an explicitly registered host driver.',
19
+ );
20
+ }
21
+ return '';
22
+ }
23
+ if (typeof hostDriver !== 'function') {
24
+ throw new GenesisError('HOST_CONTEXT_INVALID', 'The registered host driver must be a function.');
25
+ }
26
+ if (
27
+ !hostDriverInput
28
+ || typeof hostDriverInput !== 'object'
29
+ || Array.isArray(hostDriverInput)
30
+ || hostDriverInput.scope !== scope
31
+ ) {
32
+ throw new GenesisError(
33
+ 'HOST_CONTEXT_INVALID',
34
+ `The ${scope} context lane requires host input with scope \`${scope}\`.`,
35
+ );
36
+ }
37
+ const value = await hostDriver(hostDriverInput);
38
+ if (typeof value !== 'string') {
39
+ throw new GenesisError('HOST_CONTEXT_INVALID', 'The registered host driver must return plain text.');
40
+ }
41
+ const output = normalizeSource(value).trim();
42
+ if (Buffer.byteLength(output, 'utf8') > HOST_CONTEXT_MAX_BYTES[scope]) {
43
+ throw new GenesisError(
44
+ 'HOST_CONTEXT_INVALID',
45
+ `The registered host driver's ${scope} contribution exceeds ${HOST_CONTEXT_MAX_BYTES[scope]} bytes.`,
46
+ );
47
+ }
48
+ return output;
49
+ }
50
+
51
+ export function composedContext(contract, output) {
52
+ return {
53
+ contract,
54
+ status: 'ready',
55
+ output,
56
+ identity: sha256(stableJson({ contract, output })),
57
+ };
58
+ }
59
+
60
+ export async function projectTurnContext({ hostDriver, hostDriverInput } = {}) {
61
+ const output = await hostContextContribution({
62
+ hostDriver,
63
+ hostDriverInput,
64
+ scope: 'turn',
65
+ });
66
+ return {
67
+ ...composedContext(GENESIS_CONTRACTS.turnContext, output),
68
+ trust: 'untrusted',
69
+ };
70
+ }
@@ -216,6 +216,7 @@ export async function runGit(cwd, args, options = {}) {
216
216
  ...options.env,
217
217
  }),
218
218
  maxBytes: options.maxBytes,
219
+ input: options.input,
219
220
  timeoutMs: options.timeoutMs,
220
221
  signal: options.signal,
221
222
  code: options.code || 'GIT_EXEC_FAILED',
@@ -170,8 +170,13 @@ export async function inspectProgram(projectRoot) {
170
170
  const contents = moduleContents(source, programPath);
171
171
  if (!assignments.has(programPath)) throw new GenesisError('SUBSYSTEMS_PROGRAM_UNASSIGNED', `Program module has no declared subsystem: ${programPath}.`);
172
172
  const { sources } = contents;
173
- for (const sourcePath of sources) {
174
- if (!sourcePath || !isProjectContentPath(sourcePath) || !await sourceExists(projectRoot, sourcePath)) {
173
+ // A module cites dozens of sources; check them all at once and report the first problem in citation order.
174
+ const checks = await Promise.allSettled(sources.map((sourcePath) => (
175
+ sourcePath && isProjectContentPath(sourcePath) && sourceExists(projectRoot, sourcePath)
176
+ )));
177
+ for (const [index, sourcePath] of sources.entries()) {
178
+ if (checks[index].status === 'rejected') throw checks[index].reason;
179
+ if (!checks[index].value) {
175
180
  throw new GenesisError(
176
181
  'PROGRAM_SOURCE_MISSING',
177
182
  `Program module cites a missing or ineligible source file: ${sourcePath}.`,
@@ -30,6 +30,6 @@ export async function gitVisibleFileStates(projectRoot, { includePath = () => tr
30
30
 
31
31
  /** Classifies opening behavior from cheap Git-visible paths and selected Stack state. */
32
32
  export async function classifyProjectKind({ projectRoot } = {}) {
33
- const content = await inspectProjectContent(projectRoot);
34
- return content.kind === 'existing' ? 'existing' : 'new';
33
+ const { kind } = await inspectProjectContent(projectRoot, { kindOnly: true });
34
+ return kind === 'existing' ? 'existing' : 'new';
35
35
  }
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { asDiagnostic, GenesisError } from './errors.js';
5
- import { gitContext } from './git.js';
5
+ import { inspectionRoot } from './git.js';
6
6
  import {
7
7
  BLUEPRINT_PATH,
8
8
  COLLABORATION_PATH,
@@ -90,9 +90,8 @@ export async function inspectProjectFormatAtRoot(projectRoot) {
90
90
  return projectFormat('current', version, null);
91
91
  }
92
92
 
93
- export async function inspectProjectFormat({ projectRoot } = {}) {
94
- const root = (await gitContext(projectRoot)).repositoryRoot;
95
- return inspectProjectFormatAtRoot(root);
93
+ export async function inspectProjectFormat({ projectRoot, rootMode = 'repository' } = {}) {
94
+ return inspectProjectFormatAtRoot(await inspectionRoot({ projectRoot, rootMode, subject: 'Project format' }));
96
95
  }
97
96
 
98
97
  export function projectFormatDiagnostic(format) {