genesis-compiler 1.7.2 → 1.7.4

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 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. Both use the same
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 hook and OpenCode plugin use the same short operating guide
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,14 +82,42 @@ 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's
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 hook and OpenCode message
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, both turn paths are empty and the adapters render
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
+ A host that has no turn contribution can set `GENESIS_TURN_CONTEXT_ENABLED=0`.
94
+ The OpenCode adapter then skips the turn subprocess, and `genesis hook turn`
95
+ returns empty output without calling the resolver. Session guidance remains
96
+ active. Otherwise, OpenCode caches the latest user message's turn result,
97
+ including an empty result, and clears it on compaction or deletion.
98
+
99
+ OpenCode hook failures carry `GENESIS_HOOK_FAILURE` followed by JSON containing
100
+ `scope`, `outcome` (`timeout`, `unavailable`, or `failed`), `elapsedMs`,
101
+ `timeoutMs`, `code`, `signal`, and bounded `stderr`. Known secret values from the
102
+ environment are redacted. The adapter's five-second deadline is independent of
103
+ Codex hook settings. A host can render these fields as an actionable failure
104
+ instead of displaying the runtime stack.
105
+
106
+ The OpenCode session adapter also includes verified native `parentSessionIds`
107
+ in `GENESIS_HOST_CONTEXT_INPUT` for host command shims. The resolver still
108
+ receives the actual child's `providerSessionId`; Genesis does not interpret a
109
+ host's ownership rules.
110
+
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.
120
+
93
121
  Tasks are `start`, `adopt`, `work`, `deslop`, `program`, `blueprint`, `describe`, and `review`.
94
122
  `start` is the host-independent first conversation: it classifies an initialized
95
123
  project from its selected Stack and Git-visible project paths without building
@@ -165,11 +193,20 @@ does not perform adoption automatically. If the user accepts, Codex runs the
165
193
  command itself and follows its returned prompt in the same conversation. Other
166
194
  hosts can call `adoptProject()` and send its `prompt` directly.
167
195
 
168
- Codex and OpenCode can use the project-local adapters installed by `genesis
169
- init`. Both receive the same short operating guide for a new session and after
170
- compaction. Codex uses `SessionStart` for startup, clear, and compact events.
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 settings preserve unrelated hooks and configuration. The common operating
202
+ guide lists installed project skills and their canonical `.agents/skills/` paths;
203
+ an agent reads the relevant skill before following its workflow. Genesis exposes
204
+ the canonical skill directories to Claude through relative `.claude/skills/`
205
+ links, so native skill commands use the same files. Skills synchronization adds
206
+ new links and removes only its exact dangling links; existing native Claude
207
+ skills are preserved and reported as overrides.
171
208
  OpenCode renders once per session, keeps that value active in its stateless
172
- model requests, and rerenders after its `session.compacted` event. Neither
209
+ model requests, and rerenders after its `session.compacted` event. No
173
210
  adapter records a user message, authorizes edits, intercepts completion, or
174
211
  requests another model turn. Selected `Post-change` contributions are included
175
212
  in the original implementation prompt, whose project skill also keeps
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
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": {
@@ -7,21 +7,45 @@ const execute = promisify(execFile);
7
7
 
8
8
  const HOST_CONTEXT_INPUT_ENV = "GENESIS_HOST_CONTEXT_INPUT";
9
9
 
10
- async function renderContext(projectRoot, sessionId, scope) {
11
- const { stdout } = await execute(
12
- "genesis",
13
- ["hook", scope, "--project-root", projectRoot],
14
- {
15
- cwd: projectRoot,
16
- env: {
17
- ...process.env,
18
- [HOST_CONTEXT_INPUT_ENV]: JSON.stringify({ sessionId })
19
- },
20
- maxBuffer: 256 * 1024,
21
- timeout: 5_000
10
+ async function renderContext(projectRoot, sessionId, scope, parentSessionIds = []) {
11
+ if (scope === "turn" && process.env.GENESIS_TURN_CONTEXT_ENABLED === "0") return "";
12
+ const started = Date.now();
13
+ const timeoutMs = 5_000;
14
+ try {
15
+ const { stdout } = await execute(
16
+ "genesis",
17
+ ["hook", scope, "--project-root", projectRoot],
18
+ {
19
+ cwd: projectRoot,
20
+ env: {
21
+ ...process.env,
22
+ [HOST_CONTEXT_INPUT_ENV]: JSON.stringify({ sessionId, parentSessionIds })
23
+ },
24
+ maxBuffer: 256 * 1024,
25
+ timeout: timeoutMs
26
+ }
27
+ );
28
+ return stdout.trimEnd();
29
+ } catch (error) {
30
+ let stderr = String(error.stderr || "");
31
+ for (const [name, value] of Object.entries(process.env)) {
32
+ if (value && /(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|DSN|DATABASE_URL)/iu.test(name)) {
33
+ stderr = stderr.replaceAll(value, "[redacted]");
34
+ }
22
35
  }
23
- );
24
- return stdout.trimEnd();
36
+ const elapsedMs = Date.now() - started;
37
+ const details = {
38
+ scope,
39
+ outcome: error.killed && error.signal === "SIGTERM" && elapsedMs >= timeoutMs ? "timeout" :
40
+ error.code === "ENOENT" ? "unavailable" : "failed",
41
+ elapsedMs,
42
+ timeoutMs,
43
+ code: error.code ?? null,
44
+ signal: error.signal || null,
45
+ stderr: stderr.trim().slice(0, 1_500)
46
+ };
47
+ throw new Error(`GENESIS_HOOK_FAILURE ${JSON.stringify(details)}`);
48
+ }
25
49
  }
26
50
 
27
51
  function eventSessionId(event = {}) {
@@ -30,14 +54,26 @@ function eventSessionId(event = {}) {
30
54
  ).trim();
31
55
  }
32
56
 
33
- export const GenesisProjectGuidance = async ({ directory, worktree } = {}) => {
57
+ export const GenesisProjectGuidance = async ({ client, directory, worktree } = {}) => {
34
58
  const projectRoot = path.resolve(directory || worktree || process.cwd());
35
59
  const contexts = new Map();
60
+ const turnContexts = new Map();
36
61
 
37
62
  function contextForSession(sessionId) {
38
63
  let context = contexts.get(sessionId);
39
64
  if (!context) {
40
- context = renderContext(projectRoot, sessionId, "session");
65
+ context = Promise.resolve().then(async () => {
66
+ const parents = [];
67
+ let id = sessionId;
68
+ while (client && id && parents.length < 32) {
69
+ const result = await client.session.get({ path: { id } });
70
+ if (result.error) throw new Error("Genesis could not read the native session ancestry.");
71
+ id = String(result.data?.parentID || "").trim();
72
+ if (!id || id === sessionId || parents.includes(id)) break;
73
+ parents.push(id);
74
+ }
75
+ return renderContext(projectRoot, sessionId, "session", parents);
76
+ });
41
77
  contexts.set(sessionId, context);
42
78
  context.catch(() => {
43
79
  if (contexts.get(sessionId) === context) contexts.delete(sessionId);
@@ -50,7 +86,10 @@ export const GenesisProjectGuidance = async ({ directory, worktree } = {}) => {
50
86
  event: async ({ event } = {}) => {
51
87
  if (!["session.compacted", "session.deleted"].includes(event?.type)) return;
52
88
  const sessionId = eventSessionId(event);
53
- if (sessionId) contexts.delete(sessionId);
89
+ if (sessionId) {
90
+ contexts.delete(sessionId);
91
+ turnContexts.delete(sessionId);
92
+ }
54
93
  },
55
94
  "experimental.chat.system.transform": async (input = {}, output = {}) => {
56
95
  const sessionId = String(input.sessionID || input.sessionId || "").trim();
@@ -67,7 +106,15 @@ export const GenesisProjectGuidance = async ({ directory, worktree } = {}) => {
67
106
  const sessionId = String(message.info?.sessionID || "").trim();
68
107
  const messageId = String(message.info?.id || "").trim();
69
108
  if (!sessionId || !messageId || !Array.isArray(message.parts)) return;
70
- const context = await renderContext(projectRoot, sessionId, "turn");
109
+ let current = turnContexts.get(sessionId);
110
+ if (current?.messageId !== messageId) {
111
+ current = { messageId, context: renderContext(projectRoot, sessionId, "turn") };
112
+ turnContexts.set(sessionId, current);
113
+ current.context.catch(() => {
114
+ if (turnContexts.get(sessionId) === current) turnContexts.delete(sessionId);
115
+ });
116
+ }
117
+ const context = await current.context;
71
118
  if (!context || message.parts.some((part) => part?.synthetic === true && part?.text === context)) {
72
119
  return;
73
120
  }
package/src/cli.js CHANGED
@@ -562,6 +562,9 @@ async function execute({ command, operands, options }, { signal } = {}) {
562
562
  return { kind: 'discover', ...await codexAdoptionRecommendation({ projectRoot }) };
563
563
  }
564
564
  const scope = operands[0];
565
+ if (scope === 'turn' && process.env.GENESIS_TURN_CONTEXT_ENABLED === '0') {
566
+ return { kind: 'turn', status: 'ready', output: '' };
567
+ }
565
568
  if (scope === 'session' && process.env[SESSION_CONTEXT_INSTALLED_ENV] === '1') {
566
569
  return { kind: 'session', status: 'ready', output: '' };
567
570
  }
@@ -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,101 @@
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_COMMAND = genesisHookCommand('session');
11
+ const TURN_COMMAND = genesisHookCommand('turn');
12
+
13
+ async function renderedSettings(projectRoot) {
14
+ const root = (await gitContext(projectRoot)).repositoryRoot;
15
+ const location = path.join(root, SETTINGS_PATH);
16
+ let source = null;
17
+ let value = {};
18
+ try {
19
+ source = await readFile(location, 'utf8');
20
+ value = JSON.parse(source);
21
+ } catch (error) {
22
+ if (!['ENOENT', 'ENOTDIR'].includes(error.code)) {
23
+ throw new GenesisError('CLAUDE_SETTINGS_INVALID', `${SETTINGS_PATH} is not valid JSON: ${error.message}.`);
24
+ }
25
+ }
26
+ if (!value || typeof value !== 'object' || Array.isArray(value)
27
+ || (value.hooks !== undefined && (!value.hooks || typeof value.hooks !== 'object' || Array.isArray(value.hooks)))) {
28
+ throw new GenesisError('CLAUDE_SETTINGS_INVALID', `${SETTINGS_PATH} must contain an object with an optional hooks object.`);
29
+ }
30
+ value.hooks ||= {};
31
+ for (const [event, groups] of Object.entries(value.hooks)) {
32
+ if (!Array.isArray(groups)) {
33
+ throw new GenesisError('CLAUDE_SETTINGS_INVALID', `${SETTINGS_PATH} hooks.${event} must be an array.`);
34
+ }
35
+ value.hooks[event] = groups.flatMap((group) => {
36
+ if (!Array.isArray(group?.hooks)) return [group];
37
+ const hooks = group.hooks.filter((hook) => ![SESSION_COMMAND, TURN_COMMAND].includes(hook?.command));
38
+ return hooks.length ? [{ ...group, hooks }] : [];
39
+ });
40
+ }
41
+ (value.hooks.SessionStart ||= []).push({
42
+ matcher: '^(startup|resume|clear|compact)$',
43
+ hooks: [{ type: 'command', command: SESSION_COMMAND, timeout: 30 }],
44
+ });
45
+ (value.hooks.UserPromptSubmit ||= []).push({
46
+ hooks: [{ type: 'command', command: TURN_COMMAND, timeout: 30 }],
47
+ });
48
+ return { root, location, source, rendered: `${JSON.stringify(value, null, 2)}\n` };
49
+ }
50
+
51
+ // Claude discovers .claude/skills; the canonical files remain in .agents/skills.
52
+ // Only exact generated links are owned here. User-owned Claude skills win.
53
+ export async function claudeSkillLinks(root, write = false) {
54
+ const [canonical, installed] = await Promise.all(['.agents/skills', '.claude/skills'].map(async (directory) => {
55
+ try { return await readdir(path.join(root, directory), { withFileTypes: true }); }
56
+ catch (error) { if (error.code === 'ENOENT') return []; throw error; }
57
+ }));
58
+ const names = new Set(canonical.filter((entry) => entry.isDirectory()).map(({ name }) => name));
59
+ const existing = new Map(installed.map((entry) => [entry.name, entry]));
60
+ const changedFiles = [];
61
+ const warnings = [];
62
+ for (const name of new Set([...names, ...existing.keys()])) {
63
+ const relative = `.claude/skills/${name}`;
64
+ const location = path.join(root, relative);
65
+ const target = `../../.agents/skills/${name}`;
66
+ const entry = existing.get(name);
67
+ const owned = entry?.isSymbolicLink() && await readlink(location) === target;
68
+ if (!names.has(name)) {
69
+ if (owned) {
70
+ changedFiles.push(relative);
71
+ if (write) await rm(location);
72
+ }
73
+ } else if (!entry) {
74
+ changedFiles.push(relative);
75
+ if (write) {
76
+ await mkdir(path.dirname(location), { recursive: true });
77
+ await symlink(target, location, 'dir');
78
+ }
79
+ } else if (!owned) {
80
+ warnings.push({ code: 'CLAUDE_SKILL_OVERRIDE', message: `${relative} is user-owned and was preserved; Claude uses it instead of the canonical .agents/skills/${name}.` });
81
+ }
82
+ }
83
+ return { changedFiles, ...(warnings.length ? { warnings } : {}) };
84
+ }
85
+
86
+ export async function installClaudeHooks({ projectRoot } = {}) {
87
+ const { root, location, source, rendered } = await renderedSettings(projectRoot);
88
+ const result = await claudeSkillLinks(root, true);
89
+ if (source !== rendered) {
90
+ await writeFileAtomic(location, rendered);
91
+ result.changedFiles.push(SETTINGS_PATH);
92
+ }
93
+ return { status: result.changedFiles.length ? 'updated' : 'unchanged', ...result };
94
+ }
95
+
96
+ export async function inspectClaudeHooks({ projectRoot } = {}) {
97
+ const { root, source, rendered } = await renderedSettings(projectRoot);
98
+ const { changedFiles, warnings } = await claudeSkillLinks(root);
99
+ return { status: source === null ? 'missing' : source !== rendered || changedFiles.length ? 'outdated' : 'current',
100
+ ...(warnings ? { warnings } : {}) };
101
+ }
@@ -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: {
@@ -24,9 +20,9 @@ const SESSION_HOOK = {
24
20
  type: 'command',
25
21
  command: hookCommand('session'),
26
22
  commandWindows: 'genesis hook session',
27
- timeout: 5,
23
+ timeout: 30,
28
24
  statusMessage: 'Loading Genesis guidance',
29
- additionalContextLimit: 4000,
25
+ additionalContextLimit: 0,
30
26
  }],
31
27
  },
32
28
  };
@@ -38,7 +34,7 @@ const TURN_HOOK = {
38
34
  type: 'command',
39
35
  command: hookCommand('turn'),
40
36
  commandWindows: 'genesis hook turn',
41
- timeout: 5,
37
+ timeout: 30,
42
38
  additionalContextLimit: 512,
43
39
  }],
44
40
  },
@@ -88,7 +84,7 @@ async function installedHooksSource(location) {
88
84
  }
89
85
  }
90
86
 
91
- export async function installCodexHooks({ projectRoot } = {}) {
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) {
2
+ const local = `const {runCli}=await import('genesis-compiler/cli');process.exitCode=await runCli(['hook','${action}'])`;
3
+ return `if command -v genesis >/dev/null 2>&1; then genesis hook ${action}; else node --input-type=module --eval "${local}"; fi`;
4
+ }
@@ -8,7 +8,7 @@ export const HOST_CONTEXT_INPUT_ENV = 'GENESIS_HOST_CONTEXT_INPUT';
8
8
  export const SESSION_CONTEXT_INSTALLED_ENV = 'GENESIS_SESSION_CONTEXT_INSTALLED';
9
9
 
10
10
  const RESOLVER_OUTPUT_MAX_BYTES = 64 * 1024;
11
- const RESOLVER_TIMEOUT_MS = 5_000;
11
+ const RESOLVER_TIMEOUT_MS = 30_000;
12
12
 
13
13
  function resolverError(message) {
14
14
  return new GenesisError('HOST_CONTEXT_RESOLVER_FAILED', message);
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 { installCodexHooks } from './codex-hooks.js';
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 [hooks, openCodePlugin] = await Promise.all([
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
- ...hooks.changedFiles,
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.',
@@ -26,3 +26,10 @@ export async function installOpenCodePlugin({ projectRoot } = {}) {
26
26
  await writeFileAtomic(location, source);
27
27
  return { status: 'updated', changedFiles: [OPENCODE_PLUGIN_PATH] };
28
28
  }
29
+
30
+ export async function inspectOpenCodePlugin({ projectRoot } = {}) {
31
+ const root = (await gitContext(projectRoot)).repositoryRoot;
32
+ const current = await existingSource(path.join(root, OPENCODE_PLUGIN_PATH));
33
+ const source = await readFile(OPENCODE_PLUGIN_SOURCE, 'utf8');
34
+ return { status: current === source ? 'current' : current === null ? 'missing' : 'outdated' };
35
+ }
@@ -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';
@@ -226,3 +228,4 @@ export async function syncSkills(options) {
226
228
  }
227
229
 
228
230
  export { inspectSubsystems } from './index/subsystems.js';
231
+ export { inspectOpenCodePlugin, installOpenCodePlugin as syncOpenCodePlugin } from './index/opencode-plugin.js';