genesis-compiler 1.2.19 → 1.2.20

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.2.19",
3
+ "version": "1.2.20",
4
4
  "type": "module",
5
5
  "description": "An agent-independent prompt, multi-language code-index, cleanup, and verification companion with optional Codex hooks.",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis",
3
- "version": "1.2.19",
3
+ "version": "1.2.20",
4
4
  "description": "Makes Codex aware of optional Genesis adoption for existing projects.",
5
5
  "author": {
6
6
  "name": "Mobily Enterprises"
@@ -1,4 +1,6 @@
1
- Read and follow `.agents/skills/genesis-deslop/SKILL.md` completely.
1
+ For every repository listed in the task, read and follow that repository's
2
+ `.agents/skills/genesis-deslop/SKILL.md` completely. Review and clean every
3
+ listed repository before responding.
2
4
 
3
5
  This is a dedicated behavior-preserving cleanup task, separate from
4
6
  implementation. Review the requested scope, or otherwise the ordinary Git diff
@@ -1,5 +1,6 @@
1
- Read and follow `.agents/skills/genesis-program/SKILL.md` completely, operating
2
- in focused post-change reconciliation mode.
1
+ For every repository listed in the task, read and follow that repository's
2
+ `.agents/skills/genesis-program/SKILL.md` completely, operating in focused
3
+ post-change reconciliation mode.
3
4
 
4
5
  Use the preceding implementation turn, listed changed paths, actual Git diff or
5
6
  history, and relevant tests. Update Blueprint only for intentional observable
package/src/cli.js CHANGED
@@ -257,6 +257,7 @@ function writeResult(command, result) {
257
257
  }
258
258
  if (command === 'hook') {
259
259
  if (['discover', 'session'].includes(result.kind) && result.output) line(process.stdout, result.output);
260
+ else if (result.kind === 'authorize') line(process.stdout, `authorize: ${result.status}`);
260
261
  else if (result.kind === 'stop') line(process.stdout, JSON.stringify(result.output));
261
262
  return;
262
263
  }
@@ -1,4 +1,5 @@
1
1
  import { readFile, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
2
3
  import path from 'node:path';
3
4
 
4
5
  import { readInstalledAsset } from './assets.js';
@@ -13,6 +14,7 @@ import { normalizeRelative, pathState, sha256, stableJson, writeFileAtomic } fro
13
14
 
14
15
  const HOOKS_PATH = '.codex/hooks.json';
15
16
  const HOOK_STATE_SCHEMA_VERSION = 2;
17
+ const HOOK_LOCATOR_SCHEMA_VERSION = 1;
16
18
  function hookCommand(action) {
17
19
  const local = `const {runCli}=await import('genesis-compiler/cli');process.exitCode=await runCli(['hook','${action}'])`;
18
20
  return `if command -v genesis >/dev/null 2>&1; then genesis hook ${action}; else node --input-type=module --eval "${local}"; fi`;
@@ -157,7 +159,8 @@ export async function codexSessionContext({ projectRoot } = {}) {
157
159
  '- Before creating a helper or public operation, run `genesis index <name-or-path>` and reuse an existing function when it already owns the behavior.',
158
160
  '- `.genesis/machine-city.json` is the detailed code/function map; `.genesis/program-city.json` is the simpler subsystem/operation map.',
159
161
  '- Program is fallible explanation; code, tests, and runtime behavior remain evidence.',
160
- '- Before changing project files for a user-requested implementation, run `genesis hook authorize`. Never authorize answer, explanation, review, or diagnosis-only turns.',
162
+ '- Before the first project-file edit in a user-requested implementation turn, silently run `genesis hook authorize` once in every Git repository you will edit. This is internal Genesis bookkeeping, not a request for user permission: do not ask the user, announce successful authorization, or include it in progress or final summaries. Mention it only if the command fails.',
163
+ '- Authorization is turn-scoped. Run it again for a later code-changing turn after a user follow-up or interruption, but not again within the same active turn after it succeeds. The command must report `authorize: authorized` or `authorize: unchanged`; otherwise stop before editing. Never authorize answer, explanation, review, diagnosis-only, reconciliation, or Deslop turns.',
161
164
  '- Apply the engineering approach below to implementation and cleanup; the Stop hook owns optional Stack Post-change work, Blueprint/Program reconciliation, and Deslop turns.',
162
165
  '- After an authorized code-changing turn, Genesis may request separate post-change, explanation, and cleanup turns.',
163
166
  `Engineering profile: ${engineering.profile?.id || 'invalid; run \`genesis check\`'}.`,
@@ -236,6 +239,12 @@ async function hookStatePath(projectRoot, input) {
236
239
  return path.resolve(projectRoot, gitPath);
237
240
  }
238
241
 
242
+ function hookLocatorPath(input) {
243
+ const key = sha256(hookId(input, 'session_id')).slice('sha256:'.length);
244
+ const user = typeof process.getuid === 'function' ? process.getuid() : 'user';
245
+ return path.join(tmpdir(), `genesis-codex-hook-${user}-${key}.json`);
246
+ }
247
+
239
248
  function writeTurnState(statePath, state) {
240
249
  return writeFileAtomic(statePath, stableJson(state), { mode: 0o600 });
241
250
  }
@@ -247,14 +256,25 @@ async function refreshProjectIndex(projectRoot) {
247
256
  export async function recordCodexTurn({ input, projectRoot } = {}) {
248
257
  const root = (await gitContext(projectRoot || input?.cwd)).repositoryRoot;
249
258
  const statePath = await hookStatePath(root, input);
259
+ const turnId = hookId(input, 'turn_id');
260
+ const existing = await readTurnState(statePath);
261
+ if (existing?.schemaVersion === HOOK_STATE_SCHEMA_VERSION && existing.turnId === turnId) {
262
+ await writeHookLocator(input, root);
263
+ return { status: 'preserved' };
264
+ }
265
+ const repositories = registeredRepositories(existing || {}, root);
250
266
  const state = {
251
267
  schemaVersion: HOOK_STATE_SCHEMA_VERSION,
252
- turnId: hookId(input, 'turn_id'),
268
+ turnId,
253
269
  phase: 'implementation',
254
270
  implementationAuthorized: false,
255
- snapshot: await repositorySnapshot(root),
271
+ repositories,
272
+ snapshot: repositories.length > 0 && existing?.snapshot
273
+ ? existing.snapshot
274
+ : await repositorySnapshot(root),
256
275
  };
257
276
  await writeTurnState(statePath, state);
277
+ await writeHookLocator(input, root);
258
278
  return { status: 'recorded' };
259
279
  }
260
280
 
@@ -265,6 +285,41 @@ async function readTurnState(statePath) {
265
285
  }
266
286
  }
267
287
 
288
+ async function readHookLocator(input) {
289
+ try { return JSON.parse(await readFile(hookLocatorPath(input), 'utf8')); } catch (error) {
290
+ if (['ENOENT', 'ENOTDIR'].includes(error?.code)) return null;
291
+ throw error;
292
+ }
293
+ }
294
+
295
+ function writeHookLocator(input, coordinatorRoot) {
296
+ return writeFileAtomic(hookLocatorPath(input), stableJson({
297
+ schemaVersion: HOOK_LOCATOR_SCHEMA_VERSION,
298
+ coordinatorRoot,
299
+ }), { mode: 0o600 });
300
+ }
301
+
302
+ async function removeTurnState(statePath, input) {
303
+ await Promise.all([
304
+ rm(statePath, { force: true }),
305
+ rm(hookLocatorPath(input), { force: true }),
306
+ ]);
307
+ }
308
+
309
+ function registeredRepositories(state, coordinatorRoot) {
310
+ if (Array.isArray(state.repositories) && state.repositories.length > 0) {
311
+ return state.repositories;
312
+ }
313
+ if (state.implementationAuthorized === true && state.snapshot) {
314
+ return [{
315
+ projectRoot: coordinatorRoot,
316
+ snapshot: state.snapshot,
317
+ changedPaths: normalizedPaths(state.changedPaths || []).sort(),
318
+ }];
319
+ }
320
+ return [];
321
+ }
322
+
268
323
  export async function authorizeCodexTurn({
269
324
  projectRoot = process.cwd(),
270
325
  sessionId = process.env.CODEX_SESSION_ID,
@@ -275,15 +330,52 @@ export async function authorizeCodexTurn({
275
330
  'genesis hook authorize requires the current Codex session.',
276
331
  );
277
332
  }
333
+ const input = { session_id: sessionId };
278
334
  const root = (await gitContext(projectRoot)).repositoryRoot;
279
- const statePath = await hookStatePath(root, { session_id: sessionId });
335
+ const locator = await readHookLocator(input);
336
+ if (locator && (
337
+ locator.schemaVersion !== HOOK_LOCATOR_SCHEMA_VERSION
338
+ || typeof locator.coordinatorRoot !== 'string'
339
+ || !locator.coordinatorRoot
340
+ )) {
341
+ throw new GenesisError(
342
+ 'CODEX_HOOK_COORDINATOR_INVALID',
343
+ 'The current Codex turn has invalid Genesis coordination state.',
344
+ );
345
+ }
346
+ const coordinatorRoot = locator
347
+ ? (await gitContext(locator.coordinatorRoot)).repositoryRoot
348
+ : root;
349
+ const statePath = await hookStatePath(coordinatorRoot, input);
280
350
  const state = await readTurnState(statePath);
281
351
  if (!state || state.schemaVersion !== HOOK_STATE_SCHEMA_VERSION || state.phase !== 'implementation') {
282
- return { status: 'not-applicable' };
352
+ throw new GenesisError(
353
+ 'CODEX_HOOK_TURN_UNAVAILABLE',
354
+ 'No active Genesis implementation turn is available. Start from the session repository and authorize before editing.',
355
+ );
283
356
  }
284
- if (state.implementationAuthorized === true) return { status: 'unchanged' };
285
- await writeTurnState(statePath, { ...state, implementationAuthorized: true });
286
- return { status: 'authorized' };
357
+ const repositories = registeredRepositories(state, coordinatorRoot);
358
+ const registered = repositories.some((repository) => repository.projectRoot === root);
359
+ if (!registered) {
360
+ repositories.push({
361
+ projectRoot: root,
362
+ snapshot: root === coordinatorRoot && state.snapshot
363
+ ? state.snapshot
364
+ : await repositorySnapshot(root),
365
+ changedPaths: [],
366
+ });
367
+ }
368
+ await writeTurnState(statePath, {
369
+ ...state,
370
+ implementationAuthorized: true,
371
+ repositories,
372
+ });
373
+ await writeHookLocator(input, coordinatorRoot);
374
+ return {
375
+ status: registered && state.implementationAuthorized === true
376
+ ? 'unchanged'
377
+ : 'authorized',
378
+ };
287
379
  }
288
380
 
289
381
  async function committedPaths(projectRoot, before, after) {
@@ -329,72 +421,117 @@ function changedPathLines(changedPaths) {
329
421
  ];
330
422
  }
331
423
 
332
- async function reconciliationContinuation(projectRoot, changedPaths) {
333
- const [instructions, stack, engineering] = await Promise.all([
424
+ function changedRepositoryLines(repositories) {
425
+ return repositories.flatMap(({ projectRoot, changedPaths }) => [
426
+ `## Repository: \`${projectRoot}\``,
427
+ '',
428
+ ...changedPathLines(changedPaths),
429
+ '',
430
+ ]);
431
+ }
432
+
433
+ async function changedRepositories(repositories) {
434
+ return (await Promise.all(repositories.map(async (repository) => {
435
+ const currentPaths = await changedProjectPaths(
436
+ repository.projectRoot,
437
+ repository.snapshot,
438
+ await repositorySnapshot(repository.projectRoot),
439
+ );
440
+ const changedPaths = normalizedPaths([
441
+ ...(repository.changedPaths || []),
442
+ ...currentPaths,
443
+ ]).sort();
444
+ return changedPaths.length > 0 ? { ...repository, changedPaths } : null;
445
+ }))).filter(Boolean);
446
+ }
447
+
448
+ async function repositoryContexts(repositories) {
449
+ return Promise.all(repositories.map(async (repository) => {
450
+ const [stack, engineering] = await Promise.all([
451
+ optionalStack(repository.projectRoot),
452
+ optionalEngineering(repository.projectRoot),
453
+ ]);
454
+ return { ...repository, stack, engineering };
455
+ }));
456
+ }
457
+
458
+ function repositoryGuidanceLines({ projectRoot, stack, engineering }, {
459
+ deslop = false,
460
+ postChange = false,
461
+ } = {}) {
462
+ return [
463
+ `## Repository: \`${projectRoot}\``,
464
+ '',
465
+ 'ENGINEERING APPROACH',
466
+ '',
467
+ engineering.guidance,
468
+ ...(stack?.guidance ? ['', 'SELECTED STACK GUIDANCE', '', stack.guidance] : []),
469
+ ...(postChange && stack?.postChange
470
+ ? ['', 'COMPOSED STACK POST-CHANGE WORK', '', stack.postChange]
471
+ : []),
472
+ ...(deslop && stack?.deslop
473
+ ? ['', 'SELECTED STACK CLEANUP GUIDANCE', '', stack.deslop]
474
+ : []),
475
+ '',
476
+ ];
477
+ }
478
+
479
+ async function reconciliationContinuation(repositories, knownContexts) {
480
+ const [instructions, contexts] = await Promise.all([
334
481
  readInstalledAsset('reconcile'),
335
- optionalStack(projectRoot),
336
- optionalEngineering(projectRoot),
482
+ knownContexts || repositoryContexts(repositories),
337
483
  ]);
338
484
  return [
339
485
  'This is the automatic Genesis explanation turn for the preceding implementation turn.',
340
- 'Use the same conversation context and the Git-visible changes below. Do not perform Deslop yet.',
486
+ 'Use the same conversation context and the Git-visible changes below in every listed repository. Do not perform Deslop yet.',
341
487
  '',
342
- 'CHANGED PROJECT PATHS',
488
+ 'REPOSITORIES AND CHANGED PROJECT PATHS',
343
489
  '',
344
- ...changedPathLines(changedPaths),
490
+ ...changedRepositoryLines(repositories),
345
491
  '',
346
492
  instructions.trim(),
347
493
  '',
348
- 'ENGINEERING APPROACH',
494
+ 'REPOSITORY GUIDANCE',
349
495
  '',
350
- engineering.guidance,
351
- ...(stack?.guidance ? ['', 'SELECTED STACK GUIDANCE', '', stack.guidance] : []),
496
+ ...contexts.flatMap((context) => repositoryGuidanceLines(context)),
352
497
  ].join('\n');
353
498
  }
354
499
 
355
- function postChangeContinuation(changedPaths, stack, engineering) {
500
+ function postChangeContinuation(repositories, contexts) {
356
501
  return [
357
502
  'This is the one automatic Stack Post-change turn for the preceding implementation.',
358
- 'Use the same conversation context and the Git-visible changes below.',
503
+ 'Use the same conversation context and the Git-visible changes below in every listed repository.',
359
504
  'Perform only the composed Post-change work. Do not broaden the task, reconcile Genesis explanations, or perform Deslop yet.',
360
505
  'Do not repeat or schedule another Post-change turn. If none of the configured work applies, make no changes and respond minimally.',
361
506
  '',
362
- 'CHANGED PROJECT PATHS',
507
+ 'REPOSITORIES AND CHANGED PROJECT PATHS',
363
508
  '',
364
- ...changedPathLines(changedPaths),
365
- '',
366
- 'COMPOSED STACK POST-CHANGE WORK',
509
+ ...changedRepositoryLines(repositories),
367
510
  '',
368
- stack.postChange,
511
+ 'REPOSITORY GUIDANCE AND POST-CHANGE WORK',
369
512
  '',
370
- 'ENGINEERING APPROACH',
371
- '',
372
- engineering.guidance,
373
- ...(stack.guidance ? ['', 'SELECTED STACK GUIDANCE', '', stack.guidance] : []),
513
+ ...contexts.flatMap((context) => repositoryGuidanceLines(context, { postChange: true })),
374
514
  ].join('\n');
375
515
  }
376
516
 
377
- async function deslopContinuation(projectRoot, changedPaths) {
378
- const [instructions, stack, engineering] = await Promise.all([
517
+ async function deslopContinuation(repositories) {
518
+ const [instructions, contexts] = await Promise.all([
379
519
  readInstalledAsset('deslop'),
380
- optionalStack(projectRoot),
381
- optionalEngineering(projectRoot),
520
+ repositoryContexts(repositories),
382
521
  ]);
383
522
  return [
384
523
  'This is the final automatic Genesis Deslop turn for the preceding implementation.',
385
- 'Review only that work and the paths listed below. Do not broaden the task.',
524
+ 'Review every listed repository and only that work and the paths listed below. Do not broaden the task.',
386
525
  '',
387
- 'CHANGED PROJECT PATHS',
526
+ 'REPOSITORIES AND CHANGED PROJECT PATHS',
388
527
  '',
389
- ...changedPathLines(changedPaths),
528
+ ...changedRepositoryLines(repositories),
390
529
  '',
391
530
  instructions.trim(),
392
531
  '',
393
- 'ENGINEERING APPROACH',
532
+ 'REPOSITORY GUIDANCE',
394
533
  '',
395
- engineering.guidance,
396
- ...(stack?.guidance ? ['', 'SELECTED STACK GUIDANCE', '', stack.guidance] : []),
397
- ...(stack?.deslop ? ['', 'SELECTED STACK CLEANUP GUIDANCE', '', stack.deslop] : []),
534
+ ...contexts.flatMap((context) => repositoryGuidanceLines(context, { deslop: true })),
398
535
  '',
399
536
  'PROGRAM CITATION MAINTENANCE',
400
537
  '',
@@ -405,6 +542,10 @@ async function deslopContinuation(projectRoot, changedPaths) {
405
542
  ].join('\n');
406
543
  }
407
544
 
545
+ async function refreshProjectIndexes(repositories) {
546
+ await Promise.all(repositories.map(({ projectRoot }) => refreshProjectIndex(projectRoot)));
547
+ }
548
+
408
549
  export async function completeCodexTurn({ input, projectRoot } = {}) {
409
550
  const root = (await gitContext(projectRoot || input?.cwd)).repositoryRoot;
410
551
  const statePath = await hookStatePath(root, input);
@@ -412,93 +553,98 @@ export async function completeCodexTurn({ input, projectRoot } = {}) {
412
553
  if (!state || state.schemaVersion !== HOOK_STATE_SCHEMA_VERSION) return {};
413
554
 
414
555
  if (state.implementationAuthorized !== true) {
415
- await rm(statePath, { force: true });
556
+ const repositories = await changedRepositories(registeredRepositories(state, root));
557
+ if (repositories.length === 0) {
558
+ await removeTurnState(statePath, input);
559
+ return {};
560
+ }
561
+ await writeTurnState(statePath, {
562
+ ...state,
563
+ phase: 'implementation',
564
+ repositories,
565
+ });
416
566
  return {};
417
567
  }
418
568
 
419
569
  if (state.phase === 'implementation') {
420
570
  if (input?.stop_hook_active === true || state.turnId !== hookId(input, 'turn_id')) {
421
- await rm(statePath, { force: true });
571
+ await removeTurnState(statePath, input);
422
572
  return {};
423
573
  }
424
- const changedPaths = await changedProjectPaths(root, state.snapshot, await repositorySnapshot(root));
425
- if (changedPaths.length === 0) {
426
- await rm(statePath, { force: true });
574
+ const repositories = await changedRepositories(registeredRepositories(state, root));
575
+ if (repositories.length === 0) {
576
+ await removeTurnState(statePath, input);
427
577
  return {};
428
578
  }
429
- const [stack, engineering] = await Promise.all([
430
- optionalStack(root),
431
- optionalEngineering(root),
432
- ]);
433
- const postChange = Boolean(stack?.postChange?.trim());
579
+ const contexts = await repositoryContexts(repositories);
580
+ const postChange = contexts.some(({ stack }) => Boolean(stack?.postChange?.trim()));
434
581
  await writeTurnState(statePath, {
435
582
  ...state,
436
583
  phase: postChange ? 'post-change' : 'reconcile',
437
- changedPaths,
584
+ repositories,
438
585
  });
439
586
  return {
440
587
  decision: 'block',
441
588
  reason: postChange
442
- ? postChangeContinuation(changedPaths, stack, engineering)
443
- : await reconciliationContinuation(root, changedPaths),
589
+ ? postChangeContinuation(repositories, contexts)
590
+ : await reconciliationContinuation(repositories, contexts),
444
591
  };
445
592
  }
446
593
 
447
594
  if (input?.stop_hook_active !== true) {
448
- await rm(statePath, { force: true });
595
+ await removeTurnState(statePath, input);
449
596
  return {};
450
597
  }
451
598
 
452
599
  if (state.phase === 'post-change') {
453
- const currentPaths = await changedProjectPaths(root, state.snapshot, await repositorySnapshot(root));
454
- const changedPaths = normalizedPaths([...state.changedPaths, ...currentPaths]).sort();
455
- if (changedPaths.length === 0) {
456
- await rm(statePath, { force: true });
600
+ const repositories = await changedRepositories(registeredRepositories(state, root));
601
+ if (repositories.length === 0) {
602
+ await removeTurnState(statePath, input);
457
603
  return {};
458
604
  }
459
605
  await writeTurnState(statePath, {
460
606
  ...state,
461
607
  phase: 'reconcile',
462
- changedPaths,
608
+ repositories,
463
609
  });
464
610
  return {
465
611
  decision: 'block',
466
- reason: await reconciliationContinuation(root, changedPaths),
612
+ reason: await reconciliationContinuation(repositories),
467
613
  };
468
614
  }
469
615
 
470
616
  if (state.phase === 'reconcile') {
471
- const currentPaths = await changedProjectPaths(root, state.snapshot, await repositorySnapshot(root));
472
- const changedPaths = normalizedPaths([...state.changedPaths, ...currentPaths]).sort();
473
- if (changedPaths.length === 0) {
474
- await refreshProjectIndex(root);
475
- await rm(statePath, { force: true });
617
+ const registered = registeredRepositories(state, root);
618
+ const repositories = await changedRepositories(registered);
619
+ if (repositories.length === 0) {
620
+ await refreshProjectIndexes(registered);
621
+ await removeTurnState(statePath, input);
476
622
  return {};
477
623
  }
478
- await refreshProjectIndex(root);
624
+ await refreshProjectIndexes(repositories);
479
625
  await writeTurnState(statePath, {
480
626
  ...state,
481
627
  phase: 'deslop',
482
- changedPaths,
628
+ repositories,
483
629
  });
484
630
  return {
485
631
  decision: 'block',
486
- reason: await deslopContinuation(root, changedPaths),
632
+ reason: await deslopContinuation(repositories),
487
633
  };
488
634
  }
489
635
 
490
636
  if (state.phase === 'deslop') {
491
- await refreshProjectIndex(root);
492
- await rm(statePath, { force: true });
637
+ await refreshProjectIndexes(registeredRepositories(state, root));
638
+ await removeTurnState(statePath, input);
493
639
  return {};
494
640
  }
495
641
 
496
- await rm(statePath, { force: true });
642
+ await removeTurnState(statePath, input);
497
643
  return {};
498
644
  }
499
645
 
500
646
  export async function discardCodexTurn({ input, projectRoot } = {}) {
501
647
  const root = (await gitContext(projectRoot || input?.cwd)).repositoryRoot;
502
- await rm(await hookStatePath(root, input), { force: true });
648
+ await removeTurnState(await hookStatePath(root, input), input);
503
649
  return { status: 'discarded' };
504
650
  }