genesis-compiler 1.10.11 → 1.10.13
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 +16 -6
- package/docs/prompt-integration.md +12 -2
- package/package.json +1 -1
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode/project-guidance.js +6 -5
- package/prompts/review.txt +20 -4
- package/skills/genesis-program/SKILL.md +3 -3
- 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
|
@@ -736,9 +736,13 @@ The eight task types are deliberately explicit:
|
|
|
736
736
|
- `describe` creates or refreshes both the non-technical Blueprint and the
|
|
737
737
|
complete useful Program from the current codebase, optional user guidance,
|
|
738
738
|
and selected Stack guidance where it affects accurate explanation.
|
|
739
|
-
- `review`
|
|
740
|
-
|
|
741
|
-
|
|
739
|
+
- `review` asks the agent to compare Blueprint, code, Program, and tests,
|
|
740
|
+
identify contradictions and gaps, and distinguish evidence from inference.
|
|
741
|
+
It judges the tests too: the relevant ones run and pass, with what ran and
|
|
742
|
+
what was not verified reported; changed behavior has tests; no test was
|
|
743
|
+
weakened, skipped, deleted or loosened to pass; and the changed tests stay
|
|
744
|
+
fast against coarse sanity limits. It edits only tests, writing the missing
|
|
745
|
+
ones or reporting those it cannot.
|
|
742
746
|
|
|
743
747
|
Prompt generation is read-only. Missing external resources are reported in
|
|
744
748
|
the prompt but do not prevent its generation. Genesis never invents values.
|
|
@@ -1052,10 +1056,12 @@ The CLI can also prepare and verify parsers for an offline host; see
|
|
|
1052
1056
|
[language parser management](docs/parsers.md).
|
|
1053
1057
|
`migrate()` updates recognized older Genesis project-file formats and returns
|
|
1054
1058
|
the resulting structural check for hosts.
|
|
1055
|
-
`inspectProjectFormat({ projectRoot })` returns only the format marker's `path`,
|
|
1059
|
+
`inspectProjectFormat({ projectRoot, rootMode })` returns only the format marker's `path`,
|
|
1056
1060
|
`status`, `projectVersion`, `supportedVersion`, `action`, and diagnostic when
|
|
1057
|
-
invalid.
|
|
1061
|
+
invalid. By default it retains Git repository discovery and trust checks, but does not
|
|
1058
1062
|
enumerate application files, load Stack catalogs, or inspect project guidance.
|
|
1063
|
+
`rootMode: "directory"` reads an explicit absolute directory without Git, as for
|
|
1064
|
+
Stack sections below.
|
|
1059
1065
|
Missing markers distinguish uninitialized projects from unversioned Genesis
|
|
1060
1066
|
material. Each call reads current state; it neither migrates nor writes files.
|
|
1061
1067
|
`adoptProject()` also returns the initial `adopt` prompt for an existing
|
|
@@ -1116,7 +1122,11 @@ hosts may pass `rootMode: "directory"` and an explicit absolute `projectRoot`.
|
|
|
1116
1122
|
This reads that exact directory, including when it sits inside another Git
|
|
1117
1123
|
repository. It retains the same Stack validation and performs no writes or
|
|
1118
1124
|
commands. Missing/invalid roots fail; this is never an automatic fallback from
|
|
1119
|
-
Git ownership or discovery errors.
|
|
1125
|
+
Git ownership or discovery errors. `inspectEnvironment()` and
|
|
1126
|
+
`inspectProjectFormat()` accept the same `rootMode` with the same rules. A host
|
|
1127
|
+
that already knows the project root and inspects it on every request, such as a
|
|
1128
|
+
preview status poll, can pass `rootMode: "directory"` to all three and start no
|
|
1129
|
+
Git process.
|
|
1120
1130
|
|
|
1121
1131
|
`inspectStackSections({ names, projectRoot, rootMode, stackPackages })` returns
|
|
1122
1132
|
an array of those same section results in the requested order. Names must be
|
|
@@ -161,8 +161,18 @@ before selection. For an existing uninitialized codebase it owns
|
|
|
161
161
|
the adoption recommendation as part of the same portable prompt. A host sends this prompt on the
|
|
162
162
|
first chat turn; it does not recreate the questions or Stack catalog.
|
|
163
163
|
`describe` creates or refreshes the complete Blueprint and useful Program in one
|
|
164
|
-
agent task.
|
|
165
|
-
|
|
164
|
+
agent task. `review` is the generic review contract a host gives an agent after
|
|
165
|
+
a piece of work. Besides comparing Blueprint, code, Program and tests, it has
|
|
166
|
+
the agent judge the tests: the relevant ones run and pass, with what ran and
|
|
167
|
+
what was not verified reported; changed behavior has new or updated tests,
|
|
168
|
+
written in the review or reported when they cannot be; a test weakened,
|
|
169
|
+
skipped, deleted or loosened to pass is a finding; and the changed tests stay
|
|
170
|
+
fast against coarse sanity limits (a suite never runs for hours, a test needing
|
|
171
|
+
minutes is a smell, a test database is ready in seconds), never per-test
|
|
172
|
+
budgets. It points at the project skill's verification section and the sanity
|
|
173
|
+
checks guide rather than restating them, and edits only tests. The host owns
|
|
174
|
+
the agent and its interaction. Genesis owns only transparent prompt
|
|
175
|
+
construction and deterministic project operations.
|
|
166
176
|
|
|
167
177
|
## Instruction sources
|
|
168
178
|
|
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);
|
package/prompts/review.txt
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
Read `.agents/skills/genesis-project/SKILL.md` and
|
|
2
|
-
`.agents/skills/genesis-program/SKILL.md` completely, then
|
|
3
|
-
review.
|
|
2
|
+
`.agents/skills/genesis-program/SKILL.md` completely, then review the work.
|
|
4
3
|
|
|
5
4
|
Compare Blueprint intent, observable implementation, tests, Program public
|
|
6
5
|
contracts, informational implementation maps, and recorded verification in both
|
|
@@ -8,6 +7,21 @@ directions. Identify missing or contradictory behavior, misleading explanation,
|
|
|
8
7
|
unnecessarily difficult public shape, and verification gaps. Distinguish direct
|
|
9
8
|
evidence from inference and state questions that require human intent.
|
|
10
9
|
|
|
10
|
+
Judge the tests as part of the work, with the project skill's "Verify and
|
|
11
|
+
report" section and the test sections of the sanity checks guide:
|
|
12
|
+
- Run the tests relevant to the change, through the Stack's verification
|
|
13
|
+
commands or focused checks; a failure is a finding. Report what actually ran
|
|
14
|
+
and anything not verified; never report an unrun check as passing.
|
|
15
|
+
- Changed behavior has tests, new or updated, that would fail without it. Write
|
|
16
|
+
the missing ones as part of this review; when that is not possible, report
|
|
17
|
+
each with the behavior it should prove.
|
|
18
|
+
- A test weakened, skipped, deleted or loosened just to make things pass is a
|
|
19
|
+
finding, reported like a product defect.
|
|
20
|
+
- Tests stay fast. Look at what the changed tests cost and where their time
|
|
21
|
+
goes. Coarse sanity limits, never per-test budgets: a whole suite never runs
|
|
22
|
+
for hours, one test that needs minutes is a smell, and a test database is
|
|
23
|
+
ready in seconds.
|
|
24
|
+
|
|
11
25
|
Report what makes requests or tests slower than their work, citing a count or
|
|
12
26
|
timing where one exists and the code path otherwise: a list or batch whose
|
|
13
27
|
query count grows with its rows, an access check whose cost grows with stored
|
|
@@ -21,7 +35,9 @@ access decided from ids the client sends rather than the trusted context.
|
|
|
21
35
|
Judge each against the matching section and outer limit of
|
|
22
36
|
`.agents/skills/genesis-project/references/sanity-checks.md`.
|
|
23
37
|
|
|
24
|
-
|
|
25
|
-
|
|
38
|
+
Edit only tests: add or update the tests the changed behavior lacks, and change
|
|
39
|
+
no product code, Blueprint, Program, Stack or other file. Return a concise
|
|
40
|
+
report ordered by impact with exact file references, the evidence inspected,
|
|
41
|
+
the tests run and written, and the smallest sensible correction.
|
|
26
42
|
|
|
27
43
|
Review `genesis/subsystems.md` responsibilities, Program membership, and data ownership/use against source and schema. Report missing, stale, or conflicting associations without editing them.
|
|
@@ -71,14 +71,14 @@ Program module.
|
|
|
71
71
|
## Task boundaries
|
|
72
72
|
|
|
73
73
|
The caller determines whether this is initial description, complete Program
|
|
74
|
-
refresh, focused post-change reconciliation, Blueprint-only work, or
|
|
75
|
-
|
|
74
|
+
refresh, focused post-change reconciliation, Blueprint-only work, or review.
|
|
75
|
+
Respect the caller's edit boundary. Report ambiguity rather than
|
|
76
76
|
inventing intent.
|
|
77
77
|
|
|
78
78
|
## Subsystems
|
|
79
79
|
|
|
80
80
|
Maintain `genesis/subsystems.md` alongside Program in description and refresh
|
|
81
|
-
work. Blueprint-only work
|
|
81
|
+
work. Blueprint-only work and review do not change the map.
|
|
82
82
|
The map is the authored association authority, but responsibilities and table
|
|
83
83
|
references remain fallible explanations of application source and schema.
|
|
84
84
|
|
|
@@ -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);
|