genesis-compiler 1.7.1 → 1.7.3

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.
@@ -90,6 +90,32 @@ transform place a nonempty turn result separately from authored user text. If
90
90
  no resolver is configured, both 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 the generated adapter with
112
+ `inspectOpenCodePlugin({ projectRoot })` and call `syncOpenCodePlugin({ projectRoot })`
113
+ from `genesis-compiler` under their ordinary source-write admission. It updates
114
+ only `.opencode/plugins/genesis-project-guidance.js`, returns `status` and
115
+ `changedFiles`, and leaves authored project files and other plugins alone.
116
+ Reload the provider instance to use an adapter that was already loaded. The
117
+ existing `genesis migrate` command also refreshes this generated adapter.
118
+
93
119
  Tasks are `start`, `adopt`, `work`, `deslop`, `program`, `blueprint`, `describe`, and `review`.
94
120
  `start` is the host-independent first conversation: it classifies an initialized
95
121
  project from its selected Stack and Git-visible project paths without building
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
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
  }
@@ -6,3 +6,4 @@
6
6
  - If good software requires material complexity beyond these rules, stop before writing it. Explain the requirement, why the direct design is insufficient, and the smallest added complexity proposed, then ask the user to approve or clarify the tradeoff.
7
7
  - During implementation, run the relevant checks and batch related cases when they share setup. Follow the project's verification policy for broad or final checks; do not repeat them merely because an item finishes or a session resumes. Preserve required coverage and isolation.
8
8
  - Keep complete test output in local artifacts and preserve exit status. Read concise counts, timings and actionable failure excerpts first, expanding evidence only when needed. Do not load full passing reports or repeatedly poll unchanged logs into agent context. Retain startup and cleanup failures; retry only after a change or new evidence justifies it.
9
+ - For browser verification, batch related actions and return only the relevant values or page region. Capture screenshots at meaningful visual checkpoints or failures, not after every action. Preserve required viewport, identity and security coverage; prefer existing automated cases for repeated checks.
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
  }
@@ -24,7 +24,7 @@ const SESSION_HOOK = {
24
24
  type: 'command',
25
25
  command: hookCommand('session'),
26
26
  commandWindows: 'genesis hook session',
27
- timeout: 5,
27
+ timeout: 30,
28
28
  statusMessage: 'Loading Genesis guidance',
29
29
  additionalContextLimit: 4000,
30
30
  }],
@@ -38,7 +38,7 @@ const TURN_HOOK = {
38
38
  type: 'command',
39
39
  command: hookCommand('turn'),
40
40
  commandWindows: 'genesis hook turn',
41
- timeout: 5,
41
+ timeout: 30,
42
42
  additionalContextLimit: 512,
43
43
  }],
44
44
  },
@@ -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);
@@ -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
+ }
package/src/index.js CHANGED
@@ -226,3 +226,4 @@ export async function syncSkills(options) {
226
226
  }
227
227
 
228
228
  export { inspectSubsystems } from './index/subsystems.js';
229
+ export { inspectOpenCodePlugin, installOpenCodePlugin as syncOpenCodePlugin } from './index/opencode-plugin.js';