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.
- package/README.md +46 -16
- package/package.json +2 -2
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/project-guidance.js +6 -5
- package/profiles/engineering/baseline.md +8 -2
- package/prompts/deslop.txt +3 -2
- package/prompts/review.txt +12 -5
- package/skills/genesis-deslop/SKILL.md +10 -2
- package/skills/genesis-project/SKILL.md +11 -8
- package/skills/genesis-project/references/sanity-checks.md +352 -0
- package/src/cli-commands.js +193 -0
- package/src/cli.js +18 -190
- package/src/index/agent-skills.js +26 -5
- package/src/index/environment-files.js +3 -2
- package/src/index/git.js +17 -1
- package/src/index/host-context.js +70 -0
- package/src/index/process.js +1 -0
- package/src/index/program.js +7 -2
- package/src/index/project-files.js +2 -2
- package/src/index/project-format.js +3 -4
- package/src/index/project-inspection.js +33 -11
- package/src/index/session-context.js +6 -67
- package/src/index/stack-catalog.js +29 -1
- package/src/index/stack-section-inspection.js +2 -16
- package/src/index/template-source.js +45 -10
|
@@ -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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
|
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
|
|
145
|
+
let destinations;
|
|
125
146
|
try {
|
|
126
|
-
|
|
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
|
|
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 {
|
|
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 =
|
|
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
|
+
}
|
package/src/index/process.js
CHANGED
package/src/index/program.js
CHANGED
|
@@ -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
|
-
|
|
174
|
-
|
|
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
|
|
34
|
-
return
|
|
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 {
|
|
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
|
-
|
|
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) {
|