genesis-compiler 1.7.3 → 1.7.5
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 +8 -7
- package/docs/prompt-integration.md +35 -15
- package/package.json +1 -1
- package/src/cli.js +24 -3
- package/src/index/agent-integrations.js +24 -0
- package/src/index/agent-skills.js +4 -0
- package/src/index/claude-hooks.js +104 -0
- package/src/index/codex-hooks.js +13 -7
- package/src/index/hook-command.js +4 -0
- package/src/index/init.js +7 -10
- package/src/index/project-inspection.js +7 -3
- package/src/index/session-context.js +2 -1
- package/src/index.js +2 -0
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ genesis/
|
|
|
34
34
|
genesis-* Genesis workflow skills
|
|
35
35
|
<technology>/ authoritative skills installed by selected Stack pieces
|
|
36
36
|
.codex/hooks.json project-local Codex lifecycle integration
|
|
37
|
+
.claude/settings.json project-local Claude Code lifecycle integration
|
|
37
38
|
.opencode/plugins/ project-local OpenCode lifecycle integration
|
|
38
39
|
.genesis/
|
|
39
40
|
machine-city.json derived detailed source and function map
|
|
@@ -82,7 +83,7 @@ The catalog is static Markdown and contains the complete curated set of
|
|
|
82
83
|
language, database, UI, and framework pieces. A host may supply another catalog
|
|
83
84
|
through the same contract.
|
|
84
85
|
|
|
85
|
-
### Try a new project with Codex or OpenCode
|
|
86
|
+
### Try a new project with Codex, Claude Code or OpenCode
|
|
86
87
|
|
|
87
88
|
Create an ordinary empty Git repository:
|
|
88
89
|
|
|
@@ -93,7 +94,7 @@ git init
|
|
|
93
94
|
genesis init
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
Start Codex or OpenCode in that directory. For a useful discovery test, begin
|
|
97
|
+
Start Codex, Claude Code or OpenCode in that directory. For a useful discovery test, begin
|
|
97
98
|
with a deliberately incomplete request:
|
|
98
99
|
|
|
99
100
|
> I want to build something with JSKIT.
|
|
@@ -180,11 +181,11 @@ empty or already-adopted project.
|
|
|
180
181
|
|
|
181
182
|
`genesis init` creates an empty Blueprint, a focused engineering approach, an
|
|
182
183
|
empty optional Stack, the current `genesis/version`, three project Agent Skills,
|
|
183
|
-
and project-local guidance adapters for Codex and OpenCode. The skills are ordinary
|
|
184
|
+
and project-local guidance adapters for Codex, Claude Code and OpenCode. The skills are ordinary
|
|
184
185
|
[Agent Skills](https://agentskills.io): `genesis-project`, `genesis-program`,
|
|
185
186
|
and `genesis-deslop`, each with standard `SKILL.md` and `agents/openai.yaml`
|
|
186
187
|
metadata. Open `/hooks` once in Codex to review and trust the guidance hook;
|
|
187
|
-
OpenCode discovers its local project plugin automatically.
|
|
188
|
+
Claude Code loads its project hooks and OpenCode discovers its local project plugin automatically. All three use the same
|
|
188
189
|
Genesis-owned operating guide for new sessions and after compaction. When the
|
|
189
190
|
optional first-party catalog is installed beside Genesis, that guide includes
|
|
190
191
|
only its current component ids. If a named technology exactly matches an
|
|
@@ -236,7 +237,7 @@ files alone does not refresh its existing context.
|
|
|
236
237
|
|
|
237
238
|
| Command | What it does |
|
|
238
239
|
| --- | --- |
|
|
239
|
-
| `genesis init` | Initializes Genesis files, workflow skills, code indexes, and local Codex/OpenCode guidance in a new or current-format project. It selects no technology. |
|
|
240
|
+
| `genesis init` | Initializes Genesis files, workflow skills, code indexes, and local Codex/Claude Code/OpenCode guidance in a new or current-format project. It selects no technology. |
|
|
240
241
|
| `genesis adopt [guidance...]` | Initializes an existing codebase and prints the prompt for describing its real product and operational contracts without moving or modernizing source. |
|
|
241
242
|
| `genesis codex install` | Installs the optional Codex discovery plugin that recommends adoption for nonempty projects without Genesis. |
|
|
242
243
|
| `genesis engineering list` | Lists the installed engineering profiles. |
|
|
@@ -310,7 +311,7 @@ conversation is created or refreshed. They may then generate task prompts with
|
|
|
310
311
|
`sessionContextInstalled: true`, preventing the stable Collaboration and
|
|
311
312
|
Engineering guidance from being repeated in each task. A host can supply one
|
|
312
313
|
explicit session/turn driver; Genesis performs no driver discovery and labels
|
|
313
|
-
turn output untrusted. If the project-local Codex or OpenCode adapter runs in a
|
|
314
|
+
turn output untrusted. If the project-local Codex, Claude Code or OpenCode adapter runs in a
|
|
314
315
|
separate process, the host may set `GENESIS_HOST_CONTEXT_RESOLVER` to one
|
|
315
316
|
executable and optionally supply opaque JSON in
|
|
316
317
|
`GENESIS_HOST_CONTEXT_RESOLVER_DATA`. Session and turn hooks pass only the
|
|
@@ -461,7 +462,7 @@ Python, Java, C#, C/C++, PHP, Go, Rust, Ruby, Kotlin, and Shell. A future techno
|
|
|
461
462
|
adds its adapter through its Stack piece rather than teaching Genesis prompts
|
|
462
463
|
to parse that language.
|
|
463
464
|
|
|
464
|
-
The project Codex
|
|
465
|
+
The project Codex and Claude Code hooks and OpenCode plugin use the same short operating guide
|
|
465
466
|
for a new session and after compaction. They record no conversation message,
|
|
466
467
|
block no completion, and request no follow-up turn. Implementation prompts
|
|
467
468
|
compose selected Stack `Post-change` guidance into the original implementation
|
|
@@ -61,7 +61,7 @@ native placement. The driver never receives or rewrites `userMessage`, and
|
|
|
61
61
|
Genesis supports only the one explicitly supplied driver—there is no discovery
|
|
62
62
|
or plugin registry.
|
|
63
63
|
|
|
64
|
-
When the project-local Codex or OpenCode adapter runs in a separate process,
|
|
64
|
+
When the project-local Codex, Claude Code or OpenCode adapter runs in a separate process,
|
|
65
65
|
the host can configure that same one-driver boundary without adding another
|
|
66
66
|
prompt compositor:
|
|
67
67
|
|
|
@@ -82,12 +82,12 @@ JSON on stdin:
|
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
For `genesis hook turn`, the input has the same shape with `scope: "turn"`.
|
|
85
|
-
The executable returns only the requested host contribution as text. Codex
|
|
85
|
+
The executable returns only the requested host contribution as text. Codex and Claude Code
|
|
86
86
|
hook input and OpenCode's transform callbacks supply the native session id;
|
|
87
87
|
Genesis extracts that id but does not understand the host registry or the
|
|
88
|
-
provider's prompt-placement rules. The Codex prompt
|
|
88
|
+
provider's prompt-placement rules. The Codex and Claude prompt hooks and OpenCode message
|
|
89
89
|
transform place a nonempty turn result separately from authored user text. If
|
|
90
|
-
no resolver is configured,
|
|
90
|
+
no resolver is configured, all turn paths are empty and the adapters render
|
|
91
91
|
ordinary Genesis session context exactly as before.
|
|
92
92
|
|
|
93
93
|
A host that has no turn contribution can set `GENESIS_TURN_CONTEXT_ENABLED=0`.
|
|
@@ -108,13 +108,15 @@ in `GENESIS_HOST_CONTEXT_INPUT` for host command shims. The resolver still
|
|
|
108
108
|
receives the actual child's `providerSessionId`; Genesis does not interpret a
|
|
109
109
|
host's ownership rules.
|
|
110
110
|
|
|
111
|
-
After updating Genesis, hosts can inspect
|
|
112
|
-
`
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
After updating Genesis, hosts can inspect all generated integrations with
|
|
112
|
+
`inspectAgentIntegrations({ projectRoot })` and refresh them with
|
|
113
|
+
`syncAgentIntegrations({ projectRoot })` under ordinary source-write admission.
|
|
114
|
+
These finite operations cover Codex hooks, Claude settings and the OpenCode
|
|
115
|
+
plugin. Synchronization preserves unrelated settings and hooks and returns
|
|
116
|
+
`status` and `changedFiles`; it does not migrate authored project contracts.
|
|
117
|
+
The individual OpenCode inspection/sync exports remain available to callers
|
|
118
|
+
that own only that integration. A running provider picks up changes through its
|
|
119
|
+
native reload lifecycle. `genesis init` and migration install all three adapters.
|
|
118
120
|
|
|
119
121
|
Tasks are `start`, `adopt`, `work`, `deslop`, `program`, `blueprint`, `describe`, and `review`.
|
|
120
122
|
`start` is the host-independent first conversation: it classifies an initialized
|
|
@@ -191,11 +193,29 @@ does not perform adoption automatically. If the user accepts, Codex runs the
|
|
|
191
193
|
command itself and follows its returned prompt in the same conversation. Other
|
|
192
194
|
hosts can call `adoptProject()` and send its `prompt` directly.
|
|
193
195
|
|
|
194
|
-
Codex and OpenCode can use the project-local adapters installed by `genesis
|
|
195
|
-
init`.
|
|
196
|
-
compaction. Codex
|
|
196
|
+
Codex, Claude Code and OpenCode can use the project-local adapters installed by `genesis
|
|
197
|
+
init`. All receive the same short operating guide for a new session and after
|
|
198
|
+
compaction. The Codex session hook sets `additionalContextLimit: 0` so native
|
|
199
|
+
truncation cannot remove part of the bounded guide or host contribution.
|
|
200
|
+
Codex and Claude Code use `SessionStart` for startup, resume, clear, and compact events.
|
|
201
|
+
Claude's generated session hooks use `genesis hook session --claude-part <1-4>`.
|
|
202
|
+
Claude caps each hook output at 10,000 characters, so four hooks deliver the
|
|
203
|
+
unchanged guide in numbered parts below that limit. Empty parts emit nothing.
|
|
204
|
+
A guide above 36,000 characters delivers an explicit guidance-unavailable
|
|
205
|
+
warning instead of a partial guide. Claude does not enforce startup hook stop
|
|
206
|
+
requests; this warning instructs the model to report the problem before work,
|
|
207
|
+
and is not a runtime admission control. Turn guidance is already bounded at 512
|
|
208
|
+
bytes and uses the ordinary turn command. This is provider transport only;
|
|
209
|
+
composition and the host bridge remain shared.
|
|
210
|
+
Claude settings preserve unrelated hooks and configuration. The common operating
|
|
211
|
+
guide lists installed project skills and their canonical `.agents/skills/` paths;
|
|
212
|
+
an agent reads the relevant skill before following its workflow. Genesis exposes
|
|
213
|
+
the canonical skill directories to Claude through relative `.claude/skills/`
|
|
214
|
+
links, so native skill commands use the same files. Skills synchronization adds
|
|
215
|
+
new links and removes only its exact dangling links; existing native Claude
|
|
216
|
+
skills are preserved and reported as overrides.
|
|
197
217
|
OpenCode renders once per session, keeps that value active in its stateless
|
|
198
|
-
model requests, and rerenders after its `session.compacted` event.
|
|
218
|
+
model requests, and rerenders after its `session.compacted` event. No
|
|
199
219
|
adapter records a user message, authorizes edits, intercepts completion, or
|
|
200
220
|
requests another model turn. Selected `Post-change` contributions are included
|
|
201
221
|
in the original implementation prompt, whose project skill also keeps
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -88,6 +88,7 @@ Options:
|
|
|
88
88
|
--directory <path> Parser cache directory (parsers commands only)
|
|
89
89
|
--all Select every parser (parsers install/verify only)
|
|
90
90
|
--json Emit one machine-readable result
|
|
91
|
+
--claude-part <1-4> Emit one bounded Claude session-hook part
|
|
91
92
|
-h, --help Show this help
|
|
92
93
|
|
|
93
94
|
Genesis generates prompts; it never starts or manages an AI agent. Give the
|
|
@@ -111,6 +112,7 @@ function parseCommand(argv) {
|
|
|
111
112
|
strict: true,
|
|
112
113
|
options: {
|
|
113
114
|
json: { type: 'boolean', default: false },
|
|
115
|
+
'claude-part': { type: 'string' },
|
|
114
116
|
all: { type: 'boolean', default: false },
|
|
115
117
|
directory: { type: 'string' },
|
|
116
118
|
'project-root': { type: 'string' },
|
|
@@ -124,6 +126,7 @@ function parseCommand(argv) {
|
|
|
124
126
|
}
|
|
125
127
|
const options = {
|
|
126
128
|
json: parsed.values.json,
|
|
129
|
+
claudePart: parsed.values['claude-part'],
|
|
127
130
|
all: parsed.values.all,
|
|
128
131
|
directory: parsed.values.directory,
|
|
129
132
|
projectRoot: parsed.values['project-root'],
|
|
@@ -138,6 +141,9 @@ function parseCommand(argv) {
|
|
|
138
141
|
task: parsed.values.task,
|
|
139
142
|
};
|
|
140
143
|
const operands = parsed.positionals;
|
|
144
|
+
if (options.claudePart !== undefined && (command !== 'hook' || operands[0] !== 'session' || !/^[1-4]$/u.test(options.claudePart) || options.json)) {
|
|
145
|
+
fail('CLI_OPTION_NOT_APPLICABLE', 'Option --claude-part requires hook session, a part from 1 to 4, and no --json.');
|
|
146
|
+
}
|
|
141
147
|
if (command !== 'parsers' && (options.all || options.directory !== undefined)) {
|
|
142
148
|
fail('CLI_OPTION_NOT_APPLICABLE', 'Options --all and --directory apply only to parsers commands.');
|
|
143
149
|
}
|
|
@@ -665,9 +671,24 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
665
671
|
projectFormat,
|
|
666
672
|
formatDiagnostic,
|
|
667
673
|
);
|
|
668
|
-
if (options.
|
|
669
|
-
|
|
670
|
-
|
|
674
|
+
if (options.claudePart !== undefined) {
|
|
675
|
+
// Claude spills each hook output over 10,000 characters to a file.
|
|
676
|
+
// Four native hooks deliver the exact text in bounded parts instead.
|
|
677
|
+
if (result.output.length > 36000) {
|
|
678
|
+
if (options.claudePart === '1') line(process.stdout,
|
|
679
|
+
'GENESIS GUIDANCE UNAVAILABLE: The session guide exceeds the Claude hook capacity of 36,000 characters. The guide was not delivered. Tell the user before doing project work. Shorten the project guidance and restart the conversation to load it completely.');
|
|
680
|
+
return 0;
|
|
681
|
+
}
|
|
682
|
+
let start = (Number(options.claudePart) - 1) * 9000;
|
|
683
|
+
let end = start + 9000;
|
|
684
|
+
// Keep UTF-16 surrogate pairs together at the shared part boundaries.
|
|
685
|
+
if (/[\uDC00-\uDFFF]/u.test(result.output[start] || '')) start--;
|
|
686
|
+
if (/[\uDC00-\uDFFF]/u.test(result.output[end] || '')) end--;
|
|
687
|
+
const part = result.output.slice(start, end);
|
|
688
|
+
if (part) process.stdout.write(`Genesis session guidance, part ${options.claudePart} of ${Math.ceil(result.output.length / 9000)} (read in numeric order):\n${part}`);
|
|
689
|
+
} else if (options.json) line(process.stdout, JSON.stringify(result));
|
|
690
|
+
else writeResult(parsed.command, result);
|
|
691
|
+
if (!options.json) {
|
|
671
692
|
for (const warning of result.warnings || []) {
|
|
672
693
|
if (warning.code !== formatDiagnostic?.code) line(process.stderr, `WARNING: ${warning.code}: ${warning.message}`);
|
|
673
694
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { inspectClaudeHooks, installClaudeHooks } from './claude-hooks.js';
|
|
2
|
+
import { inspectCodexHooks, installCodexHooks } from './codex-hooks.js';
|
|
3
|
+
import { inspectOpenCodePlugin, installOpenCodePlugin } from './opencode-plugin.js';
|
|
4
|
+
|
|
5
|
+
export async function inspectAgentIntegrations(options = {}) {
|
|
6
|
+
const [codex, claude, opencode] = await Promise.all([
|
|
7
|
+
inspectCodexHooks(options), inspectClaudeHooks(options), inspectOpenCodePlugin(options),
|
|
8
|
+
]);
|
|
9
|
+
const integrations = { codex, claude, opencode };
|
|
10
|
+
const states = Object.values(integrations).map(({ status }) => status);
|
|
11
|
+
return {
|
|
12
|
+
status: states.includes('missing') ? 'missing' : states.includes('outdated') ? 'outdated' : 'current',
|
|
13
|
+
integrations,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function syncAgentIntegrations(options = {}) {
|
|
18
|
+
const results = await Promise.all([
|
|
19
|
+
installCodexHooks(options), installClaudeHooks(options), installOpenCodePlugin(options),
|
|
20
|
+
]);
|
|
21
|
+
const changedFiles = results.flatMap(({ changedFiles }) => changedFiles).sort();
|
|
22
|
+
const warnings = results.flatMap((result) => result.warnings || []);
|
|
23
|
+
return { status: changedFiles.length ? 'updated' : 'unchanged', changedFiles, ...(warnings.length ? { warnings } : {}) };
|
|
24
|
+
}
|
|
@@ -16,6 +16,7 @@ import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
|
16
16
|
import { parse as parseYaml } from 'yaml';
|
|
17
17
|
|
|
18
18
|
import { asDiagnostic, GenesisError } from './errors.js';
|
|
19
|
+
import { claudeSkillLinks } from './claude-hooks.js';
|
|
19
20
|
import { sha256, stableJson, writeFileAtomic } from './utils.js';
|
|
20
21
|
|
|
21
22
|
const packageRoot = fileURLToPath(new URL('../../', import.meta.url));
|
|
@@ -482,6 +483,9 @@ export async function syncProjectSkills({ projectRoot, stack } = {}) {
|
|
|
482
483
|
await writeFileAtomic(manifest.location, rendered);
|
|
483
484
|
changedFiles.push(MANIFEST_PATH);
|
|
484
485
|
}
|
|
486
|
+
const links = await claudeSkillLinks(projectRoot, true);
|
|
487
|
+
changedFiles.push(...links.changedFiles);
|
|
488
|
+
diagnostics.push(...(links.warnings || []));
|
|
485
489
|
return {
|
|
486
490
|
status: changedFiles.length > 0 ? 'updated' : 'unchanged',
|
|
487
491
|
changedFiles: [...new Set(changedFiles)].sort(),
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, readlink, rm, symlink } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { GenesisError } from './errors.js';
|
|
5
|
+
import { gitContext } from './git.js';
|
|
6
|
+
import { genesisHookCommand } from './hook-command.js';
|
|
7
|
+
import { writeFileAtomic } from './utils.js';
|
|
8
|
+
|
|
9
|
+
const SETTINGS_PATH = '.claude/settings.json';
|
|
10
|
+
const SESSION_COMMANDS = [1, 2, 3, 4].map((claudePart) => genesisHookCommand('session', { claudePart }));
|
|
11
|
+
const TURN_COMMAND = genesisHookCommand('turn');
|
|
12
|
+
// Refresh exact generated commands, including the previous plain-text output.
|
|
13
|
+
const OWNED_COMMANDS = new Set([...SESSION_COMMANDS, TURN_COMMAND,
|
|
14
|
+
genesisHookCommand('session')]);
|
|
15
|
+
|
|
16
|
+
async function renderedSettings(projectRoot) {
|
|
17
|
+
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
18
|
+
const location = path.join(root, SETTINGS_PATH);
|
|
19
|
+
let source = null;
|
|
20
|
+
let value = {};
|
|
21
|
+
try {
|
|
22
|
+
source = await readFile(location, 'utf8');
|
|
23
|
+
value = JSON.parse(source);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (!['ENOENT', 'ENOTDIR'].includes(error.code)) {
|
|
26
|
+
throw new GenesisError('CLAUDE_SETTINGS_INVALID', `${SETTINGS_PATH} is not valid JSON: ${error.message}.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
30
|
+
|| (value.hooks !== undefined && (!value.hooks || typeof value.hooks !== 'object' || Array.isArray(value.hooks)))) {
|
|
31
|
+
throw new GenesisError('CLAUDE_SETTINGS_INVALID', `${SETTINGS_PATH} must contain an object with an optional hooks object.`);
|
|
32
|
+
}
|
|
33
|
+
value.hooks ||= {};
|
|
34
|
+
for (const [event, groups] of Object.entries(value.hooks)) {
|
|
35
|
+
if (!Array.isArray(groups)) {
|
|
36
|
+
throw new GenesisError('CLAUDE_SETTINGS_INVALID', `${SETTINGS_PATH} hooks.${event} must be an array.`);
|
|
37
|
+
}
|
|
38
|
+
value.hooks[event] = groups.flatMap((group) => {
|
|
39
|
+
if (!Array.isArray(group?.hooks)) return [group];
|
|
40
|
+
const hooks = group.hooks.filter((hook) => !OWNED_COMMANDS.has(hook?.command));
|
|
41
|
+
return hooks.length ? [{ ...group, hooks }] : [];
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
(value.hooks.SessionStart ||= []).push({
|
|
45
|
+
matcher: '^(startup|resume|clear|compact)$',
|
|
46
|
+
hooks: SESSION_COMMANDS.map((command) => ({ type: 'command', command, timeout: 30 })),
|
|
47
|
+
});
|
|
48
|
+
(value.hooks.UserPromptSubmit ||= []).push({
|
|
49
|
+
hooks: [{ type: 'command', command: TURN_COMMAND, timeout: 30 }],
|
|
50
|
+
});
|
|
51
|
+
return { root, location, source, rendered: `${JSON.stringify(value, null, 2)}\n` };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Claude discovers .claude/skills; the canonical files remain in .agents/skills.
|
|
55
|
+
// Only exact generated links are owned here. User-owned Claude skills win.
|
|
56
|
+
export async function claudeSkillLinks(root, write = false) {
|
|
57
|
+
const [canonical, installed] = await Promise.all(['.agents/skills', '.claude/skills'].map(async (directory) => {
|
|
58
|
+
try { return await readdir(path.join(root, directory), { withFileTypes: true }); }
|
|
59
|
+
catch (error) { if (error.code === 'ENOENT') return []; throw error; }
|
|
60
|
+
}));
|
|
61
|
+
const names = new Set(canonical.filter((entry) => entry.isDirectory()).map(({ name }) => name));
|
|
62
|
+
const existing = new Map(installed.map((entry) => [entry.name, entry]));
|
|
63
|
+
const changedFiles = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
for (const name of new Set([...names, ...existing.keys()])) {
|
|
66
|
+
const relative = `.claude/skills/${name}`;
|
|
67
|
+
const location = path.join(root, relative);
|
|
68
|
+
const target = `../../.agents/skills/${name}`;
|
|
69
|
+
const entry = existing.get(name);
|
|
70
|
+
const owned = entry?.isSymbolicLink() && await readlink(location) === target;
|
|
71
|
+
if (!names.has(name)) {
|
|
72
|
+
if (owned) {
|
|
73
|
+
changedFiles.push(relative);
|
|
74
|
+
if (write) await rm(location);
|
|
75
|
+
}
|
|
76
|
+
} else if (!entry) {
|
|
77
|
+
changedFiles.push(relative);
|
|
78
|
+
if (write) {
|
|
79
|
+
await mkdir(path.dirname(location), { recursive: true });
|
|
80
|
+
await symlink(target, location, 'dir');
|
|
81
|
+
}
|
|
82
|
+
} else if (!owned) {
|
|
83
|
+
warnings.push({ code: 'CLAUDE_SKILL_OVERRIDE', message: `${relative} is user-owned and was preserved; Claude uses it instead of the canonical .agents/skills/${name}.` });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { changedFiles, ...(warnings.length ? { warnings } : {}) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function installClaudeHooks({ projectRoot } = {}) {
|
|
90
|
+
const { root, location, source, rendered } = await renderedSettings(projectRoot);
|
|
91
|
+
const result = await claudeSkillLinks(root, true);
|
|
92
|
+
if (source !== rendered) {
|
|
93
|
+
await writeFileAtomic(location, rendered);
|
|
94
|
+
result.changedFiles.push(SETTINGS_PATH);
|
|
95
|
+
}
|
|
96
|
+
return { status: result.changedFiles.length ? 'updated' : 'unchanged', ...result };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function inspectClaudeHooks({ projectRoot } = {}) {
|
|
100
|
+
const { root, source, rendered } = await renderedSettings(projectRoot);
|
|
101
|
+
const { changedFiles, warnings } = await claudeSkillLinks(root);
|
|
102
|
+
return { status: source === null ? 'missing' : source !== rendered || changedFiles.length ? 'outdated' : 'current',
|
|
103
|
+
...(warnings ? { warnings } : {}) };
|
|
104
|
+
}
|
package/src/index/codex-hooks.js
CHANGED
|
@@ -6,16 +6,12 @@ import { gitContext } from './git.js';
|
|
|
6
6
|
import { classifyProjectKind } from './project-files.js';
|
|
7
7
|
import { writeFileAtomic } from './utils.js';
|
|
8
8
|
import { inspectProjectFormatAtRoot } from './project-format.js';
|
|
9
|
+
import { genesisHookCommand as hookCommand } from './hook-command.js';
|
|
9
10
|
|
|
10
11
|
const HOOKS_PATH = '.codex/hooks.json';
|
|
11
12
|
const LEGACY_GENESIS_HOOKS_DESCRIPTION = 'Genesis project hooks.';
|
|
12
13
|
const PROJECT_HOOK_ACTIONS = ['session', 'turn', 'begin', 'stop', 'end'];
|
|
13
14
|
|
|
14
|
-
function hookCommand(action) {
|
|
15
|
-
const local = `const {runCli}=await import('genesis-compiler/cli');process.exitCode=await runCli(['hook','${action}'])`;
|
|
16
|
-
return `if command -v genesis >/dev/null 2>&1; then genesis hook ${action}; else node --input-type=module --eval "${local}"; fi`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
15
|
const SESSION_HOOK = {
|
|
20
16
|
event: 'SessionStart',
|
|
21
17
|
group: {
|
|
@@ -26,7 +22,7 @@ const SESSION_HOOK = {
|
|
|
26
22
|
commandWindows: 'genesis hook session',
|
|
27
23
|
timeout: 30,
|
|
28
24
|
statusMessage: 'Loading Genesis guidance',
|
|
29
|
-
additionalContextLimit:
|
|
25
|
+
additionalContextLimit: 0,
|
|
30
26
|
}],
|
|
31
27
|
},
|
|
32
28
|
};
|
|
@@ -88,7 +84,7 @@ async function installedHooksSource(location) {
|
|
|
88
84
|
}
|
|
89
85
|
}
|
|
90
86
|
|
|
91
|
-
|
|
87
|
+
async function renderedHooks(projectRoot) {
|
|
92
88
|
const root = (await gitContext(projectRoot)).repositoryRoot;
|
|
93
89
|
const location = path.join(root, HOOKS_PATH);
|
|
94
90
|
const { source, value } = await installedHooksSource(location);
|
|
@@ -100,6 +96,16 @@ export async function installCodexHooks({ projectRoot } = {}) {
|
|
|
100
96
|
const turnGroups = value.hooks[TURN_HOOK.event] ||= [];
|
|
101
97
|
turnGroups.push(TURN_HOOK.group);
|
|
102
98
|
const rendered = `${JSON.stringify(value, null, 2)}\n`;
|
|
99
|
+
return { source, rendered, location };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function inspectCodexHooks({ projectRoot } = {}) {
|
|
103
|
+
const { source, rendered } = await renderedHooks(projectRoot);
|
|
104
|
+
return { status: source === rendered ? 'current' : source === null ? 'missing' : 'outdated' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function installCodexHooks({ projectRoot } = {}) {
|
|
108
|
+
const { source, rendered, location } = await renderedHooks(projectRoot);
|
|
103
109
|
if (source === rendered) return { status: 'unchanged', changedFiles: [] };
|
|
104
110
|
await writeFileAtomic(location, rendered);
|
|
105
111
|
return { status: 'updated', changedFiles: [HOOKS_PATH] };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export function genesisHookCommand(action, { claudePart } = {}) {
|
|
2
|
+
const local = `const {runCli}=await import('genesis-compiler/cli');process.exitCode=await runCli(['hook','${action}'${claudePart ? `,'--claude-part','${claudePart}'` : ''}])`;
|
|
3
|
+
return `if command -v genesis >/dev/null 2>&1; then genesis hook ${action}${claudePart ? ` --claude-part ${claudePart}` : ''}; else node --input-type=module --eval "${local}"; fi`;
|
|
4
|
+
}
|
package/src/index/init.js
CHANGED
|
@@ -4,11 +4,10 @@ import path from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
import { syncProjectSkills } from './agent-skills.js';
|
|
6
6
|
import { BLUEPRINT_SKELETON_SOURCE } from './blueprint.js';
|
|
7
|
-
import {
|
|
7
|
+
import { syncAgentIntegrations } from './agent-integrations.js';
|
|
8
8
|
import { COLLABORATION_SKELETON_SOURCE } from './collaboration.js';
|
|
9
9
|
import { ENGINEERING_SKELETON_SOURCE } from './engineering.js';
|
|
10
10
|
import { gitContext } from './git.js';
|
|
11
|
-
import { installOpenCodePlugin } from './opencode-plugin.js';
|
|
12
11
|
import {
|
|
13
12
|
BLUEPRINT_PATH,
|
|
14
13
|
COLLABORATION_PATH,
|
|
@@ -50,17 +49,13 @@ export async function initializeProject({ projectRoot, stackPackages = [] } = {}
|
|
|
50
49
|
await mkdir(path.join(root, PROGRAM_ROOT), { recursive: true });
|
|
51
50
|
const stack = await readStack(root, { stackPackages });
|
|
52
51
|
const skills = await syncProjectSkills({ projectRoot: root, stack });
|
|
53
|
-
const
|
|
54
|
-
installCodexHooks({ projectRoot: root }),
|
|
55
|
-
installOpenCodePlugin({ projectRoot: root }),
|
|
56
|
-
]);
|
|
52
|
+
const integrations = await syncAgentIntegrations({ projectRoot: root });
|
|
57
53
|
const version = projectFormat.status === 'uninitialized'
|
|
58
54
|
? await createIfMissing(root, PROJECT_VERSION_PATH, CURRENT_PROJECT_FORMAT_SOURCE)
|
|
59
55
|
: null;
|
|
60
56
|
const changedFiles = [
|
|
61
57
|
...created,
|
|
62
|
-
...
|
|
63
|
-
...openCodePlugin.changedFiles,
|
|
58
|
+
...integrations.changedFiles,
|
|
64
59
|
...skills.changedFiles,
|
|
65
60
|
version,
|
|
66
61
|
]
|
|
@@ -69,12 +64,14 @@ export async function initializeProject({ projectRoot, stackPackages = [] } = {}
|
|
|
69
64
|
return {
|
|
70
65
|
status: changedFiles.length > 0 ? 'updated' : 'unchanged',
|
|
71
66
|
summary: changedFiles.length > 0
|
|
72
|
-
? 'Initialized Genesis, Agent Skills, and project guidance for Codex and OpenCode.'
|
|
73
|
-
: 'Genesis, its Agent Skills, and project guidance for Codex and OpenCode are already initialized.',
|
|
67
|
+
? 'Initialized Genesis, Agent Skills, and project guidance for Codex, Claude Code and OpenCode.'
|
|
68
|
+
: 'Genesis, its Agent Skills, and project guidance for Codex, Claude Code and OpenCode are already initialized.',
|
|
74
69
|
changedFiles,
|
|
75
70
|
diagnostics: skills.diagnostics,
|
|
71
|
+
...(integrations.warnings ? { warnings: integrations.warnings } : {}),
|
|
76
72
|
guidance: [
|
|
77
73
|
'Open Codex and use /hooks to review and trust the project guidance hook.',
|
|
74
|
+
'Claude Code loads the project guidance hooks from .claude/settings.json.',
|
|
78
75
|
'OpenCode loads the project guidance plugin automatically.',
|
|
79
76
|
'Genesis workflow skills are available in .agents/skills/.',
|
|
80
77
|
'Describe product intent in genesis/blueprint.md.',
|
|
@@ -23,9 +23,11 @@ export const GENESIS_BOOTSTRAP_PATHS = Object.freeze([
|
|
|
23
23
|
'.genesis/machine-city.json',
|
|
24
24
|
'.genesis/program-city.json',
|
|
25
25
|
'.codex/hooks.json',
|
|
26
|
+
'.claude/settings.json',
|
|
26
27
|
'.opencode/plugins/genesis-project-guidance.js',
|
|
27
28
|
'.agents/skills/.genesis-managed.json',
|
|
28
29
|
...['genesis-project', 'genesis-program', 'genesis-deslop'].flatMap((name) => [
|
|
30
|
+
`.claude/skills/${name}`,
|
|
29
31
|
`.agents/skills/${name}/SKILL.md`,
|
|
30
32
|
`.agents/skills/${name}/agents/openai.yaml`,
|
|
31
33
|
]),
|
|
@@ -33,10 +35,12 @@ export const GENESIS_BOOTSTRAP_PATHS = Object.freeze([
|
|
|
33
35
|
const bootstrapPaths = new Set(GENESIS_BOOTSTRAP_PATHS);
|
|
34
36
|
|
|
35
37
|
export async function inspectProjectContent(projectRoot) {
|
|
38
|
+
// Exclude dependency paths in Git before collecting and sorting its output.
|
|
39
|
+
const sourcePaths = ['--', '.', ':(exclude,glob)**/node_modules/**'];
|
|
36
40
|
const [visible, deleted, ignored] = await Promise.all([
|
|
37
|
-
runGit(projectRoot, ['ls-files', '-z', '--cached', '--others', '--exclude-standard',
|
|
38
|
-
runGit(projectRoot, ['ls-files', '-z', '--deleted',
|
|
39
|
-
runGit(projectRoot, ['ls-files', '-z', '--others', '--ignored', '--exclude-standard',
|
|
41
|
+
runGit(projectRoot, ['ls-files', '-z', '--cached', '--others', '--exclude-standard', ...sourcePaths]),
|
|
42
|
+
runGit(projectRoot, ['ls-files', '-z', '--deleted', ...sourcePaths]),
|
|
43
|
+
runGit(projectRoot, ['ls-files', '-z', '--others', '--ignored', '--exclude-standard', ...sourcePaths]),
|
|
40
44
|
]);
|
|
41
45
|
const absent = new Set(deleted.stdout.toString('utf8').split('\0').filter(Boolean));
|
|
42
46
|
const paths = [...new Set([visible, ignored].flatMap(({ stdout }) => stdout.toString('utf8').split('\0').filter(Boolean)))]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readCollaboration } from './collaboration.js';
|
|
2
|
-
import { inspectProjectSkills } from './agent-skills.js';
|
|
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
5
|
import { asDiagnostic, GenesisError } from './errors.js';
|
|
@@ -166,6 +166,7 @@ export async function projectSessionContext({
|
|
|
166
166
|
'COLLABORATION APPROACH',
|
|
167
167
|
'',
|
|
168
168
|
collaboration.guidance,
|
|
169
|
+
...(skills.skills?.length ? ['', 'PROJECT AGENT SKILLS', '', renderAgentSkillCatalog(skills.skills)] : []),
|
|
169
170
|
...(hostContext ? ['', 'HOST CONTEXT', '', hostContext] : []),
|
|
170
171
|
].join('\n');
|
|
171
172
|
return { ...composedContext(GENESIS_CONTRACTS.sessionContext, output), warnings: skills.diagnostics };
|
package/src/index.js
CHANGED
|
@@ -31,6 +31,8 @@ import { projectSessionContext, projectTurnContext } from './index/session-conte
|
|
|
31
31
|
import { withTrustedGitRepository } from './index/process.js';
|
|
32
32
|
|
|
33
33
|
export { inspectProject };
|
|
34
|
+
export { inspectClaudeHooks, installClaudeHooks as syncClaudeHooks } from './index/claude-hooks.js';
|
|
35
|
+
export { inspectAgentIntegrations, syncAgentIntegrations } from './index/agent-integrations.js';
|
|
34
36
|
export { listParsers, installParsers, verifyParsers, parserEnvironment } from './index/code-indexers/parsers.js';
|
|
35
37
|
export { listTemplates } from './index/template-catalog.js';
|
|
36
38
|
export { applyTemplate } from './index/template-project.js';
|