genesis-compiler 1.10.11 → 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 +9 -3
- package/package.json +1 -1
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/project-guidance.js +6 -5
- 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/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 +32 -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/README.md
CHANGED
|
@@ -1052,10 +1052,12 @@ The CLI can also prepare and verify parsers for an offline host; see
|
|
|
1052
1052
|
[language parser management](docs/parsers.md).
|
|
1053
1053
|
`migrate()` updates recognized older Genesis project-file formats and returns
|
|
1054
1054
|
the resulting structural check for hosts.
|
|
1055
|
-
`inspectProjectFormat({ projectRoot })` returns only the format marker's `path`,
|
|
1055
|
+
`inspectProjectFormat({ projectRoot, rootMode })` returns only the format marker's `path`,
|
|
1056
1056
|
`status`, `projectVersion`, `supportedVersion`, `action`, and diagnostic when
|
|
1057
|
-
invalid.
|
|
1057
|
+
invalid. By default it retains Git repository discovery and trust checks, but does not
|
|
1058
1058
|
enumerate application files, load Stack catalogs, or inspect project guidance.
|
|
1059
|
+
`rootMode: "directory"` reads an explicit absolute directory without Git, as for
|
|
1060
|
+
Stack sections below.
|
|
1059
1061
|
Missing markers distinguish uninitialized projects from unversioned Genesis
|
|
1060
1062
|
material. Each call reads current state; it neither migrates nor writes files.
|
|
1061
1063
|
`adoptProject()` also returns the initial `adopt` prompt for an existing
|
|
@@ -1116,7 +1118,11 @@ hosts may pass `rootMode: "directory"` and an explicit absolute `projectRoot`.
|
|
|
1116
1118
|
This reads that exact directory, including when it sits inside another Git
|
|
1117
1119
|
repository. It retains the same Stack validation and performs no writes or
|
|
1118
1120
|
commands. Missing/invalid roots fail; this is never an automatic fallback from
|
|
1119
|
-
Git ownership or discovery errors.
|
|
1121
|
+
Git ownership or discovery errors. `inspectEnvironment()` and
|
|
1122
|
+
`inspectProjectFormat()` accept the same `rootMode` with the same rules. A host
|
|
1123
|
+
that already knows the project root and inspects it on every request, such as a
|
|
1124
|
+
preview status poll, can pass `rootMode: "directory"` to all three and start no
|
|
1125
|
+
Git process.
|
|
1120
1126
|
|
|
1121
1127
|
`inspectStackSections({ names, projectRoot, rootMode, stackPackages })` returns
|
|
1122
1128
|
an array of those same section results in the requested order. Names must be
|
package/package.json
CHANGED
|
@@ -6,11 +6,11 @@ import { promisify } from "node:util";
|
|
|
6
6
|
const execute = promisify(execFile);
|
|
7
7
|
|
|
8
8
|
const HOST_CONTEXT_INPUT_ENV = "GENESIS_HOST_CONTEXT_INPUT";
|
|
9
|
+
const HOOK_TIMEOUT_MS = 5_000;
|
|
9
10
|
|
|
10
|
-
async function renderContext(projectRoot, sessionId, scope, parentSessionIds = []) {
|
|
11
|
+
async function renderContext(projectRoot, sessionId, scope, parentSessionIds = [], timeoutMs = HOOK_TIMEOUT_MS) {
|
|
11
12
|
if (scope === "turn" && process.env.GENESIS_TURN_CONTEXT_ENABLED === "0") return "";
|
|
12
13
|
const started = Date.now();
|
|
13
|
-
const timeoutMs = 5_000;
|
|
14
14
|
try {
|
|
15
15
|
const { stdout } = await execute(
|
|
16
16
|
"genesis",
|
|
@@ -54,7 +54,8 @@ function eventSessionId(event = {}) {
|
|
|
54
54
|
).trim();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
// OpenCode passes no hookTimeoutMs, so its hooks get five seconds; a test passes a shorter deadline to prove it.
|
|
58
|
+
export const GenesisProjectGuidance = async ({ client, directory, worktree, hookTimeoutMs = HOOK_TIMEOUT_MS } = {}) => {
|
|
58
59
|
const projectRoot = path.resolve(directory || worktree || process.cwd());
|
|
59
60
|
const contexts = new Map();
|
|
60
61
|
const turnContexts = new Map();
|
|
@@ -72,7 +73,7 @@ export const GenesisProjectGuidance = async ({ client, directory, worktree } = {
|
|
|
72
73
|
if (!id || id === sessionId || parents.includes(id)) break;
|
|
73
74
|
parents.push(id);
|
|
74
75
|
}
|
|
75
|
-
return renderContext(projectRoot, sessionId, "session", parents);
|
|
76
|
+
return renderContext(projectRoot, sessionId, "session", parents, hookTimeoutMs);
|
|
76
77
|
});
|
|
77
78
|
contexts.set(sessionId, context);
|
|
78
79
|
context.catch(() => {
|
|
@@ -108,7 +109,7 @@ export const GenesisProjectGuidance = async ({ client, directory, worktree } = {
|
|
|
108
109
|
if (!sessionId || !messageId || !Array.isArray(message.parts)) return;
|
|
109
110
|
let current = turnContexts.get(sessionId);
|
|
110
111
|
if (current?.messageId !== messageId) {
|
|
111
|
-
current = { messageId, context: renderContext(projectRoot, sessionId, "turn") };
|
|
112
|
+
current = { messageId, context: renderContext(projectRoot, sessionId, "turn", [], hookTimeoutMs) };
|
|
112
113
|
turnContexts.set(sessionId, current);
|
|
113
114
|
current.context.catch(() => {
|
|
114
115
|
if (turnContexts.get(sessionId) === current) turnContexts.delete(sessionId);
|
|
@@ -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/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) {
|
|
@@ -51,22 +51,39 @@ async function isBootstrapGitignore(projectRoot, paths) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
async function listFiles(projectRoot, options) {
|
|
55
55
|
// Exclude dependency paths in Git before collecting and sorting its output.
|
|
56
|
-
const
|
|
56
|
+
const { stdout } = await runGit(projectRoot, ['ls-files', '-z', ...options, '--', '.', ':(exclude,glob)**/node_modules/**']);
|
|
57
|
+
return stdout.toString('utf8').split('\0').filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* With kindOnly, only `kind` is returned. Listing ignored files walks ignored dependency trees such as node_modules
|
|
62
|
+
* despite the exclusion, which dominates the inspection of an installed application, so it is skipped when Git's
|
|
63
|
+
* visible files already include project content: ignored files can add existing content, never remove it.
|
|
64
|
+
*/
|
|
65
|
+
export async function inspectProjectContent(projectRoot, { kindOnly = false } = {}) {
|
|
66
|
+
const listIgnored = () => listFiles(projectRoot, ['--others', '--ignored', '--exclude-standard']);
|
|
57
67
|
const [visible, deleted, ignored] = await Promise.all([
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
listFiles(projectRoot, ['--cached', '--others', '--exclude-standard']),
|
|
69
|
+
listFiles(projectRoot, ['--deleted']),
|
|
70
|
+
kindOnly ? [] : listIgnored(),
|
|
61
71
|
]);
|
|
62
|
-
|
|
63
|
-
const
|
|
72
|
+
if (!kindOnly) return classifyContent(projectRoot, visible, deleted, ignored);
|
|
73
|
+
const { kind } = await classifyContent(projectRoot, visible, deleted, []);
|
|
74
|
+
if (kind === 'existing') return { kind };
|
|
75
|
+
return { kind: (await classifyContent(projectRoot, visible, deleted, await listIgnored())).kind };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function classifyContent(projectRoot, visible, deleted, ignored) {
|
|
79
|
+
const absent = new Set(deleted);
|
|
80
|
+
const paths = [...new Set([...visible, ...ignored])]
|
|
64
81
|
.filter((file) => !absent.has(file) && file !== '.git' && !file.startsWith('.git/'))
|
|
65
82
|
.sort();
|
|
66
83
|
// OpenCode installs its plugin dependency beside the generated guidance plugin.
|
|
67
84
|
// Recognize only its exact ignored scaffolding, never an authored config tree.
|
|
68
85
|
const opencodePaths = ['.opencode/.gitignore', '.opencode/package.json', '.opencode/package-lock.json'];
|
|
69
|
-
const ignoredPaths = new Set(ignored
|
|
86
|
+
const ignoredPaths = new Set(ignored);
|
|
70
87
|
let opencodeBootstrap = [];
|
|
71
88
|
if (opencodePaths.every((file) => ignoredPaths.has(file))) {
|
|
72
89
|
try {
|
|
@@ -100,15 +117,19 @@ export async function inspectProjectContent(projectRoot) {
|
|
|
100
117
|
|
|
101
118
|
/** Read-only opening inspection: no index generation, verification, or application execution. */
|
|
102
119
|
export async function inspectProject({ projectRoot, stackPackages = [] } = {}) {
|
|
103
|
-
|
|
120
|
+
return inspectProjectAtRoot((await gitContext(projectRoot)).repositoryRoot, { stackPackages });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The same inspection of a resolved repository root; without `count`, content reports only its kind. */
|
|
124
|
+
export async function inspectProjectAtRoot(root, { stackPackages = [], count = true } = {}) {
|
|
104
125
|
const [content, format] = await Promise.all([
|
|
105
|
-
inspectProjectContent(root),
|
|
126
|
+
inspectProjectContent(root, { kindOnly: !count }),
|
|
106
127
|
inspectProjectFormatAtRoot(root),
|
|
107
128
|
]);
|
|
108
129
|
const result = {
|
|
109
130
|
contract: GENESIS_CONTRACTS.projectInspection,
|
|
110
131
|
state: 'attention',
|
|
111
|
-
content: { kind: content.kind, count: content.existingPaths.length },
|
|
132
|
+
content: count ? { kind: content.kind, count: content.existingPaths.length } : { kind: content.kind },
|
|
112
133
|
projectFormat: format,
|
|
113
134
|
templateEligible: false,
|
|
114
135
|
stackComponents: [],
|
|
@@ -2,17 +2,12 @@ import { readCollaboration } from './collaboration.js';
|
|
|
2
2
|
import { inspectProjectSkills, renderAgentSkillCatalog } from './agent-skills.js';
|
|
3
3
|
import { GENESIS_CONTRACTS } from './contracts.js';
|
|
4
4
|
import { readEngineering, readEngineeringBaseline } from './engineering.js';
|
|
5
|
-
import { asDiagnostic
|
|
5
|
+
import { asDiagnostic } from './errors.js';
|
|
6
6
|
import { gitContext } from './git.js';
|
|
7
|
-
import {
|
|
7
|
+
import { inspectProjectAtRoot } from './project-inspection.js';
|
|
8
8
|
import { readStack } from './stack.js';
|
|
9
9
|
import { listStackCatalogPieces } from './stack-catalog.js';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
const HOST_CONTEXT_MAX_BYTES = Object.freeze({
|
|
13
|
-
session: 24 * 1024,
|
|
14
|
-
turn: 512,
|
|
15
|
-
});
|
|
10
|
+
import { composedContext, hostContextContribution } from './host-context.js';
|
|
16
11
|
|
|
17
12
|
async function optionalStack(projectRoot, stackPackages) {
|
|
18
13
|
try { return await readStack(projectRoot, { stackPackages }); } catch { return null; }
|
|
@@ -47,53 +42,6 @@ async function optionalCollaboration(projectRoot) {
|
|
|
47
42
|
}
|
|
48
43
|
}
|
|
49
44
|
|
|
50
|
-
async function hostContextContribution({ hostDriver, hostDriverInput, scope }) {
|
|
51
|
-
if (hostDriver === undefined || hostDriver === null) {
|
|
52
|
-
if (hostDriverInput !== undefined) {
|
|
53
|
-
throw new GenesisError(
|
|
54
|
-
'HOST_CONTEXT_INVALID',
|
|
55
|
-
'Host context input requires an explicitly registered host driver.',
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
return '';
|
|
59
|
-
}
|
|
60
|
-
if (typeof hostDriver !== 'function') {
|
|
61
|
-
throw new GenesisError('HOST_CONTEXT_INVALID', 'The registered host driver must be a function.');
|
|
62
|
-
}
|
|
63
|
-
if (
|
|
64
|
-
!hostDriverInput
|
|
65
|
-
|| typeof hostDriverInput !== 'object'
|
|
66
|
-
|| Array.isArray(hostDriverInput)
|
|
67
|
-
|| hostDriverInput.scope !== scope
|
|
68
|
-
) {
|
|
69
|
-
throw new GenesisError(
|
|
70
|
-
'HOST_CONTEXT_INVALID',
|
|
71
|
-
`The ${scope} context lane requires host input with scope \`${scope}\`.`,
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
const value = await hostDriver(hostDriverInput);
|
|
75
|
-
if (typeof value !== 'string') {
|
|
76
|
-
throw new GenesisError('HOST_CONTEXT_INVALID', 'The registered host driver must return plain text.');
|
|
77
|
-
}
|
|
78
|
-
const output = normalizeSource(value).trim();
|
|
79
|
-
if (Buffer.byteLength(output, 'utf8') > HOST_CONTEXT_MAX_BYTES[scope]) {
|
|
80
|
-
throw new GenesisError(
|
|
81
|
-
'HOST_CONTEXT_INVALID',
|
|
82
|
-
`The registered host driver's ${scope} contribution exceeds ${HOST_CONTEXT_MAX_BYTES[scope]} bytes.`,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
return output;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function composedContext(contract, output) {
|
|
89
|
-
return {
|
|
90
|
-
contract,
|
|
91
|
-
status: 'ready',
|
|
92
|
-
output,
|
|
93
|
-
identity: sha256(stableJson({ contract, output })),
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
45
|
async function optionalStackComponentIds(projectRoot, stack, stackPackages) {
|
|
98
46
|
try {
|
|
99
47
|
const packages = [...new Set([...(stack?.stackPackages || []), ...stackPackages])];
|
|
@@ -116,7 +64,8 @@ export async function projectSessionContext({
|
|
|
116
64
|
optionalCollaboration(root),
|
|
117
65
|
optionalEngineering(root),
|
|
118
66
|
hostContextContribution({ hostDriver, hostDriverInput, scope: 'session' }),
|
|
119
|
-
|
|
67
|
+
// The opening state needs only the content kind, not a count of every ignored file.
|
|
68
|
+
inspectProjectAtRoot(root, { stackPackages, count: false }),
|
|
120
69
|
]);
|
|
121
70
|
const availableComponents = await optionalStackComponentIds(root, stack, stackPackages);
|
|
122
71
|
const skills = stack && inspection.projectFormat.status === 'current'
|
|
@@ -172,14 +121,4 @@ export async function projectSessionContext({
|
|
|
172
121
|
return { ...composedContext(GENESIS_CONTRACTS.sessionContext, output), warnings: skills.diagnostics };
|
|
173
122
|
}
|
|
174
123
|
|
|
175
|
-
export
|
|
176
|
-
const output = await hostContextContribution({
|
|
177
|
-
hostDriver,
|
|
178
|
-
hostDriverInput,
|
|
179
|
-
scope: 'turn',
|
|
180
|
-
});
|
|
181
|
-
return {
|
|
182
|
-
...composedContext(GENESIS_CONTRACTS.turnContext, output),
|
|
183
|
-
trust: 'untrusted',
|
|
184
|
-
};
|
|
185
|
-
}
|
|
124
|
+
export { projectTurnContext } from './host-context.js';
|
|
@@ -7,11 +7,39 @@ import {
|
|
|
7
7
|
normalizeStackPieceId,
|
|
8
8
|
parseStackPieceSource,
|
|
9
9
|
} from './stack-piece.js';
|
|
10
|
+
import { sha256 } from './utils.js';
|
|
10
11
|
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
13
|
const PACKAGE_NAME = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/u;
|
|
13
14
|
const FIRST_PARTY_STACK_PACKAGE = 'genesis-stack';
|
|
14
15
|
|
|
16
|
+
// Parsed catalogue pieces by source text. One operation reads the catalogue several times (the Stack, then the code
|
|
17
|
+
// index, context and prompts each read it again), and every read parsed every piece. A parse depends only on the text,
|
|
18
|
+
// the expected id and the path its errors name, so each distinct piece is parsed once per process; the result is
|
|
19
|
+
// frozen so no reader can change what another reads. Every read still reads each file, so an edited piece is parsed
|
|
20
|
+
// again, and a piece that fails to parse is never kept.
|
|
21
|
+
const MAX_PARSED_PIECES = 512;
|
|
22
|
+
const parsedPieces = new Map();
|
|
23
|
+
|
|
24
|
+
function deepFreeze(value) {
|
|
25
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
26
|
+
Object.freeze(value);
|
|
27
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parsedStackPiece(source, expectedId, piecePath) {
|
|
33
|
+
const key = `${sha256(source)}\0${expectedId}\0${piecePath}`;
|
|
34
|
+
let piece = parsedPieces.get(key);
|
|
35
|
+
if (!piece) {
|
|
36
|
+
piece = deepFreeze(parseStackPieceSource(source, { expectedId, path: piecePath }));
|
|
37
|
+
if (parsedPieces.size >= MAX_PARSED_PIECES) parsedPieces.delete(parsedPieces.keys().next().value);
|
|
38
|
+
parsedPieces.set(key, piece);
|
|
39
|
+
}
|
|
40
|
+
return piece;
|
|
41
|
+
}
|
|
42
|
+
|
|
15
43
|
export function normalizeStackPackageName(value) {
|
|
16
44
|
const name = String(value ?? '').trim();
|
|
17
45
|
if (!PACKAGE_NAME.test(name)) {
|
|
@@ -94,7 +122,7 @@ async function readPieceDirectory(
|
|
|
94
122
|
return Promise.all(files.map(async (file) => {
|
|
95
123
|
const id = normalizeStackPieceId(file.slice(0, -3));
|
|
96
124
|
const source = await readFile(path.join(directory, file), 'utf8');
|
|
97
|
-
const piece =
|
|
125
|
+
const piece = parsedStackPiece(source, id, `${sourcePrefix}:${id}`);
|
|
98
126
|
return {
|
|
99
127
|
...piece,
|
|
100
128
|
skill: piece.skill === null ? null : { ...piece.skill, resolveFrom: stackPackageRoot },
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { realpath, stat } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
|
|
4
1
|
import { GENESIS_CONTRACTS } from './contracts.js';
|
|
5
|
-
import {
|
|
2
|
+
import { inspectionRoot } from './git.js';
|
|
6
3
|
import { readStack } from './stack.js';
|
|
7
4
|
import { normalizeStackSectionName } from './stack-section.js';
|
|
8
5
|
import { sha256, stableJson } from './utils.js';
|
|
@@ -29,18 +26,7 @@ export async function inspectProjectStackSections({
|
|
|
29
26
|
if (new Set(sectionNames).size !== sectionNames.length) {
|
|
30
27
|
throw new TypeError('Stack section inspection requires distinct names.');
|
|
31
28
|
}
|
|
32
|
-
|
|
33
|
-
if (rootMode === 'repository') {
|
|
34
|
-
root = (await gitContext(projectRoot)).repositoryRoot;
|
|
35
|
-
} else if (rootMode === 'directory') {
|
|
36
|
-
if (typeof projectRoot !== 'string' || !path.isAbsolute(projectRoot)) {
|
|
37
|
-
throw new TypeError('Directory inspection requires an explicit absolute projectRoot.');
|
|
38
|
-
}
|
|
39
|
-
root = await realpath(projectRoot);
|
|
40
|
-
if (!(await stat(root)).isDirectory()) throw new TypeError('Directory inspection requires a directory.');
|
|
41
|
-
} else {
|
|
42
|
-
throw new TypeError('Stack section rootMode must be repository or directory.');
|
|
43
|
-
}
|
|
29
|
+
const root = await inspectionRoot({ projectRoot, rootMode, subject: 'Stack section' });
|
|
44
30
|
const stack = await readStack(root, { stackPackages });
|
|
45
31
|
return sectionNames.map((sectionName) => {
|
|
46
32
|
const section = stack.extensions.find((candidate) => candidate.name === sectionName);
|