genesis-compiler 1.7.2 → 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.
- package/docs/prompt-integration.md +26 -0
- package/package.json +1 -1
- package/plugins/opencode/project-guidance.js +65 -18
- package/src/cli.js +3 -0
- package/src/index/codex-hooks.js +2 -2
- package/src/index/host-context-resolver.js +1 -1
- package/src/index/opencode-plugin.js +7 -0
- package/src/index.js +1 -0
|
@@ -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
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
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
|
-
|
|
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
|
}
|
package/src/index/codex-hooks.js
CHANGED
|
@@ -24,7 +24,7 @@ const SESSION_HOOK = {
|
|
|
24
24
|
type: 'command',
|
|
25
25
|
command: hookCommand('session'),
|
|
26
26
|
commandWindows: 'genesis hook session',
|
|
27
|
-
timeout:
|
|
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:
|
|
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 =
|
|
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