genesis-compiler 1.2.29 → 1.3.1

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
@@ -24,6 +24,7 @@ The useful mental model is:
24
24
  genesis/
25
25
  version project-file format version used for deterministic migrations
26
26
  blueprint.md non-technical product intent
27
+ collaboration.md project-wide communication choices and authored requirements
27
28
  engineering.md selected engineering profile and project-specific requirements
28
29
  stack.md selected components, resources, verification, and project-owned operation contracts
29
30
  stack/ optional per-component Description, Guidance, Adoption, Post-change, and Deslop customization
@@ -237,6 +238,60 @@ Project-scoped options follow the command, for example
237
238
  a host or script needs the normalized result. Run `genesis --help` for the
238
239
  compact CLI synopsis.
239
240
 
241
+ ## Collaboration approach
242
+
243
+ Genesis keeps project-wide communication guidance in ordinary source rather
244
+ than in a host-private policy or a repeated user-message prefix:
245
+
246
+ ```markdown
247
+ # Collaboration approach
248
+
249
+ ## Tone
250
+
251
+ - `encouraging`
252
+
253
+ ## Response length
254
+
255
+ - `concise`
256
+
257
+ ## Assumed experience
258
+
259
+ - `comfortable`
260
+
261
+ ## Explanation style
262
+
263
+ - `concise`
264
+
265
+ ## Project requirements
266
+
267
+ - Nothing.
268
+ ```
269
+
270
+ Inspect or change it without parsing Markdown yourself:
271
+
272
+ ```bash
273
+ genesis collaboration show
274
+ genesis collaboration set direct balanced expert conclusions "Be candid."
275
+ ```
276
+
277
+ Genesis owns the supported choices and their concise expansions. Authored
278
+ project requirements receive one fixed framing sentence and are otherwise
279
+ preserved. They are omitted when the source says `- Nothing.`.
280
+
281
+ Persistent hosts should install `projectSessionContext()` once when a provider
282
+ conversation is created or refreshed. They may then generate task prompts with
283
+ `sessionContextInstalled: true`, preventing the stable Collaboration and
284
+ Engineering guidance from being repeated in each task. A host can supply one
285
+ explicit session/turn driver; Genesis performs no driver discovery and labels
286
+ turn output untrusted. If the project-local Codex or OpenCode adapter runs in a
287
+ separate process, the host may set `GENESIS_HOST_CONTEXT_RESOLVER` to one
288
+ executable and optionally supply opaque JSON in
289
+ `GENESIS_HOST_CONTEXT_RESOLVER_DATA`. Session and turn hooks pass only the
290
+ requested lane, native provider session id, and that configured data to the
291
+ executable. Session output is composed into stable context; a nonempty turn
292
+ result is capped at 512 bytes, labeled untrusted, and kept separate from the
293
+ authored user message. This is a single process bridge, not a plugin registry.
294
+
240
295
  ## Engineering approach
241
296
 
242
297
  Every profile inherits one universal rule: implementation must remain easy to
@@ -780,7 +835,8 @@ genesis migrate
780
835
 
781
836
  Migration is deterministic and forward-only. The format 2 migration snapshots
782
837
  the previously effective Stack operations into project-owned sections before
783
- removing component-runtime fallback. Migration also synchronizes managed skills
838
+ removing component-runtime fallback; format 3 adds the portable Collaboration
839
+ declaration. Migration also synchronizes managed skills
784
840
  and hooks, regenerates derived indexes, and then returns a fresh `check`
785
841
  result. A newer project is never downgraded. Every project-scoped CLI
786
842
  invocation warns when the recorded format does not match the running CLI, and
@@ -797,6 +853,7 @@ import {
797
853
  getContext,
798
854
  indexCodebase,
799
855
  initialize,
856
+ inspectCollaboration,
800
857
  inspectEngineering,
801
858
  inspectEnvironment,
802
859
  inspectStackSection,
@@ -804,6 +861,9 @@ import {
804
861
  listEngineeringProfiles,
805
862
  listStackPieces,
806
863
  migrate,
864
+ projectSessionContext,
865
+ projectTurnContext,
866
+ setCollaboration,
807
867
  setEngineeringProfile,
808
868
  verify,
809
869
  } from 'genesis-compiler';
@@ -818,6 +878,12 @@ codebase. `installCodex()` installs the optional global discovery plugin.
818
878
  `listEngineeringProfiles()`, `inspectEngineering()`, and
819
879
  `setEngineeringProfile()` expose the same built-in catalog and portable
820
880
  project selection used by the CLI and hosts.
881
+ `inspectCollaboration()` and `setCollaboration()` expose the portable
882
+ communication declaration and Genesis-owned choice expansions.
883
+ `projectSessionContext()` composes stable project guidance with one explicitly
884
+ supplied host-driver result. `projectTurnContext()` invokes that same optional
885
+ driver for the separate untrusted turn lane without accepting user-message
886
+ text.
821
887
  `getContext()` resolves source paths to the Program modules that cite them,
822
888
  the functions already declared there, selected Stack guidance, available Agent
823
889
  Skills, and verification commands. `indexCodebase()` regenerates or returns the
@@ -829,8 +895,9 @@ projection paths without returning any supplied environment value.
829
895
  interpreting or executing its contents.
830
896
 
831
897
  Normalized results identify their stable public contract in the `contract`
832
- field: `genesis.engineering.v1`, `genesis.environment.v2`,
833
- `genesis.stack-section.v1`, or
898
+ field: `genesis.collaboration.v1`, `genesis.engineering.v1`,
899
+ `genesis.environment.v2`, `genesis.session-context.v1`,
900
+ `genesis.stack-section.v1`, `genesis.turn-context.v1`, or
834
901
  `genesis.verification.v1`. A consumer defines any schema embedded inside the
835
902
  opaque section body.
836
903
 
@@ -15,6 +15,81 @@ const work = await generatePrompt({
15
15
  await currentAgent.send(work.prompt);
16
16
  ```
17
17
 
18
+ A persistent host installs stable context separately and keeps ordinary user
19
+ text unchanged:
20
+
21
+ ```js
22
+ import {
23
+ generatePrompt,
24
+ projectSessionContext,
25
+ projectTurnContext,
26
+ } from 'genesis-compiler';
27
+
28
+ const sessionContext = await projectSessionContext({
29
+ projectRoot,
30
+ hostDriver,
31
+ hostDriverInput: {
32
+ scope: 'session',
33
+ conversationKind: 'main',
34
+ session: { managedPreview: true },
35
+ },
36
+ });
37
+ await provider.installTrustedSessionContext(sessionContext.output);
38
+
39
+ const task = await generatePrompt({
40
+ projectRoot,
41
+ task: 'work',
42
+ request: userMessage,
43
+ sessionContextInstalled: true,
44
+ });
45
+ const turnContext = await projectTurnContext({
46
+ hostDriver,
47
+ hostDriverInput: {
48
+ scope: 'turn',
49
+ actor: { preferredAddressName: 'Merc' },
50
+ },
51
+ });
52
+ await provider.sendUserTurn({
53
+ text: task.prompt,
54
+ untrustedContext: turnContext.output,
55
+ });
56
+ ```
57
+
58
+ The provider method names above are illustrative. Genesis supplies semantic
59
+ session and untrusted-turn results; the host's provider adapter owns their
60
+ native placement. The driver never receives or rewrites `userMessage`, and
61
+ Genesis supports only the one explicitly supplied driver—there is no discovery
62
+ or plugin registry.
63
+
64
+ When the project-local Codex or OpenCode adapter runs in a separate process,
65
+ the host can configure that same one-driver boundary without adding another
66
+ prompt compositor:
67
+
68
+ ```text
69
+ GENESIS_HOST_CONTEXT_RESOLVER=/path/to/host-context-resolver
70
+ GENESIS_HOST_CONTEXT_RESOLVER_DATA={"registry":"/runtime/host-sessions.json"}
71
+ ```
72
+
73
+ For `genesis hook session`, Genesis invokes that one executable with bounded
74
+ JSON on stdin:
75
+
76
+ ```json
77
+ {
78
+ "scope": "session",
79
+ "providerSessionId": "native-session-id",
80
+ "data": { "registry": "/runtime/host-sessions.json" }
81
+ }
82
+ ```
83
+
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
86
+ hook input and OpenCode's transform callbacks supply the native session id;
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
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
91
+ ordinary Genesis session context exactly as before.
92
+
18
93
  Tasks are `start`, `adopt`, `work`, `deslop`, `program`, `blueprint`, `describe`, and `review`.
19
94
  `start` is the host-independent first conversation: it classifies an initialized
20
95
  project from its selected Stack and Git-visible project paths without building
@@ -48,6 +123,9 @@ Genesis keeps each instruction at one useful level:
48
123
  structural Indexers, and an optional authoritative Agent Skill source.
49
124
  - `genesis/blueprint.md`, `genesis/program/`, and `genesis/stack.md` are the
50
125
  project's own intent, explanation, and selected technical composition.
126
+ - `genesis/collaboration.md` selects Genesis-owned tone, response-length,
127
+ assumed-experience, and explanation-style instructions plus optional authored
128
+ project requirements.
51
129
  - `profiles/engineering/*.md` owns the installed versioned engineering
52
130
  profiles. `genesis/engineering.md` selects one profile and records explicit
53
131
  project requirements. A universal complexity gate is composed ahead of the
@@ -97,6 +175,21 @@ requests another model turn. Selected `Post-change` contributions are included
97
175
  in the original implementation prompt, whose project skill also keeps
98
176
  intentional Blueprint and affected Program changes aligned.
99
177
 
178
+ An out-of-process host may configure the single resolver described above. Each
179
+ adapter passes only its native session id to Genesis; Genesis sends that id,
180
+ the requested lane, and explicitly configured opaque data to the resolver.
181
+ Session output is composed with the stable guide; turn output remains a small
182
+ untrusted item beside a selected real user turn. This is one process bridge,
183
+ not a driver or plugin registry.
184
+
185
+ Hosts that own a persistent provider conversation can call
186
+ `projectSessionContext()` directly and pass at most one explicit host driver.
187
+ The complete result includes Genesis Collaboration and Engineering guidance and
188
+ has a stable identity. `projectTurnContext()` calls that driver only for a real
189
+ turn selected by the host and labels its separate result untrusted. Hosts use
190
+ `sessionContextInstalled: true` for task prompts only after installing the
191
+ session result; the default command-line prompt remains self-contained.
192
+
100
193
  The operating guide dynamically lists only the component ids in the currently
101
194
  available Stack catalogs. The naked CLI uses an explicitly installed
102
195
  first-party `genesis-stack` package as its initial catalog without exposing the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.2.29",
3
+ "version": "1.3.1",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis",
3
- "version": "1.2.29",
3
+ "version": "1.3.1",
4
4
  "description": "Makes Codex aware of optional Genesis adoption for existing projects.",
5
5
  "author": {
6
6
  "name": "Mobily Enterprises"
@@ -5,12 +5,18 @@ import { promisify } from "node:util";
5
5
 
6
6
  const execute = promisify(execFile);
7
7
 
8
- async function renderProjectContext(projectRoot) {
8
+ const HOST_CONTEXT_INPUT_ENV = "GENESIS_HOST_CONTEXT_INPUT";
9
+
10
+ async function renderContext(projectRoot, sessionId, scope) {
9
11
  const { stdout } = await execute(
10
12
  "genesis",
11
- ["hook", "session", "--project-root", projectRoot],
13
+ ["hook", scope, "--project-root", projectRoot],
12
14
  {
13
15
  cwd: projectRoot,
16
+ env: {
17
+ ...process.env,
18
+ [HOST_CONTEXT_INPUT_ENV]: JSON.stringify({ sessionId })
19
+ },
14
20
  maxBuffer: 256 * 1024,
15
21
  timeout: 5_000
16
22
  }
@@ -25,13 +31,13 @@ function eventSessionId(event = {}) {
25
31
  }
26
32
 
27
33
  export const GenesisProjectGuidance = async ({ directory, worktree } = {}) => {
28
- const projectRoot = path.resolve(worktree || directory || process.cwd());
34
+ const projectRoot = path.resolve(directory || worktree || process.cwd());
29
35
  const contexts = new Map();
30
36
 
31
37
  function contextForSession(sessionId) {
32
38
  let context = contexts.get(sessionId);
33
39
  if (!context) {
34
- context = renderProjectContext(projectRoot);
40
+ context = renderContext(projectRoot, sessionId, "session");
35
41
  contexts.set(sessionId, context);
36
42
  context.catch(() => {
37
43
  if (contexts.get(sessionId) === context) contexts.delete(sessionId);
@@ -50,7 +56,37 @@ export const GenesisProjectGuidance = async ({ directory, worktree } = {}) => {
50
56
  const sessionId = String(input.sessionID || input.sessionId || "").trim();
51
57
  if (!sessionId) return;
52
58
  const context = await contextForSession(sessionId);
53
- if (!output.system.includes(context)) output.system.push(context);
59
+ if (context && !output.system.includes(context)) output.system.push(context);
60
+ },
61
+ "experimental.chat.messages.transform": async (...hookArguments) => {
62
+ const output = hookArguments[1] || {};
63
+ const messages = Array.isArray(output.messages) ? output.messages : [];
64
+ const messageIndex = messages.findLastIndex((message) => message?.info?.role === "user");
65
+ if (messageIndex < 0) return;
66
+ const message = messages[messageIndex];
67
+ const sessionId = String(message.info?.sessionID || "").trim();
68
+ const messageId = String(message.info?.id || "").trim();
69
+ if (!sessionId || !messageId || !Array.isArray(message.parts)) return;
70
+ const context = await renderContext(projectRoot, sessionId, "turn");
71
+ if (!context || message.parts.some((part) => part?.synthetic === true && part?.text === context)) {
72
+ return;
73
+ }
74
+ output.messages = messages.map((candidate, index) => index === messageIndex
75
+ ? {
76
+ ...candidate,
77
+ parts: [
78
+ ...candidate.parts,
79
+ {
80
+ id: `${messageId}_genesis_turn_context`,
81
+ messageID: messageId,
82
+ sessionID: sessionId,
83
+ synthetic: true,
84
+ text: context,
85
+ type: "text"
86
+ }
87
+ ]
88
+ }
89
+ : candidate);
54
90
  }
55
91
  };
56
92
  };
@@ -8,12 +8,23 @@ description: Deslop committed work through an explicit behavior-preserving clean
8
8
  Review and simplify committed work without changing product behavior. Deslop is
9
9
  explicit: never run it merely because implementation finished.
10
10
 
11
- When this repository is Genesis itself or has `genesis-compiler` installed
12
- locally, invoke every Genesis CLI operation through the project-pinned package:
13
- `npm exec --no -- genesis <arguments>`. This runs without fetching another
14
- package. Otherwise use `genesis <arguments>` only when that executable is
15
- already available on `PATH`. Never install or update Genesis merely to satisfy
16
- a workflow instruction.
11
+ Resolve the invocation before running the first Genesis operation. Do not use a
12
+ Genesis operation or `genesis --version` as an availability probe.
13
+
14
+ 1. Read the repository-root `package.json`. Treat Genesis as project-pinned only
15
+ when that manifest's `name` is `genesis-compiler`, or its `dependencies`,
16
+ `devDependencies`, or `optionalDependencies` contain the exact
17
+ `genesis-compiler` key.
18
+ 2. For a project-pinned repository, invoke every Genesis CLI operation with
19
+ `npm exec --no -- genesis <arguments>`. This runs without fetching another
20
+ package. If the declared package is not installed, report that the project's
21
+ dependencies are not prepared; do not fall back to another Genesis version.
22
+ 3. Otherwise, do not try `npm exec`. Run `command -v genesis` (or the current
23
+ shell's equivalent `PATH` lookup). When it resolves an executable, invoke
24
+ `genesis <arguments>` directly.
25
+ 4. When neither source is available, stop and report that Genesis is
26
+ unavailable. Never install or update Genesis merely to satisfy a workflow
27
+ instruction.
17
28
 
18
29
  ## Load the effective project instructions once
19
30
 
@@ -25,12 +25,23 @@ one exact investigation.
25
25
 
26
26
  ## Run Genesis commands
27
27
 
28
- When this repository is Genesis itself or has `genesis-compiler` installed
29
- locally, invoke every Genesis CLI operation through the project-pinned package:
30
- `npm exec --no -- genesis <arguments>`. This runs without fetching another
31
- package. Otherwise use `genesis <arguments>` only when that executable is
32
- already available on `PATH`. Never install or update Genesis merely to satisfy
33
- a workflow instruction.
28
+ Resolve the invocation before running the first Genesis operation. Do not use a
29
+ Genesis operation or `genesis --version` as an availability probe.
30
+
31
+ 1. Read the repository-root `package.json`. Treat Genesis as project-pinned only
32
+ when that manifest's `name` is `genesis-compiler`, or its `dependencies`,
33
+ `devDependencies`, or `optionalDependencies` contain the exact
34
+ `genesis-compiler` key.
35
+ 2. For a project-pinned repository, invoke every Genesis CLI operation with
36
+ `npm exec --no -- genesis <arguments>`. This runs without fetching another
37
+ package. If the declared package is not installed, report that the project's
38
+ dependencies are not prepared; do not fall back to another Genesis version.
39
+ 3. Otherwise, do not try `npm exec`. Run `command -v genesis` (or the current
40
+ shell's equivalent `PATH` lookup). When it resolves an executable, invoke
41
+ `genesis <arguments>` directly.
42
+ 4. When neither source is available, stop and report that Genesis is
43
+ unavailable. Never install or update Genesis merely to satisfy a workflow
44
+ instruction.
34
45
 
35
46
  ## Resolve explicit technology choices
36
47
 
package/src/cli.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  getContext,
10
10
  indexCodebase,
11
11
  initialize,
12
+ inspectCollaboration,
12
13
  inspectEngineering,
13
14
  inspectEnvironment,
14
15
  inspectStackSection,
@@ -16,6 +17,7 @@ import {
16
17
  listEngineeringProfiles,
17
18
  listStackPieces,
18
19
  migrate,
20
+ setCollaboration,
19
21
  setEngineeringProfile,
20
22
  verify,
21
23
  } from './index.js';
@@ -24,11 +26,17 @@ import {
24
26
  } from './index/codex-hooks.js';
25
27
  import { engineeringProfile, readEngineeringBaseline } from './index/engineering.js';
26
28
  import { asDiagnostic, fail } from './index/errors.js';
29
+ import {
30
+ HOST_CONTEXT_INPUT_ENV,
31
+ HOST_CONTEXT_RESOLVER_ENV,
32
+ SESSION_CONTEXT_INSTALLED_ENV,
33
+ resolveConfiguredHostContext,
34
+ } from './index/host-context-resolver.js';
27
35
  import {
28
36
  inspectProjectFormat,
29
37
  projectFormatDiagnostic,
30
38
  } from './index/project-format.js';
31
- import { projectSessionContext } from './index/session-context.js';
39
+ import { projectSessionContext, projectTurnContext } from './index/session-context.js';
32
40
  import { installedFirstPartyStackPackages } from './index/stack-catalog.js';
33
41
  import { readStack } from './index/stack.js';
34
42
 
@@ -36,6 +44,8 @@ const USAGE = `Usage:
36
44
  genesis init
37
45
  genesis adopt [product guidance...]
38
46
  genesis codex install
47
+ genesis collaboration show
48
+ genesis collaboration set <tone> <response-length> <experience> <explanation-style> [project requirements...]
39
49
  genesis engineering list
40
50
  genesis engineering show [profile]
41
51
  genesis engineering set <profile>
@@ -63,7 +73,7 @@ prompt to the agent you already use. Review all edits through the ordinary Git
63
73
  diff, then run genesis verify for the Stack's concrete checks.
64
74
  `;
65
75
 
66
- const COMMANDS = new Set(['adopt', 'check', 'codex', 'context', 'engineering', 'hook', 'index', 'init', 'inspect', 'migrate', 'prompt', 'stack', 'verify']);
76
+ const COMMANDS = new Set(['adopt', 'check', 'codex', 'collaboration', 'context', 'engineering', 'hook', 'index', 'init', 'inspect', 'migrate', 'prompt', 'stack', 'verify']);
67
77
 
68
78
  function parseCommand(argv) {
69
79
  if (argv.length === 0 || argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') {
@@ -97,7 +107,21 @@ function parseCommand(argv) {
97
107
  if (options.task !== undefined && command !== 'prompt') {
98
108
  fail('CLI_OPTION_NOT_APPLICABLE', `Option --task is not applicable to ${command}.`);
99
109
  }
100
- if (command === 'stack') {
110
+ if (command === 'collaboration') {
111
+ const [action] = operands;
112
+ if (!['show', 'set'].includes(action)) {
113
+ fail('CLI_COLLABORATION_ACTION_REQUIRED', 'Command collaboration requires show or set.');
114
+ }
115
+ if (action === 'show' && operands.length !== 1) {
116
+ fail('CLI_EXTRA_ARGUMENT', 'Command collaboration show accepts no extra arguments.');
117
+ }
118
+ if (action === 'set' && operands.length < 5) {
119
+ fail(
120
+ 'CLI_COLLABORATION_VALUES_REQUIRED',
121
+ 'Command collaboration set requires tone, response length, experience, and explanation style.',
122
+ );
123
+ }
124
+ } else if (command === 'stack') {
101
125
  if (!['list', 'add'].includes(operands[0])) {
102
126
  fail('CLI_STACK_ACTION_REQUIRED', 'Command stack requires list or add.');
103
127
  }
@@ -136,8 +160,8 @@ function parseCommand(argv) {
136
160
  'Command inspect requires environment or section <name>.',
137
161
  );
138
162
  }
139
- } else if (command === 'hook' && (operands.length !== 1 || !['discover', 'session'].includes(operands[0]))) {
140
- fail('CLI_HOOK_ACTION_REQUIRED', 'Command hook requires exactly one of: discover, session.');
163
+ } else if (command === 'hook' && (operands.length !== 1 || !['discover', 'session', 'turn'].includes(operands[0]))) {
164
+ fail('CLI_HOOK_ACTION_REQUIRED', 'Command hook requires exactly one of: discover, session, turn.');
141
165
  } else if (!['adopt', 'context', 'hook', 'index', 'inspect', 'prompt'].includes(command) && operands.length > 0) {
142
166
  fail('CLI_EXTRA_ARGUMENT', `Command ${command} accepts no arguments.`);
143
167
  }
@@ -159,6 +183,7 @@ function writeCheck(result) {
159
183
  const version = format.projectVersion === null ? format.status : format.projectVersion;
160
184
  line(process.stdout, `Project format: ${version} (${format.status}; CLI supports ${format.supportedVersion})`);
161
185
  line(process.stdout, `Blueprint: ${result.blueprint}`);
186
+ line(process.stdout, `Collaboration approach: ${result.collaboration}`);
162
187
  line(process.stdout, `Engineering approach: ${result.engineering}`);
163
188
  line(process.stdout, `Stack: ${result.stack}`);
164
189
  line(process.stdout, `Agent Skills: ${result.skills}`);
@@ -174,6 +199,21 @@ function writeCheck(result) {
174
199
  line(process.stdout, `Check: ${result.status}`);
175
200
  }
176
201
 
202
+ function writeCollaboration(result) {
203
+ line(process.stdout, `Tone: ${result.tone}`);
204
+ line(process.stdout, `Response length: ${result.responseLength}`);
205
+ line(process.stdout, `Assumed experience: ${result.experience}`);
206
+ line(process.stdout, `Explanation style: ${result.explanationStyle}`);
207
+ if (result.requirements) {
208
+ line(process.stdout, 'Project requirements:');
209
+ line(process.stdout, result.requirements);
210
+ }
211
+ if (result.action === 'set') {
212
+ namedItems('Changed files', result.changedFiles);
213
+ line(process.stdout, `collaboration: ${result.status}`);
214
+ }
215
+ }
216
+
177
217
  function writeEngineering(result) {
178
218
  if (result.action === 'list') {
179
219
  for (const profile of result.profiles) {
@@ -236,6 +276,10 @@ function writeResult(command, result) {
236
276
  writeEngineering(result);
237
277
  return;
238
278
  }
279
+ if (command === 'collaboration') {
280
+ writeCollaboration(result);
281
+ return;
282
+ }
239
283
  if (command === 'context') {
240
284
  process.stdout.write(result.context.endsWith('\n') ? result.context : `${result.context}\n`);
241
285
  return;
@@ -263,7 +307,7 @@ function writeResult(command, result) {
263
307
  return;
264
308
  }
265
309
  if (command === 'hook') {
266
- if (['discover', 'session'].includes(result.kind) && result.output) line(process.stdout, result.output);
310
+ if (['discover', 'session', 'turn'].includes(result.kind) && result.output) line(process.stdout, result.output);
267
311
  return;
268
312
  }
269
313
  if (command === 'stack' && Array.isArray(result.pieces)) {
@@ -292,6 +336,32 @@ function writeResult(command, result) {
292
336
  line(process.stdout, `${command}: ${result.status}`);
293
337
  }
294
338
 
339
+ async function hookSessionId(environment = process.env) {
340
+ if (!environment[HOST_CONTEXT_RESOLVER_ENV]) return null;
341
+ let source = environment[HOST_CONTEXT_INPUT_ENV];
342
+ if (source === undefined && !process.stdin.isTTY) {
343
+ const chunks = [];
344
+ let bytes = 0;
345
+ for await (const chunk of process.stdin) {
346
+ bytes += Buffer.byteLength(chunk);
347
+ if (bytes > 16 * 1024) {
348
+ fail('HOST_CONTEXT_INPUT_INVALID', 'The host hook input exceeds 16 KiB.');
349
+ }
350
+ chunks.push(chunk);
351
+ }
352
+ source = Buffer.concat(chunks).toString('utf8');
353
+ }
354
+ if (!String(source || '').trim()) return null;
355
+ let input;
356
+ try {
357
+ input = JSON.parse(source);
358
+ } catch {
359
+ fail('HOST_CONTEXT_INPUT_INVALID', 'The host hook input must contain JSON.');
360
+ }
361
+ const sessionId = input?.sessionId ?? input?.session_id;
362
+ return typeof sessionId === 'string' && sessionId.trim() ? sessionId.trim() : null;
363
+ }
364
+
295
365
  async function cliStackPackages(projectRoot, supplied) {
296
366
  if (supplied.length > 0) return supplied;
297
367
  try {
@@ -312,6 +382,22 @@ async function execute({ command, operands, options }, { signal } = {}) {
312
382
  return adoptProject({ projectRoot, request: operands.join(' '), stackPackages });
313
383
  }
314
384
  if (command === 'codex') return installCodex();
385
+ if (command === 'collaboration') {
386
+ if (operands[0] === 'show') {
387
+ return { action: 'show', ...await inspectCollaboration({ projectRoot }) };
388
+ }
389
+ return {
390
+ action: 'set',
391
+ ...await setCollaboration({
392
+ projectRoot,
393
+ tone: operands[1],
394
+ responseLength: operands[2],
395
+ experience: operands[3],
396
+ explanationStyle: operands[4],
397
+ ...(operands.length > 5 ? { requirements: operands.slice(5).join(' ') } : {}),
398
+ }),
399
+ };
400
+ }
315
401
  if (command === 'engineering') {
316
402
  if (operands[0] === 'list') {
317
403
  return { action: 'list', status: 'ok', profiles: await listEngineeringProfiles() };
@@ -379,7 +465,38 @@ async function execute({ command, operands, options }, { signal } = {}) {
379
465
  if (operands[0] === 'discover') {
380
466
  return { kind: 'discover', ...await codexAdoptionRecommendation({ projectRoot }) };
381
467
  }
382
- return { kind: 'session', ...await projectSessionContext({ projectRoot, stackPackages }) };
468
+ const scope = operands[0];
469
+ if (scope === 'session' && process.env[SESSION_CONTEXT_INSTALLED_ENV] === '1') {
470
+ return { kind: 'session', status: 'ready', output: '' };
471
+ }
472
+ const providerSessionId = await hookSessionId();
473
+ const hostContext = await resolveConfiguredHostContext({
474
+ projectRoot,
475
+ providerSessionId,
476
+ scope,
477
+ });
478
+ if (scope === 'turn') {
479
+ return hostContext === null
480
+ ? { kind: 'turn', status: 'ready', output: '' }
481
+ : {
482
+ kind: 'turn',
483
+ ...await projectTurnContext({
484
+ hostDriver: () => hostContext,
485
+ hostDriverInput: { scope: 'turn' },
486
+ }),
487
+ };
488
+ }
489
+ return {
490
+ kind: 'session',
491
+ ...await projectSessionContext({
492
+ ...(hostContext === null ? {} : {
493
+ hostDriver: () => hostContext,
494
+ hostDriverInput: { scope: 'session' },
495
+ }),
496
+ projectRoot,
497
+ stackPackages,
498
+ }),
499
+ };
383
500
  }
384
501
  if (command === 'verify') {
385
502
  return verify({
@@ -1,4 +1,5 @@
1
1
  import { readBlueprint } from './blueprint.js';
2
+ import { readCollaboration } from './collaboration.js';
2
3
  import { inspectProjectSkills } from './agent-skills.js';
3
4
  import { readStack } from './stack.js';
4
5
  import { asDiagnostic } from './errors.js';
@@ -19,6 +20,7 @@ function invalidResult(area, error, projectFormat) {
19
20
  status: 'invalid',
20
21
  projectFormat,
21
22
  blueprint: area === 'blueprint' ? 'invalid' : 'valid',
23
+ collaboration: area === 'collaboration' ? 'invalid' : 'unknown',
22
24
  engineering: area === 'engineering' ? 'invalid' : 'unknown',
23
25
  stack: area === 'stack' ? 'invalid' : 'unknown',
24
26
  skills: area === 'skills' ? 'invalid' : 'unknown',
@@ -43,6 +45,7 @@ function formatMismatchResult(projectFormat) {
43
45
  status,
44
46
  projectFormat,
45
47
  blueprint: 'unknown',
48
+ collaboration: 'unknown',
46
49
  engineering: 'unknown',
47
50
  stack: 'unknown',
48
51
  skills: 'unknown',
@@ -75,7 +78,13 @@ export async function checkProject({
75
78
  }
76
79
 
77
80
  let stack;
81
+ let collaboration;
78
82
  let engineering;
83
+ try {
84
+ collaboration = await readCollaboration(root);
85
+ } catch (error) {
86
+ return invalidResult('collaboration', error, projectFormat);
87
+ }
79
88
  try {
80
89
  engineering = await readEngineering(root);
81
90
  } catch (error) {
@@ -154,6 +163,7 @@ export async function checkProject({
154
163
  : needsAttention ? 'attention' : 'ok',
155
164
  projectFormat,
156
165
  blueprint: 'valid',
166
+ collaboration: collaboration.status,
157
167
  engineering: engineering.status,
158
168
  stack: 'valid',
159
169
  skills: skills.status,