genesis-compiler 1.3.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/docs/templates.md +122 -0
- package/package.json +2 -1
- package/plugins/genesis/.codex-plugin/plugin.json +4 -2
- package/prompts/start-existing-uninitialized.txt +14 -5
- package/prompts/start-new.txt +7 -0
- package/src/cli.js +46 -5
- package/src/index/codex-hooks.js +4 -3
- package/src/index/contracts.js +3 -0
- package/src/index/process.js +2 -0
- package/src/index/project-files.js +4 -9
- package/src/index/project-inspection.js +107 -0
- package/src/index/prompt.js +26 -0
- package/src/index/session-context.js +12 -1
- package/src/index/template-catalog.js +100 -0
- package/src/index/template-project.js +152 -0
- package/src/index/template-source.js +51 -0
- package/src/index.js +13 -2
package/README.md
CHANGED
|
@@ -63,6 +63,10 @@ completely or correctly explains the implementation.
|
|
|
63
63
|
|
|
64
64
|
## Quick start
|
|
65
65
|
|
|
66
|
+
For ready-made applications and the shared new/existing-project workflow, see
|
|
67
|
+
[Project opening and templates](docs/templates.md). Templates belong to Genesis
|
|
68
|
+
and work through the CLI or Vibe64's Preview pane.
|
|
69
|
+
|
|
66
70
|
Genesis requires Node.js 22 or newer and Git.
|
|
67
71
|
|
|
68
72
|
Install the framework-neutral compiler and the optional first-party technology
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Project opening and templates
|
|
2
|
+
|
|
3
|
+
`genesis init` installs the portable Genesis documents, workflow skills, and
|
|
4
|
+
agent integration in an ordinary Git repository. It preserves existing source.
|
|
5
|
+
Initialization is a decision to use Genesis; it is not evidence that an existing
|
|
6
|
+
application has been described or that an application has been created.
|
|
7
|
+
|
|
8
|
+
`genesis inspect project --json` is a read-only opening inspection shared by
|
|
9
|
+
the CLI, session guidance, and hosts such as Vibe64:
|
|
10
|
+
|
|
11
|
+
| State | Evidence | Next step |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `new` | No content beyond known bootstrap files | Choose a template or create through conversation |
|
|
14
|
+
| `adoption` | Existing content with no Genesis, or initialized documents that still lack product/Stack description | Ask what the project does and what should run; work backwards from existing code into its documents |
|
|
15
|
+
| `ready` | Current format, valid core declarations and any present Program, existing source, product and Stack description | Continue ordinary work |
|
|
16
|
+
| `attention` | Unversioned/old/invalid/newer Genesis, malformed declarations, or broken Program source references | Use the specific diagnostic and `nextAction` for migration, repair, or a compiler update |
|
|
17
|
+
|
|
18
|
+
`genesis/version` is the format sentinel. Existing Genesis documents without it
|
|
19
|
+
are an unversioned migration case, never an empty project. The supported format
|
|
20
|
+
is independent of the npm package version. A Stack description can use selected
|
|
21
|
+
components or authored project contracts when no catalogue technology matches.
|
|
22
|
+
|
|
23
|
+
The exact bootstrap allowlist is `GENESIS_BOOTSTRAP_PATHS` in
|
|
24
|
+
`src/index/project-inspection.js`: the four core Markdown documents and version,
|
|
25
|
+
the two derived Cities, Genesis's Codex/OpenCode integration files, its managed
|
|
26
|
+
skills manifest, and the SKILL.md/openai.yaml pair for each of the three Genesis
|
|
27
|
+
workflow skills. Whole `.agents/`, `.codex/`, or `genesis/` directories are not
|
|
28
|
+
exempt. A README, `.gitignore`, application manifest, unfamiliar source file, or
|
|
29
|
+
custom agent tool counts as existing content. Ignored files also count, except
|
|
30
|
+
installed `node_modules` dependencies. Source recognition does not depend on a
|
|
31
|
+
language detector or an AI model.
|
|
32
|
+
|
|
33
|
+
Opening a session does not run verification, rebuild the source index, install
|
|
34
|
+
dependencies, prepare a database, or start the application. The inspection is
|
|
35
|
+
repeated as a small read when opening/resuming a session and when a host refreshes
|
|
36
|
+
its setup view. There is no persistent “ready” flag that can misclassify a later
|
|
37
|
+
Git import. Program source references are structural evidence; prose accuracy
|
|
38
|
+
still requires review against the implementation during relevant work.
|
|
39
|
+
|
|
40
|
+
## Choose a starter
|
|
41
|
+
|
|
42
|
+
Install the optional catalogue alongside the compiler:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
npm install --global genesis-compiler genesis-stack
|
|
46
|
+
git init
|
|
47
|
+
genesis init
|
|
48
|
+
genesis templates list
|
|
49
|
+
genesis templates apply official:jskit/public
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The first-party JSKIT repository is
|
|
53
|
+
[vibe64-dev/seed-jskit](https://github.com/vibe64-dev/seed-jskit). `public` supplies
|
|
54
|
+
an adaptive public app; `accounts` adds local accounts, private Home and MySQL.
|
|
55
|
+
Each branch is a complete, current Genesis project with actual application
|
|
56
|
+
source, dependency lockfile, setup/run contracts and focused checks.
|
|
57
|
+
|
|
58
|
+
Applying fetches the explicitly named branch once, resolves its commit and reads
|
|
59
|
+
its ordinary Git blobs into a temporary project. Genesis rejects links,
|
|
60
|
+
submodules, unsafe paths, dependencies and private dotenv files in the template.
|
|
61
|
+
It validates the prepared project before changing the destination. A project
|
|
62
|
+
lock serializes competing imports; the source and authored bootstrap files are
|
|
63
|
+
checked again immediately before the copy. An ordinary copy failure rolls back
|
|
64
|
+
written files. If a process is forcibly killed, inspect any partial work and the
|
|
65
|
+
Git-local `genesis-template.lock` before removing the abandoned lock and retrying;
|
|
66
|
+
Genesis never treats partial application code as an empty destination.
|
|
67
|
+
|
|
68
|
+
The destination keeps its Git history, remotes, branch, meaningful Blueprint,
|
|
69
|
+
collaboration/engineering choices, and authored Stack operation sections.
|
|
70
|
+
Template Stack components/packages are combined with existing selections;
|
|
71
|
+
existing complete operation sections take precedence. No dependency command,
|
|
72
|
+
database command, agent, or Git commit runs during import. The result reports
|
|
73
|
+
the resolved source repository, branch and commit; no provenance ledger is
|
|
74
|
+
written to the project. Review the ordinary diff and commit or use Vibe64 Save.
|
|
75
|
+
|
|
76
|
+
The application declares its actual operations in `genesis/stack.md`. CLI users
|
|
77
|
+
provide the declared runtimes and resources and execute those commands themselves
|
|
78
|
+
or with their agent. Vibe64 can provide runtimes, resource bindings, workspace
|
|
79
|
+
preparation and preview routing through its existing host facilities.
|
|
80
|
+
|
|
81
|
+
## Supply catalogues
|
|
82
|
+
|
|
83
|
+
A catalogue repository has `genesis.templates.json` at its branch root:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"schemaVersion": 1,
|
|
88
|
+
"templates": [{
|
|
89
|
+
"id": "jskit/public",
|
|
90
|
+
"technology": "jskit",
|
|
91
|
+
"name": "Company JSKIT app",
|
|
92
|
+
"description": "The company's public application starter.",
|
|
93
|
+
"repository": "https://github.com/example/seed-jskit.git",
|
|
94
|
+
"branch": "public"
|
|
95
|
+
}]
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Add explicit sources to either command, repeating the option for more sources:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
genesis templates list --template-source company=https://github.com/example/starters.git#main
|
|
103
|
+
genesis templates apply company:jskit/public --template-source company=https://github.com/example/starters.git#main
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Repositories use HTTPS; explicit local absolute paths are also accepted for
|
|
107
|
+
offline use and testing. The catalogue's technology matches a selected Stack
|
|
108
|
+
component in its template. Skills do not trigger template selection: one complete
|
|
109
|
+
template may select several components such as JSKIT, Node and MySQL.
|
|
110
|
+
|
|
111
|
+
An installed Stack package may ship the same file and declare
|
|
112
|
+
`"genesis": { "templates": { "namespace": "company", "path": "genesis.templates.json" } }`
|
|
113
|
+
in its package.json, alongside `stackPieces`. Project-recorded Stack packages
|
|
114
|
+
and explicit `--stack-package` packages contribute their catalogue data.
|
|
115
|
+
Namespaces must be unique. Competing providers remain separate choices, such as
|
|
116
|
+
`official:jskit/public` and `company:jskit/public`. An ambiguous unqualified ID
|
|
117
|
+
is rejected; source order never chooses a winner.
|
|
118
|
+
|
|
119
|
+
The JavaScript API exposes `inspectProject`, `listTemplates`, and `applyTemplate`.
|
|
120
|
+
Catalogue options accept `stackPackages` and `templateSources`; explicit source
|
|
121
|
+
records contain `namespace`, `repository`, and `branch`. Hosts configure those
|
|
122
|
+
sources themselves and accept only a selected template ID from their browser.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genesis-compiler",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "An agent-independent prompt, multi-language code-index, cleanup, and verification companion with project agent guidance.",
|
|
6
6
|
"repository": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"bin",
|
|
27
27
|
"docs/assurance-model.md",
|
|
28
28
|
"docs/stack-components.md",
|
|
29
|
+
"docs/templates.md",
|
|
29
30
|
"docs/prompt-integration.md",
|
|
30
31
|
"prompts/blueprint.txt",
|
|
31
32
|
"prompts/adopt.txt",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genesis",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Makes Codex aware of optional Genesis adoption for existing projects.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mobily Enterprises"
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"longDescription": "Recommends the optional Genesis explanatory layer when Codex opens an existing project that has not adopted it.",
|
|
12
12
|
"developerName": "Mobily Enterprises",
|
|
13
13
|
"category": "Productivity",
|
|
14
|
-
"capabilities": [
|
|
14
|
+
"capabilities": [
|
|
15
|
+
"Read"
|
|
16
|
+
],
|
|
15
17
|
"defaultPrompt": [
|
|
16
18
|
"Adopt Genesis in this existing project"
|
|
17
19
|
]
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
This is an existing
|
|
2
|
-
|
|
3
|
-
or
|
|
1
|
+
This is an existing application that has not yet been described and configured
|
|
2
|
+
in Genesis. It may already have initialized but empty Genesis documents. Never
|
|
3
|
+
offer a template or replace its source. Ask what this project does and which
|
|
4
|
+
application, command, or output the person wants to run. Use answers already
|
|
5
|
+
given, and accept "inspect it for me" without requiring a technology choice.
|
|
6
|
+
Ask about ambiguity such as multiple applications only after inspecting evidence.
|
|
7
|
+
Recommend preparing the project for guided editing before
|
|
4
8
|
substantial work. Explain in product language that preparation preserves the
|
|
5
9
|
implementation while importing its current product, setup, run, data, preview,
|
|
6
10
|
and verification facts. Do not require the person to know Genesis terminology or
|
|
7
|
-
type a command.
|
|
8
|
-
|
|
11
|
+
type a command. When genesis.initialized is true, they have already opted into
|
|
12
|
+
Genesis; run the Genesis `adopt` operation and follow its prompt without requesting
|
|
13
|
+
that approval again. Otherwise, run it when they approve (including an explicit
|
|
14
|
+
request to set up this existing project). If they decline, continue normally and
|
|
15
|
+
do not repeat the recommendation.
|
|
16
|
+
Opening a session never requires tests, builds, dependency installation, database
|
|
17
|
+
preparation, or application verification.
|
package/prompts/start-new.txt
CHANGED
|
@@ -3,6 +3,13 @@ what is being built, who or what will use or invoke it, and the first observable
|
|
|
3
3
|
useful outcome. Ask at most three concise questions at a time, and do not repeat
|
|
4
4
|
facts already supplied by the user. A Stack choice is not product intent.
|
|
5
5
|
|
|
6
|
+
Offer a ready-made starting point through `genesis templates list` when helpful.
|
|
7
|
+
On an explicit choice, use `genesis templates apply <catalogue:technology/variant>`.
|
|
8
|
+
Each choice is one complete repository branch, not a template per selected Skill.
|
|
9
|
+
If multiple catalogues match, ask the user to choose a qualified identifier; never
|
|
10
|
+
guess a source. Applying a template preserves authored project intent and Git
|
|
11
|
+
history. Existing application source must go through adoption, never templates.
|
|
12
|
+
|
|
6
13
|
Use `availableStackPieces` to offer only relevant technology choices in product
|
|
7
14
|
language. Never silently select one. If the user names an unselected technology,
|
|
8
15
|
run the Genesis `stack list` operation first. For one exact match, ask whether to
|
package/src/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { parseArgs } from 'node:util';
|
|
|
4
4
|
import {
|
|
5
5
|
addStack,
|
|
6
6
|
adoptProject,
|
|
7
|
+
applyTemplate,
|
|
7
8
|
check,
|
|
8
9
|
generatePrompt,
|
|
9
10
|
getContext,
|
|
@@ -13,9 +14,11 @@ import {
|
|
|
13
14
|
inspectEngineering,
|
|
14
15
|
inspectEnvironment,
|
|
15
16
|
inspectStackSection,
|
|
17
|
+
inspectProject,
|
|
16
18
|
installCodex,
|
|
17
19
|
listEngineeringProfiles,
|
|
18
20
|
listStackPieces,
|
|
21
|
+
listTemplates,
|
|
19
22
|
migrate,
|
|
20
23
|
setCollaboration,
|
|
21
24
|
setEngineeringProfile,
|
|
@@ -51,10 +54,13 @@ const USAGE = `Usage:
|
|
|
51
54
|
genesis engineering set <profile>
|
|
52
55
|
genesis stack list
|
|
53
56
|
genesis stack add <piece...>
|
|
57
|
+
genesis templates list
|
|
58
|
+
genesis templates apply <catalogue:technology/variant>
|
|
54
59
|
genesis context <path...>
|
|
55
60
|
genesis index [function-or-path...]
|
|
56
61
|
genesis migrate
|
|
57
62
|
genesis inspect environment
|
|
63
|
+
genesis inspect project
|
|
58
64
|
genesis inspect section <name>
|
|
59
65
|
genesis prompt [request...]
|
|
60
66
|
genesis prompt --task <start|adopt|work|deslop|program|blueprint|describe|review> [request...]
|
|
@@ -64,6 +70,7 @@ const USAGE = `Usage:
|
|
|
64
70
|
Options:
|
|
65
71
|
--project-root <path> Set the project root (default: current directory)
|
|
66
72
|
--stack-package <name> Add an installed external Stack package (repeatable)
|
|
73
|
+
--template-source <namespace=repository[#branch]> Add a template catalogue (repeatable)
|
|
67
74
|
--task <task> Select the prompt task (default: work)
|
|
68
75
|
--json Emit one machine-readable result
|
|
69
76
|
-h, --help Show this help
|
|
@@ -73,7 +80,7 @@ prompt to the agent you already use. Review all edits through the ordinary Git
|
|
|
73
80
|
diff, then run genesis verify for the Stack's concrete checks.
|
|
74
81
|
`;
|
|
75
82
|
|
|
76
|
-
const COMMANDS = new Set(['adopt', 'check', 'codex', 'collaboration', 'context', 'engineering', 'hook', 'index', 'init', 'inspect', 'migrate', 'prompt', 'stack', 'verify']);
|
|
83
|
+
const COMMANDS = new Set(['adopt', 'check', 'codex', 'collaboration', 'context', 'engineering', 'hook', 'index', 'init', 'inspect', 'migrate', 'prompt', 'stack', 'templates', 'verify']);
|
|
77
84
|
|
|
78
85
|
function parseCommand(argv) {
|
|
79
86
|
if (argv.length === 0 || argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') {
|
|
@@ -91,6 +98,7 @@ function parseCommand(argv) {
|
|
|
91
98
|
json: { type: 'boolean', default: false },
|
|
92
99
|
'project-root': { type: 'string' },
|
|
93
100
|
'stack-package': { type: 'string', multiple: true, default: [] },
|
|
101
|
+
'template-source': { type: 'string', multiple: true, default: [] },
|
|
94
102
|
task: { type: 'string' },
|
|
95
103
|
},
|
|
96
104
|
});
|
|
@@ -101,13 +109,27 @@ function parseCommand(argv) {
|
|
|
101
109
|
json: parsed.values.json,
|
|
102
110
|
projectRoot: parsed.values['project-root'],
|
|
103
111
|
stackPackages: parsed.values['stack-package'],
|
|
112
|
+
templateSources: parsed.values['template-source'].map((source) => {
|
|
113
|
+
const separator = source.indexOf('=');
|
|
114
|
+
if (separator < 1) fail('CLI_TEMPLATE_SOURCE_INVALID', 'Use --template-source namespace=repository[#branch].');
|
|
115
|
+
const repositoryRef = source.slice(separator + 1);
|
|
116
|
+
const hash = repositoryRef.lastIndexOf('#');
|
|
117
|
+
return { namespace: source.slice(0, separator), repository: hash < 0 ? repositoryRef : repositoryRef.slice(0, hash), branch: hash < 0 ? 'main' : repositoryRef.slice(hash + 1) };
|
|
118
|
+
}),
|
|
104
119
|
task: parsed.values.task,
|
|
105
120
|
};
|
|
106
121
|
const operands = parsed.positionals;
|
|
122
|
+
if (options.templateSources.length && command !== 'templates') {
|
|
123
|
+
fail('CLI_OPTION_NOT_APPLICABLE', `Option --template-source is not applicable to ${command}.`);
|
|
124
|
+
}
|
|
107
125
|
if (options.task !== undefined && command !== 'prompt') {
|
|
108
126
|
fail('CLI_OPTION_NOT_APPLICABLE', `Option --task is not applicable to ${command}.`);
|
|
109
127
|
}
|
|
110
|
-
if (command === '
|
|
128
|
+
if (command === 'templates') {
|
|
129
|
+
if (!(operands[0] === 'list' && operands.length === 1) && !(operands[0] === 'apply' && operands.length === 2)) {
|
|
130
|
+
fail('CLI_TEMPLATE_ACTION_REQUIRED', 'Use templates list or templates apply <catalogue:technology/variant>.');
|
|
131
|
+
}
|
|
132
|
+
} else if (command === 'collaboration') {
|
|
111
133
|
const [action] = operands;
|
|
112
134
|
if (!['show', 'set'].includes(action)) {
|
|
113
135
|
fail('CLI_COLLABORATION_ACTION_REQUIRED', 'Command collaboration requires show or set.');
|
|
@@ -152,12 +174,12 @@ function parseCommand(argv) {
|
|
|
152
174
|
} else if (command === 'context' && operands.length === 0) {
|
|
153
175
|
fail('CONTEXT_PATH_REQUIRED', 'Command context requires at least one project path.');
|
|
154
176
|
} else if (command === 'inspect') {
|
|
155
|
-
const ordinaryInspection = operands.length === 1 && operands[0]
|
|
177
|
+
const ordinaryInspection = operands.length === 1 && ['environment', 'project'].includes(operands[0]);
|
|
156
178
|
const sectionInspection = operands.length >= 2 && operands[0] === 'section';
|
|
157
179
|
if (!ordinaryInspection && !sectionInspection) {
|
|
158
180
|
fail(
|
|
159
181
|
'CLI_INSPECT_TARGET_REQUIRED',
|
|
160
|
-
'Command inspect requires environment or section <name>.',
|
|
182
|
+
'Command inspect requires project, environment, or section <name>.',
|
|
161
183
|
);
|
|
162
184
|
}
|
|
163
185
|
} else if (command === 'hook' && (operands.length !== 1 || !['discover', 'session', 'turn'].includes(operands[0]))) {
|
|
@@ -256,6 +278,21 @@ function writeInspection(result) {
|
|
|
256
278
|
}
|
|
257
279
|
|
|
258
280
|
function writeResult(command, result) {
|
|
281
|
+
if (command === 'templates') {
|
|
282
|
+
if (result.templates) for (const entry of result.templates) line(process.stdout, `${entry.id}: ${entry.name} — ${entry.description}`);
|
|
283
|
+
else {
|
|
284
|
+
line(process.stdout, `Applied ${result.template.id} from ${result.source.repository} (${result.source.branch} at ${result.source.revision}).`);
|
|
285
|
+
namedItems('Changed files', result.changedFiles);
|
|
286
|
+
line(process.stdout, result.guidance);
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (command === 'inspect' && result.inspection === 'project') {
|
|
291
|
+
line(process.stdout, `Project: ${result.state}; source: ${result.content.kind}; Genesis: ${result.projectFormat.status}`);
|
|
292
|
+
line(process.stdout, `Next action: ${result.nextAction}. Templates: ${result.templateEligible ? 'available' : 'not applicable'}.`);
|
|
293
|
+
for (const diagnostic of result.diagnostics) line(process.stdout, `${diagnostic.code}: ${diagnostic.message}`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
259
296
|
if (command === 'prompt') {
|
|
260
297
|
process.stdout.write(result.prompt.endsWith('\n') ? result.prompt : `${result.prompt}\n`);
|
|
261
298
|
return;
|
|
@@ -377,6 +414,10 @@ async function execute({ command, operands, options }, { signal } = {}) {
|
|
|
377
414
|
const projectRoot = options.projectRoot || process.cwd();
|
|
378
415
|
const stackPackages = await cliStackPackages(projectRoot, options.stackPackages || []);
|
|
379
416
|
if (command === 'init') return initialize({ projectRoot, stackPackages });
|
|
417
|
+
if (command === 'templates') {
|
|
418
|
+
const templateOptions = { projectRoot, stackPackages, templateSources: options.templateSources };
|
|
419
|
+
return operands[0] === 'list' ? listTemplates(templateOptions) : applyTemplate({ ...templateOptions, templateId: operands[1] });
|
|
420
|
+
}
|
|
380
421
|
if (command === 'migrate') return migrate({ projectRoot, stackPackages });
|
|
381
422
|
if (command === 'adopt') {
|
|
382
423
|
return adoptProject({ projectRoot, request: operands.join(' '), stackPackages });
|
|
@@ -455,7 +496,7 @@ async function execute({ command, operands, options }, { signal } = {}) {
|
|
|
455
496
|
}),
|
|
456
497
|
};
|
|
457
498
|
}
|
|
458
|
-
const inspections = { environment: inspectEnvironment };
|
|
499
|
+
const inspections = { environment: inspectEnvironment, project: inspectProject };
|
|
459
500
|
return {
|
|
460
501
|
inspection: operands[0],
|
|
461
502
|
...await inspections[operands[0]]({ projectRoot, stackPackages }),
|
package/src/index/codex-hooks.js
CHANGED
|
@@ -4,7 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { GenesisError } from './errors.js';
|
|
5
5
|
import { gitContext } from './git.js';
|
|
6
6
|
import { classifyProjectKind } from './project-files.js';
|
|
7
|
-
import {
|
|
7
|
+
import { writeFileAtomic } from './utils.js';
|
|
8
|
+
import { inspectProjectFormatAtRoot } from './project-format.js';
|
|
8
9
|
|
|
9
10
|
const HOOKS_PATH = '.codex/hooks.json';
|
|
10
11
|
const LEGACY_GENESIS_HOOKS_DESCRIPTION = 'Genesis project hooks.';
|
|
@@ -18,7 +19,7 @@ function hookCommand(action) {
|
|
|
18
19
|
const SESSION_HOOK = {
|
|
19
20
|
event: 'SessionStart',
|
|
20
21
|
group: {
|
|
21
|
-
matcher: '^(startup|clear|compact)$',
|
|
22
|
+
matcher: '^(startup|resume|clear|compact)$',
|
|
22
23
|
hooks: [{
|
|
23
24
|
type: 'command',
|
|
24
25
|
command: hookCommand('session'),
|
|
@@ -107,7 +108,7 @@ export async function installCodexHooks({ projectRoot } = {}) {
|
|
|
107
108
|
export async function codexAdoptionRecommendation({ projectRoot = process.cwd() } = {}) {
|
|
108
109
|
let root;
|
|
109
110
|
try { root = (await gitContext(projectRoot)).repositoryRoot; } catch { return { status: 'not-applicable', output: '' }; }
|
|
110
|
-
if ((await
|
|
111
|
+
if ((await inspectProjectFormatAtRoot(root)).status !== 'uninitialized') {
|
|
111
112
|
return { status: 'not-applicable', output: '' };
|
|
112
113
|
}
|
|
113
114
|
if (await classifyProjectKind({ projectRoot: root }) === 'new') {
|
package/src/index/contracts.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export const GENESIS_CONTRACTS = Object.freeze({
|
|
2
|
+
projectInspection: 'genesis.project-inspection.v1',
|
|
3
|
+
templates: 'genesis.templates.v1',
|
|
4
|
+
templateApplication: 'genesis.template-application.v1',
|
|
2
5
|
collaboration: 'genesis.collaboration.v1',
|
|
3
6
|
derivedArtifacts: 'genesis.derived-artifacts.v1',
|
|
4
7
|
engineering: 'genesis.engineering.v1',
|
package/src/index/process.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
|
-
import { isProjectContentPath, OPENCODE_PLUGIN_PATH } from './paths.js';
|
|
4
3
|
import { runGit } from './process.js';
|
|
5
4
|
import { pathState } from './utils.js';
|
|
5
|
+
import { inspectProjectContent } from './project-inspection.js';
|
|
6
6
|
|
|
7
7
|
async function visiblePaths(projectRoot) {
|
|
8
8
|
const [visible, deleted] = await Promise.all([
|
|
@@ -25,12 +25,7 @@ export async function gitVisibleFileStates(projectRoot, { includePath = () => tr
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** Classifies opening behavior from cheap Git-visible paths and selected Stack state. */
|
|
28
|
-
export async function classifyProjectKind({ projectRoot
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
isProjectContentPath(file)
|
|
32
|
-
&& file !== OPENCODE_PLUGIN_PATH
|
|
33
|
-
&& !file.split('/').includes('node_modules')
|
|
34
|
-
));
|
|
35
|
-
return existing ? 'existing' : 'new';
|
|
28
|
+
export async function classifyProjectKind({ projectRoot } = {}) {
|
|
29
|
+
const content = await inspectProjectContent(projectRoot);
|
|
30
|
+
return content.kind === 'existing' ? 'existing' : 'new';
|
|
36
31
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { readBlueprint } from './blueprint.js';
|
|
5
|
+
import { readCollaboration } from './collaboration.js';
|
|
6
|
+
import { GENESIS_CONTRACTS } from './contracts.js';
|
|
7
|
+
import { readEngineering } from './engineering.js';
|
|
8
|
+
import { asDiagnostic } from './errors.js';
|
|
9
|
+
import { gitContext } from './git.js';
|
|
10
|
+
import { inspectProjectFormatAtRoot, projectFormatDiagnostic } from './project-format.js';
|
|
11
|
+
import { inspectProgram } from './program.js';
|
|
12
|
+
import { runGit } from './process.js';
|
|
13
|
+
import { readStack } from './stack.js';
|
|
14
|
+
|
|
15
|
+
// These are initialization outputs, not whole directories that may contain user code.
|
|
16
|
+
export const GENESIS_BOOTSTRAP_PATHS = Object.freeze([
|
|
17
|
+
'genesis/version',
|
|
18
|
+
'genesis/blueprint.md',
|
|
19
|
+
'genesis/stack.md',
|
|
20
|
+
'genesis/collaboration.md',
|
|
21
|
+
'genesis/engineering.md',
|
|
22
|
+
'.genesis/machine-city.json',
|
|
23
|
+
'.genesis/program-city.json',
|
|
24
|
+
'.codex/hooks.json',
|
|
25
|
+
'.opencode/plugins/genesis-project-guidance.js',
|
|
26
|
+
'.agents/skills/.genesis-managed.json',
|
|
27
|
+
...['genesis-project', 'genesis-program', 'genesis-deslop'].flatMap((name) => [
|
|
28
|
+
`.agents/skills/${name}/SKILL.md`,
|
|
29
|
+
`.agents/skills/${name}/agents/openai.yaml`,
|
|
30
|
+
]),
|
|
31
|
+
]);
|
|
32
|
+
const bootstrapPaths = new Set(GENESIS_BOOTSTRAP_PATHS);
|
|
33
|
+
|
|
34
|
+
export async function inspectProjectContent(projectRoot) {
|
|
35
|
+
const [visible, deleted, ignored] = await Promise.all([
|
|
36
|
+
runGit(projectRoot, ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.']),
|
|
37
|
+
runGit(projectRoot, ['ls-files', '-z', '--deleted', '--', '.']),
|
|
38
|
+
runGit(projectRoot, ['ls-files', '-z', '--others', '--ignored', '--exclude-standard', '--', '.']),
|
|
39
|
+
]);
|
|
40
|
+
const absent = new Set(deleted.stdout.toString('utf8').split('\0').filter(Boolean));
|
|
41
|
+
const paths = [...new Set([visible, ignored].flatMap(({ stdout }) => stdout.toString('utf8').split('\0').filter(Boolean)))]
|
|
42
|
+
.filter((file) => !absent.has(file) && file !== '.git' && !file.startsWith('.git/'))
|
|
43
|
+
.sort();
|
|
44
|
+
const existingPaths = paths.filter((file) => !bootstrapPaths.has(file) && !file.split('/').includes('node_modules'));
|
|
45
|
+
return {
|
|
46
|
+
kind: existingPaths.length ? 'existing' : paths.length ? 'bootstrap' : 'empty',
|
|
47
|
+
existingPaths,
|
|
48
|
+
paths,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Read-only opening inspection: no index generation, verification, or application execution. */
|
|
53
|
+
export async function inspectProject({ projectRoot, stackPackages = [] } = {}) {
|
|
54
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
55
|
+
const [content, format] = await Promise.all([
|
|
56
|
+
inspectProjectContent(root),
|
|
57
|
+
inspectProjectFormatAtRoot(root),
|
|
58
|
+
]);
|
|
59
|
+
const result = {
|
|
60
|
+
contract: GENESIS_CONTRACTS.projectInspection,
|
|
61
|
+
state: 'attention',
|
|
62
|
+
content: { kind: content.kind, count: content.existingPaths.length },
|
|
63
|
+
projectFormat: format,
|
|
64
|
+
templateEligible: false,
|
|
65
|
+
stackComponents: [],
|
|
66
|
+
diagnostics: [],
|
|
67
|
+
nextAction: 'repair',
|
|
68
|
+
};
|
|
69
|
+
if (!['uninitialized', 'current'].includes(format.status)) {
|
|
70
|
+
result.nextAction = format.action;
|
|
71
|
+
result.diagnostics = [projectFormatDiagnostic(format)];
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
if (format.status === 'uninitialized') {
|
|
75
|
+
return { ...result,
|
|
76
|
+
state: content.kind === 'existing' ? 'adoption' : 'new',
|
|
77
|
+
templateEligible: content.kind !== 'existing',
|
|
78
|
+
nextAction: content.kind === 'existing' ? 'adopt' : 'init',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let blueprintEmpty = false;
|
|
83
|
+
try {
|
|
84
|
+
const blueprint = await readBlueprint(root, { required: true });
|
|
85
|
+
blueprintEmpty = !blueprint.description;
|
|
86
|
+
} catch (error) { result.diagnostics.push(asDiagnostic(error)); }
|
|
87
|
+
let stack;
|
|
88
|
+
try {
|
|
89
|
+
// Missing Stack must not be hidden by readStack's optional default.
|
|
90
|
+
await readFile(path.join(root, 'genesis/stack.md'), 'utf8');
|
|
91
|
+
stack = await readStack(root, { stackPackages });
|
|
92
|
+
result.stackComponents = stack.components.map(({ id }) => id);
|
|
93
|
+
} catch (error) { result.diagnostics.push(asDiagnostic(error)); }
|
|
94
|
+
for (const read of [readCollaboration, readEngineering]) {
|
|
95
|
+
try { await read(root); } catch (error) { result.diagnostics.push(asDiagnostic(error)); }
|
|
96
|
+
}
|
|
97
|
+
try { await inspectProgram(root); } catch (error) { result.diagnostics.push(asDiagnostic(error)); }
|
|
98
|
+
if (result.diagnostics.length) return result;
|
|
99
|
+
if (content.kind !== 'existing') {
|
|
100
|
+
return { ...result, state: 'new', templateEligible: true, nextAction: 'create' };
|
|
101
|
+
}
|
|
102
|
+
const stackDescribed = stack.components.length > 0 || stack.projectContracts.some(({ lines }) => lines.some((line) => line.trim()));
|
|
103
|
+
if (blueprintEmpty || !stackDescribed) {
|
|
104
|
+
return { ...result, state: 'adoption', nextAction: 'adopt' };
|
|
105
|
+
}
|
|
106
|
+
return { ...result, state: 'ready', nextAction: 'work' };
|
|
107
|
+
}
|
package/src/index/prompt.js
CHANGED
|
@@ -20,6 +20,7 @@ import { withStackEnvironmentDefaults } from './stack-environment-defaults.js';
|
|
|
20
20
|
import { stableJson } from './utils.js';
|
|
21
21
|
import { classifyProjectKind } from './project-files.js';
|
|
22
22
|
import { inspectProjectFormatAtRoot } from './project-format.js';
|
|
23
|
+
import { inspectProject } from './project-inspection.js';
|
|
23
24
|
|
|
24
25
|
const TASKS = new Set(['start', 'adopt', 'work', 'deslop', 'program', 'blueprint', 'describe', 'review']);
|
|
25
26
|
const DEFAULT_REQUEST = {
|
|
@@ -284,6 +285,7 @@ async function generateExplanationPrompt({ instructions, program, request, root,
|
|
|
284
285
|
}
|
|
285
286
|
|
|
286
287
|
async function generateStartPrompt({
|
|
288
|
+
inspection,
|
|
287
289
|
hiddenStackPieces,
|
|
288
290
|
instructions,
|
|
289
291
|
program,
|
|
@@ -292,6 +294,20 @@ async function generateStartPrompt({
|
|
|
292
294
|
sessionPrompt,
|
|
293
295
|
stackPackages,
|
|
294
296
|
}) {
|
|
297
|
+
inspection ??= await inspectProject({ projectRoot: root, stackPackages });
|
|
298
|
+
if (inspection.state === 'adoption') {
|
|
299
|
+
return {
|
|
300
|
+
status: 'ready', task: 'start',
|
|
301
|
+
prompt: renderPrompt({
|
|
302
|
+
instructions: await startInstructions(instructions, 'existing-uninitialized'), request,
|
|
303
|
+
context: { task: 'start', projectRoot: root, projectKind: 'existing-uninitialized', inspection,
|
|
304
|
+
genesis: { initialized: inspection.projectFormat.status === 'current' }, ...sessionPrompt.context },
|
|
305
|
+
collaborationGuidance: sessionPrompt.collaborationGuidance,
|
|
306
|
+
engineeringGuidance: sessionPrompt.engineeringGuidance,
|
|
307
|
+
}),
|
|
308
|
+
warnings: inspection.diagnostics, verificationCommands: [],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
295
311
|
let blueprint;
|
|
296
312
|
try {
|
|
297
313
|
blueprint = await readBlueprint(root, { required: true });
|
|
@@ -384,6 +400,15 @@ export async function generateProjectPrompt({
|
|
|
384
400
|
}
|
|
385
401
|
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
386
402
|
const userRequest = requestText(request, task);
|
|
403
|
+
const openingInspection = task === 'start' ? await inspectProject({ projectRoot: root, stackPackages }) : null;
|
|
404
|
+
if (openingInspection) {
|
|
405
|
+
const inspection = openingInspection;
|
|
406
|
+
if (inspection.state === 'attention') {
|
|
407
|
+
return { status: 'ready', task: 'start',
|
|
408
|
+
prompt: `This existing Genesis project needs attention. Preserve source and history; never offer a seed. Explain the reported issue and the exact next action. Do not run application verification on startup.\n\n${JSON.stringify(inspection, null, 2)}\n\nUSER REQUEST\n${userRequest}\n`,
|
|
409
|
+
warnings: inspection.diagnostics, verificationCommands: [] };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
387
412
|
const [instructions, collaboration, engineering] = await Promise.all([
|
|
388
413
|
readInstalledAsset(task),
|
|
389
414
|
collaborationForPrompt(root),
|
|
@@ -426,6 +451,7 @@ export async function generateProjectPrompt({
|
|
|
426
451
|
const program = await observeProgram(root);
|
|
427
452
|
if (task === 'start') {
|
|
428
453
|
return generateStartPrompt({
|
|
454
|
+
inspection: openingInspection,
|
|
429
455
|
instructions,
|
|
430
456
|
hiddenStackPieces,
|
|
431
457
|
program,
|
|
@@ -3,6 +3,7 @@ import { GENESIS_CONTRACTS } from './contracts.js';
|
|
|
3
3
|
import { readEngineering, readEngineeringBaseline } from './engineering.js';
|
|
4
4
|
import { GenesisError } from './errors.js';
|
|
5
5
|
import { gitContext } from './git.js';
|
|
6
|
+
import { inspectProject } from './project-inspection.js';
|
|
6
7
|
import { readStack } from './stack.js';
|
|
7
8
|
import { listStackCatalogPieces } from './stack-catalog.js';
|
|
8
9
|
import { normalizeSource, sha256, stableJson } from './utils.js';
|
|
@@ -109,11 +110,12 @@ export async function projectSessionContext({
|
|
|
109
110
|
stackPackages = [],
|
|
110
111
|
} = {}) {
|
|
111
112
|
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
112
|
-
const [stack, collaboration, engineering, hostContext] = await Promise.all([
|
|
113
|
+
const [stack, collaboration, engineering, hostContext, inspection] = await Promise.all([
|
|
113
114
|
optionalStack(root, stackPackages),
|
|
114
115
|
optionalCollaboration(root),
|
|
115
116
|
optionalEngineering(root),
|
|
116
117
|
hostContextContribution({ hostDriver, hostDriverInput, scope: 'session' }),
|
|
118
|
+
inspectProject({ projectRoot: root, stackPackages }),
|
|
117
119
|
]);
|
|
118
120
|
const availableComponents = await optionalStackComponentIds(root, stack, stackPackages);
|
|
119
121
|
const selected = stack?.components.map(({ id }) => id) || [];
|
|
@@ -122,6 +124,15 @@ export async function projectSessionContext({
|
|
|
122
124
|
: 'unavailable; run the Genesis `check` operation';
|
|
123
125
|
const output = [
|
|
124
126
|
'This is a Genesis-enriched project.',
|
|
127
|
+
`Project opening state: ${inspection.state}.`,
|
|
128
|
+
...(inspection.state === 'new' ? [
|
|
129
|
+
'- This directory contains only bootstrap files. Offer a ready-made template (`genesis templates list`) or creation through conversation. Apply one explicitly selected template to this empty project; a selected Stack alone does not mean source exists.',
|
|
130
|
+
] : inspection.state === 'adoption' ? [
|
|
131
|
+
'- Existing project content needs description and configuration. Ask what this project does and what the user wants to run, using answers already supplied. Inspect its implementation to work backwards into Blueprint, Stack and Program. Do not offer seeds or replace existing source/history. Run `genesis adopt` and follow its prompt; initialized Genesis files already express the user’s decision to use Genesis.',
|
|
132
|
+
] : inspection.state === 'attention' ? [
|
|
133
|
+
`- Genesis needs attention: ${inspection.diagnostics.map(({ message }) => message).join(' ')} Next action: ${inspection.nextAction}. Preserve source and explain the specific issue before the relevant migration or repair. Do not offer seeds.`,
|
|
134
|
+
] : []),
|
|
135
|
+
'- Opening or resuming a session only reads project metadata and content paths. Do not run verification, tests, builds, dependency installation, or database preparation just because a session started. Run relevant checks when the requested work calls for them.',
|
|
125
136
|
'- Read `genesis/blueprint.md`, `genesis/collaboration.md`, `genesis/engineering.md`, and `genesis/stack.md` for product intent, collaboration approach, engineering approach, and selected technology.',
|
|
126
137
|
'- For a new project whose Blueprint does not yet establish product direction, do not research technology or create source until the user has made clear what is being built, who or what will use or invoke it, and the first observable useful outcome. Ask only unresolved high-impact questions. A reply confirms only what it explicitly answers; Stack confirmation is not product intent.',
|
|
127
138
|
'- Once product direction is clear, choose one smallest implementation path through selected technology guidance, or authoritative technology documentation when the catalog has no match, and read only what that path requires. Do not survey alternatives, clone whole technology repositories, inspect unrelated package internals, or delegate research unless one concrete failure requires one exact investigation.',
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { GenesisError } from './errors.js';
|
|
6
|
+
import { GENESIS_CONTRACTS } from './contracts.js';
|
|
7
|
+
import { readGitSnapshot } from './template-source.js';
|
|
8
|
+
import { readStack } from './stack.js';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const NAME = /^[a-z][a-z0-9-]*$/u;
|
|
12
|
+
const ID = /^[a-z][a-z0-9-]*\/[a-z][a-z0-9-]*$/u;
|
|
13
|
+
|
|
14
|
+
function templateCatalogError(message) {
|
|
15
|
+
return new GenesisError('TEMPLATE_CATALOG_INVALID', message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseTemplateCatalog(value, namespace) {
|
|
19
|
+
if (!NAME.test(namespace) || value?.schemaVersion !== 1 || !Array.isArray(value.templates)) {
|
|
20
|
+
throw templateCatalogError('A template catalogue requires a namespace, schemaVersion 1, and templates.');
|
|
21
|
+
}
|
|
22
|
+
const ids = new Set();
|
|
23
|
+
return value.templates.map((entry) => {
|
|
24
|
+
if (!ID.test(entry?.id) || ids.has(entry.id)
|
|
25
|
+
|| typeof entry.name !== 'string' || !entry.name.trim()
|
|
26
|
+
|| typeof entry.repository !== 'string' || !entry.repository
|
|
27
|
+
|| typeof entry.branch !== 'string' || !entry.branch
|
|
28
|
+
|| entry.technology !== entry.id.split('/')[0]) {
|
|
29
|
+
throw templateCatalogError(`Invalid or duplicate template in ${namespace}: ${entry?.id || '(missing id)'}.`);
|
|
30
|
+
}
|
|
31
|
+
ids.add(entry.id);
|
|
32
|
+
return {
|
|
33
|
+
id: `${namespace}:${entry.id}`,
|
|
34
|
+
namespace,
|
|
35
|
+
technology: entry.technology,
|
|
36
|
+
variant: entry.id.split('/')[1],
|
|
37
|
+
name: entry.name.trim(),
|
|
38
|
+
description: typeof entry.description === 'string' ? entry.description.trim() : '',
|
|
39
|
+
repository: entry.repository,
|
|
40
|
+
branch: entry.branch,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function installedCatalogs(stackPackages, projectRoot) {
|
|
46
|
+
const sources = [];
|
|
47
|
+
for (const packageName of stackPackages) {
|
|
48
|
+
let manifestPath;
|
|
49
|
+
try {
|
|
50
|
+
manifestPath = require.resolve(`${packageName}/package.json`, { paths: [projectRoot, ...require.resolve.paths(packageName)] });
|
|
51
|
+
} catch { throw templateCatalogError(`Configured Stack package is not installed: ${packageName}.`); }
|
|
52
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
53
|
+
const declaration = manifest.genesis?.templates;
|
|
54
|
+
if (!declaration) continue;
|
|
55
|
+
if (!NAME.test(declaration.namespace) || declaration.path !== 'genesis.templates.json') {
|
|
56
|
+
throw templateCatalogError(`Invalid template declaration in ${packageName}.`);
|
|
57
|
+
}
|
|
58
|
+
sources.push({
|
|
59
|
+
namespace: declaration.namespace,
|
|
60
|
+
catalog: JSON.parse(await readFile(path.join(path.dirname(manifestPath), declaration.path), 'utf8')),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return sources;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Catalogues are explicit data sources. A Stack or Skill never implicitly installs a template. */
|
|
67
|
+
export async function listTemplates({ projectRoot = process.cwd(), stackPackages = [], templateSources = [] } = {}) {
|
|
68
|
+
let recorded = [];
|
|
69
|
+
try { recorded = (await readStack(projectRoot, { stackPackages })).stackPackages; } catch { /* Explicit sources also work before init. */ }
|
|
70
|
+
stackPackages = [...new Set([...recorded, ...stackPackages])];
|
|
71
|
+
const sources = [...await installedCatalogs(stackPackages, projectRoot), ...templateSources];
|
|
72
|
+
const namespaces = new Set();
|
|
73
|
+
const templates = [];
|
|
74
|
+
for (const source of sources) {
|
|
75
|
+
if (!NAME.test(source.namespace) || namespaces.has(source.namespace)) {
|
|
76
|
+
throw templateCatalogError(`Duplicate or invalid template source namespace: ${source.namespace}.`);
|
|
77
|
+
}
|
|
78
|
+
namespaces.add(source.namespace);
|
|
79
|
+
let catalog = source.catalog;
|
|
80
|
+
if (!catalog) {
|
|
81
|
+
const snapshot = await readGitSnapshot({ repository: source.repository, branch: source.branch || 'main' });
|
|
82
|
+
const file = snapshot.files.find(({ path: name }) => name === 'genesis.templates.json');
|
|
83
|
+
if (!file) throw templateCatalogError(`${source.namespace} does not contain genesis.templates.json.`);
|
|
84
|
+
try { catalog = JSON.parse(file.contents.toString('utf8')); }
|
|
85
|
+
catch { throw templateCatalogError(`${source.namespace}/genesis.templates.json is not valid JSON.`); }
|
|
86
|
+
}
|
|
87
|
+
templates.push(...parseTemplateCatalog(catalog, source.namespace));
|
|
88
|
+
}
|
|
89
|
+
return { contract: GENESIS_CONTRACTS.templates, templates };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function resolveTemplate(templates, id) {
|
|
93
|
+
const matches = templates.filter((entry) => entry.id === id || (!id.includes(':') && entry.id.split(':')[1] === id));
|
|
94
|
+
if (matches.length !== 1) {
|
|
95
|
+
throw new GenesisError(matches.length ? 'TEMPLATE_AMBIGUOUS' : 'TEMPLATE_NOT_FOUND',
|
|
96
|
+
matches.length ? `Choose a catalogue-qualified template: ${matches.map(({ id: name }) => name).join(', ')}.`
|
|
97
|
+
: `No configured template matches ${id}.`, { matches: matches.map(({ id: name }) => name) });
|
|
98
|
+
}
|
|
99
|
+
return matches[0];
|
|
100
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, rm, rmdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseBlueprintSource } from './blueprint.js';
|
|
5
|
+
import { buildProjectIndex } from './code-index.js';
|
|
6
|
+
import { GenesisError } from './errors.js';
|
|
7
|
+
import { GENESIS_CONTRACTS } from './contracts.js';
|
|
8
|
+
import { gitContext } from './git.js';
|
|
9
|
+
import { initializeProject } from './init.js';
|
|
10
|
+
import { GENESIS_BOOTSTRAP_PATHS, inspectProject } from './project-inspection.js';
|
|
11
|
+
import { runGit, runGitText } from './process.js';
|
|
12
|
+
import { listTemplates, resolveTemplate } from './template-catalog.js';
|
|
13
|
+
import { readGitSnapshot } from './template-source.js';
|
|
14
|
+
import { readStack } from './stack.js';
|
|
15
|
+
import { writeFileAtomic } from './utils.js';
|
|
16
|
+
|
|
17
|
+
function sections(source) {
|
|
18
|
+
const result = new Map();
|
|
19
|
+
let name = '';
|
|
20
|
+
let fence = false;
|
|
21
|
+
for (const line of source.split('\n')) {
|
|
22
|
+
if (/^```/u.test(line)) fence = !fence;
|
|
23
|
+
const heading = !fence && line.match(/^## (.+)$/u);
|
|
24
|
+
if (heading) { name = heading[1]; result.set(name, []); }
|
|
25
|
+
else if (name) result.get(name).push(line);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Existing authored sections remain whole. The template contributes only missing sections.
|
|
31
|
+
function mergeTemplateStack(templateSource, existingSource) {
|
|
32
|
+
const template = sections(templateSource);
|
|
33
|
+
const existing = sections(existingSource);
|
|
34
|
+
for (const [name, lines] of existing) {
|
|
35
|
+
if (['Components', 'Stack packages'].includes(name)) {
|
|
36
|
+
template.set(name, [...new Set([...(template.get(name) || []), ...lines].map((line) => line.trim()).filter(Boolean))]);
|
|
37
|
+
} else template.set(name, lines);
|
|
38
|
+
}
|
|
39
|
+
return `# Stack\n\n${[...template].map(([name, lines]) => `## ${name}\n\n${lines.join('\n').trim()}`).join('\n\n')}\n`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function existingFile(root, name) {
|
|
43
|
+
const location = path.join(root, name);
|
|
44
|
+
try {
|
|
45
|
+
const state = await lstat(location);
|
|
46
|
+
if (!state.isFile()) throw new GenesisError('TEMPLATE_DESTINATION_CONFLICT', `Template destination is not an ordinary file: ${name}.`);
|
|
47
|
+
return await readFile(location);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error.code === 'ENOENT') return null;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function filesBelow(root, relative = '') {
|
|
55
|
+
const files = [];
|
|
56
|
+
for (const entry of await readdir(path.join(root, relative), { withFileTypes: true })) {
|
|
57
|
+
if (!relative && entry.name === '.git') continue;
|
|
58
|
+
const name = relative ? `${relative}/${entry.name}` : entry.name;
|
|
59
|
+
if (entry.isDirectory()) files.push(...await filesBelow(root, name));
|
|
60
|
+
else if (entry.isFile()) files.push({ path: name, contents: await readFile(path.join(root, name)), mode: (await lstat(path.join(root, name))).mode & 0o111 ? 0o777 : 0o666 });
|
|
61
|
+
else throw new GenesisError('TEMPLATE_TREE_INVALID', `Template contains a non-ordinary file: ${name}.`);
|
|
62
|
+
}
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function assertSeedable(root, stackPackages) {
|
|
67
|
+
const inspection = await inspectProject({ projectRoot: root, stackPackages });
|
|
68
|
+
if (!inspection.templateEligible) {
|
|
69
|
+
throw new GenesisError('TEMPLATE_PROJECT_NOT_EMPTY', 'This project contains existing content or Genesis needs attention. Its source was preserved.', { inspection });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Apply one complete source tree without changing Git history or running application commands. */
|
|
74
|
+
export async function applyTemplate({ projectRoot, templateId, stackPackages = [], templateSources = [] } = {}) {
|
|
75
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
76
|
+
const lockPath = path.resolve(root, await runGitText(root, ['rev-parse', '--git-path', 'genesis-template.lock']));
|
|
77
|
+
try { await mkdir(lockPath); }
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error.code === 'EEXIST') throw new GenesisError('TEMPLATE_PROJECT_BUSY', 'Another template operation holds this project lock.');
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
let stagingRoot;
|
|
83
|
+
try {
|
|
84
|
+
await assertSeedable(root, stackPackages);
|
|
85
|
+
const { templates } = await listTemplates({ projectRoot: root, stackPackages, templateSources });
|
|
86
|
+
const template = resolveTemplate(templates, String(templateId || ''));
|
|
87
|
+
const preserved = new Map();
|
|
88
|
+
for (const name of GENESIS_BOOTSTRAP_PATHS) preserved.set(name, await existingFile(root, name));
|
|
89
|
+
const snapshot = await readGitSnapshot(template);
|
|
90
|
+
stagingRoot = await mkdtemp(path.join(os.tmpdir(), 'genesis-template-project-'));
|
|
91
|
+
await runGit(stagingRoot, ['init', '--quiet']);
|
|
92
|
+
for (const file of snapshot.files) {
|
|
93
|
+
if (file.path.split('/').includes('node_modules') || file.path.split('/').some((part) => part === '.env' || part.startsWith('.env.') && part !== '.env.example')) {
|
|
94
|
+
throw new GenesisError('TEMPLATE_TREE_INVALID', `Template contains private environment or installed dependencies: ${file.path}.`);
|
|
95
|
+
}
|
|
96
|
+
await mkdir(path.dirname(path.join(stagingRoot, file.path)), { recursive: true });
|
|
97
|
+
await writeFile(path.join(stagingRoot, file.path), file.contents, { flag: 'wx', mode: file.mode });
|
|
98
|
+
}
|
|
99
|
+
const stackSource = await existingFile(stagingRoot, 'genesis/stack.md');
|
|
100
|
+
if (!stackSource) throw new GenesisError('TEMPLATE_STACK_REQUIRED', 'A template must contain its complete genesis/stack.md.');
|
|
101
|
+
const seedStack = await readStack(stagingRoot, { stackPackages });
|
|
102
|
+
if (!seedStack.components.some(({ id }) => id === template.technology)) {
|
|
103
|
+
throw new GenesisError('TEMPLATE_STACK_MISMATCH', 'The template Stack does not contain its advertised technology.');
|
|
104
|
+
}
|
|
105
|
+
for (const [name, contents] of preserved) {
|
|
106
|
+
if (contents === null || name.startsWith('.genesis/')) continue;
|
|
107
|
+
if (name === 'genesis/blueprint.md' && !parseBlueprintSource(contents.toString('utf8')).description) continue;
|
|
108
|
+
let value = contents;
|
|
109
|
+
if (name === 'genesis/stack.md') value = mergeTemplateStack(stackSource.toString('utf8'), contents.toString('utf8'));
|
|
110
|
+
await mkdir(path.dirname(path.join(stagingRoot, name)), { recursive: true });
|
|
111
|
+
await writeFile(path.join(stagingRoot, name), value);
|
|
112
|
+
}
|
|
113
|
+
await initializeProject({ projectRoot: stagingRoot, stackPackages });
|
|
114
|
+
await buildProjectIndex({ projectRoot: stagingRoot, stackPackages });
|
|
115
|
+
const prepared = await inspectProject({ projectRoot: stagingRoot, stackPackages });
|
|
116
|
+
if (prepared.state !== 'ready') throw new GenesisError('TEMPLATE_PROJECT_INVALID', 'The prepared template does not contain a usable Genesis project.', { inspection: prepared });
|
|
117
|
+
await assertSeedable(root, stackPackages);
|
|
118
|
+
for (const [name, contents] of preserved) {
|
|
119
|
+
const current = await existingFile(root, name);
|
|
120
|
+
if ((contents === null) !== (current === null) || contents && !contents.equals(current)) {
|
|
121
|
+
throw new GenesisError('TEMPLATE_DESTINATION_CHANGED', 'Project context changed while the template was being prepared. Nothing was applied.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const files = await filesBelow(stagingRoot);
|
|
125
|
+
const applied = [];
|
|
126
|
+
try {
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const previous = await existingFile(root, file.path);
|
|
129
|
+
if (previous && previous.equals(file.contents)) continue;
|
|
130
|
+
if (previous && !preserved.has(file.path)) throw new GenesisError('TEMPLATE_DESTINATION_CHANGED', `Existing project content was preserved: ${file.path}.`);
|
|
131
|
+
await mkdir(path.dirname(path.join(root, file.path)), { recursive: true });
|
|
132
|
+
if (previous) await writeFileAtomic(path.join(root, file.path), file.contents);
|
|
133
|
+
else await writeFile(path.join(root, file.path), file.contents, { flag: 'wx', mode: file.mode });
|
|
134
|
+
applied.push({ path: file.path, previous });
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
for (const file of applied.reverse()) {
|
|
138
|
+
if (file.previous) await writeFileAtomic(path.join(root, file.path), file.previous);
|
|
139
|
+
else await rm(path.join(root, file.path));
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
return { contract: GENESIS_CONTRACTS.templateApplication, status: 'applied', template,
|
|
144
|
+
source: { repository: snapshot.repository, branch: snapshot.branch, revision: snapshot.revision },
|
|
145
|
+
changedFiles: applied.map(({ path: name }) => name).sort(),
|
|
146
|
+
guidance: 'The starting application is ready for its declared workspace setup. No application commands or Git commits were run.',
|
|
147
|
+
};
|
|
148
|
+
} finally {
|
|
149
|
+
if (stagingRoot) await rm(stagingRoot, { recursive: true, force: true });
|
|
150
|
+
await rmdir(lockPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { GenesisError } from './errors.js';
|
|
5
|
+
import { runGit, runGitText } from './process.js';
|
|
6
|
+
|
|
7
|
+
function validateRepository(repository) {
|
|
8
|
+
if (typeof repository !== 'string' || !repository || repository.startsWith('-')) {
|
|
9
|
+
throw new GenesisError('TEMPLATE_SOURCE_INVALID', 'A template repository is required.');
|
|
10
|
+
}
|
|
11
|
+
if (path.isAbsolute(repository)) return;
|
|
12
|
+
let url;
|
|
13
|
+
try { url = new URL(repository); } catch { /* diagnosed below */ }
|
|
14
|
+
if (!url || url.protocol !== 'https:' || url.username || url.password || url.hash || url.search) {
|
|
15
|
+
throw new GenesisError('TEMPLATE_SOURCE_INVALID', 'Template sources must be HTTPS repositories or explicit local absolute paths.');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Fetch a branch once and read ordinary blobs; never execute or check out remote hooks/filters. */
|
|
20
|
+
export async function readGitSnapshot({ repository, branch }) {
|
|
21
|
+
validateRepository(repository);
|
|
22
|
+
if (typeof branch !== 'string' || !branch || branch.startsWith('-')) {
|
|
23
|
+
throw new GenesisError('TEMPLATE_SOURCE_INVALID', 'A template branch is required.');
|
|
24
|
+
}
|
|
25
|
+
const temporaryRoot = await mkdtemp(path.join(os.tmpdir(), 'genesis-template-source-'));
|
|
26
|
+
try {
|
|
27
|
+
await runGit(temporaryRoot, ['init', '--bare', '--quiet']);
|
|
28
|
+
await runGit(temporaryRoot, ['check-ref-format', `refs/heads/${branch}`]);
|
|
29
|
+
await runGit(temporaryRoot, ['fetch', '--quiet', '--depth=1', '--no-tags', '--', repository, `refs/heads/${branch}`], { timeoutMs: 120_000 });
|
|
30
|
+
const revision = await runGitText(temporaryRoot, ['rev-parse', 'FETCH_HEAD^{commit}']);
|
|
31
|
+
const tree = await runGit(temporaryRoot, ['ls-tree', '-rz', '--full-tree', revision]);
|
|
32
|
+
const files = [];
|
|
33
|
+
let bytes = 0;
|
|
34
|
+
for (const item of tree.stdout.toString('utf8').split('\0').filter(Boolean)) {
|
|
35
|
+
const separator = item.indexOf('\t');
|
|
36
|
+
const [mode, type, object] = item.slice(0, separator).split(' ');
|
|
37
|
+
const name = item.slice(separator + 1);
|
|
38
|
+
if (!['100644', '100755'].includes(mode) || type !== 'blob' || path.isAbsolute(name)
|
|
39
|
+
|| name.includes('\\') || name.split('/').some((part) => !part || ['.', '..', '.git'].includes(part.toLowerCase()))) {
|
|
40
|
+
throw new GenesisError('TEMPLATE_TREE_INVALID', `Template contains an unsupported path or file: ${name}.`);
|
|
41
|
+
}
|
|
42
|
+
const blob = await runGit(temporaryRoot, ['cat-file', 'blob', object]);
|
|
43
|
+
bytes += blob.stdout.length;
|
|
44
|
+
if (bytes > 64 * 1024 * 1024 || files.length >= 10_000) {
|
|
45
|
+
throw new GenesisError('TEMPLATE_TREE_TOO_LARGE', 'Template exceeds the source size limit.');
|
|
46
|
+
}
|
|
47
|
+
files.push({ path: name, contents: blob.stdout, mode: mode === '100755' ? 0o777 : 0o666 });
|
|
48
|
+
}
|
|
49
|
+
return { repository, branch, revision, files };
|
|
50
|
+
} finally { await rm(temporaryRoot, { recursive: true, force: true }); }
|
|
51
|
+
}
|
package/src/index.js
CHANGED
|
@@ -22,9 +22,14 @@ import { inspectProjectStackSection } from './index/stack-section-inspection.js'
|
|
|
22
22
|
import { listStackCatalogPieces } from './index/stack-catalog.js';
|
|
23
23
|
import { addStackPieces, readStack } from './index/stack.js';
|
|
24
24
|
import { uniqueSorted } from './index/utils.js';
|
|
25
|
+
import { inspectProject } from './index/project-inspection.js';
|
|
25
26
|
import { verifyProject } from './index/verification.js';
|
|
26
27
|
import { projectSessionContext, projectTurnContext } from './index/session-context.js';
|
|
27
28
|
import { withTrustedGitRepository } from './index/process.js';
|
|
29
|
+
|
|
30
|
+
export { inspectProject };
|
|
31
|
+
export { listTemplates } from './index/template-catalog.js';
|
|
32
|
+
export { applyTemplate } from './index/template-project.js';
|
|
28
33
|
import {
|
|
29
34
|
HOST_CONTEXT_RESOLVER_DATA_ENV,
|
|
30
35
|
HOST_CONTEXT_RESOLVER_ENV,
|
|
@@ -59,8 +64,14 @@ async function initializeWithIndex(projectRoot, stackPackages = []) {
|
|
|
59
64
|
return withIndexResult(initialized, index);
|
|
60
65
|
}
|
|
61
66
|
|
|
62
|
-
export function initialize({ projectRoot = process.cwd(), stackPackages = [] } = {}) {
|
|
63
|
-
|
|
67
|
+
export async function initialize({ projectRoot = process.cwd(), stackPackages = [] } = {}) {
|
|
68
|
+
const initialized = await initializeWithIndex(projectRoot, stackPackages);
|
|
69
|
+
const inspection = await inspectProject({ projectRoot, stackPackages });
|
|
70
|
+
return { ...initialized, inspection, guidance: `${initialized.guidance}\n${inspection.state === 'adoption'
|
|
71
|
+
? 'Existing application detected. Initialization has not documented it: open your agent to describe the project and choose what to run. Do not seed this repository.'
|
|
72
|
+
: inspection.state === 'new'
|
|
73
|
+
? 'Ready for a new project. Open your agent or list ready-made starting points with genesis templates list.'
|
|
74
|
+
: 'Open your agent to continue. Session startup does not run application verification.'}` };
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
export async function migrate({ projectRoot = process.cwd(), stackPackages = [] } = {}) {
|