genesis-compiler 1.2.28 → 1.3.0

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/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({
@@ -3,6 +3,9 @@ import { readFile } from 'node:fs/promises';
3
3
  const root = new URL('../../', import.meta.url);
4
4
  const assets = {
5
5
  start: new URL('prompts/start.txt', root),
6
+ 'start-new': new URL('prompts/start-new.txt', root),
7
+ 'start-existing': new URL('prompts/start-existing.txt', root),
8
+ 'start-existing-uninitialized': new URL('prompts/start-existing-uninitialized.txt', root),
6
9
  adopt: new URL('prompts/adopt.txt', root),
7
10
  work: new URL('prompts/work.txt', root),
8
11
  describe: new URL('prompts/describe.txt', root),
@@ -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,
@@ -3,13 +3,12 @@ import path from 'node:path';
3
3
 
4
4
  import { GenesisError } from './errors.js';
5
5
  import { gitContext } from './git.js';
6
- import { isProjectContentPath } from './paths.js';
7
- import { runGit } from './process.js';
8
- import { normalizeRelative, pathState, writeFileAtomic } from './utils.js';
6
+ import { classifyProjectKind } from './project-files.js';
7
+ import { pathState, writeFileAtomic } from './utils.js';
9
8
 
10
9
  const HOOKS_PATH = '.codex/hooks.json';
11
10
  const LEGACY_GENESIS_HOOKS_DESCRIPTION = 'Genesis project hooks.';
12
- const PROJECT_HOOK_ACTIONS = ['session', 'begin', 'stop', 'end'];
11
+ const PROJECT_HOOK_ACTIONS = ['session', 'turn', 'begin', 'stop', 'end'];
13
12
 
14
13
  function hookCommand(action) {
15
14
  const local = `const {runCli}=await import('genesis-compiler/cli');process.exitCode=await runCli(['hook','${action}'])`;
@@ -31,6 +30,19 @@ const SESSION_HOOK = {
31
30
  },
32
31
  };
33
32
 
33
+ const TURN_HOOK = {
34
+ event: 'UserPromptSubmit',
35
+ group: {
36
+ hooks: [{
37
+ type: 'command',
38
+ command: hookCommand('turn'),
39
+ commandWindows: 'genesis hook turn',
40
+ timeout: 5,
41
+ additionalContextLimit: 512,
42
+ }],
43
+ },
44
+ };
45
+
34
46
  const OBSOLETE_PROJECT_HOOK_COMMANDS = new Set(PROJECT_HOOK_ACTIONS.flatMap((action) => [
35
47
  hookCommand(action),
36
48
  `genesis hook ${action}`,
@@ -84,26 +96,23 @@ export async function installCodexHooks({ projectRoot } = {}) {
84
96
  removeGenesisProjectHooks(value.hooks);
85
97
  const groups = value.hooks[SESSION_HOOK.event] ||= [];
86
98
  groups.push(SESSION_HOOK.group);
99
+ const turnGroups = value.hooks[TURN_HOOK.event] ||= [];
100
+ turnGroups.push(TURN_HOOK.group);
87
101
  const rendered = `${JSON.stringify(value, null, 2)}\n`;
88
102
  if (source === rendered) return { status: 'unchanged', changedFiles: [] };
89
103
  await writeFileAtomic(location, rendered);
90
104
  return { status: 'updated', changedFiles: [HOOKS_PATH] };
91
105
  }
92
106
 
93
- function zeroPaths(buffer) {
94
- return [...new Set(buffer.toString('utf8').split('\0').map(normalizeRelative).filter(Boolean))];
95
- }
96
-
97
107
  export async function codexAdoptionRecommendation({ projectRoot = process.cwd() } = {}) {
98
108
  let root;
99
109
  try { root = (await gitContext(projectRoot)).repositoryRoot; } catch { return { status: 'not-applicable', output: '' }; }
100
110
  if ((await pathState(path.join(root, 'genesis/blueprint.md'))).exists) {
101
111
  return { status: 'not-applicable', output: '' };
102
112
  }
103
- const files = zeroPaths((await runGit(root, [
104
- 'ls-files', '-z', '--cached', '--others', '--exclude-standard',
105
- ])).stdout).filter(isProjectContentPath);
106
- if (files.length === 0) return { status: 'not-applicable', output: '' };
113
+ if (await classifyProjectKind({ projectRoot: root }) === 'new') {
114
+ return { status: 'not-applicable', output: '' };
115
+ }
107
116
  return {
108
117
  status: 'ready',
109
118
  output: [