genesis-compiler 1.2.18 → 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/README.md CHANGED
@@ -261,26 +261,27 @@ Python, Java, C#, C/C++, PHP, Go, Rust, Ruby, Kotlin, and Shell. A future techno
261
261
  adds its adapter through its Stack piece rather than teaching Genesis prompts
262
262
  to parse that language.
263
263
 
264
- At the end of a turn that changed Git-visible project files, the Stop hook asks
265
- Codex for bounded follow-up turns. When the selected Stack contributes
266
- `Post-change` work, all contributions compose deterministically into one
267
- optional code-changing turn. Genesis then reconciles the Blueprint and affected
268
- Program explanations from the complete changed-path set and actual Git diff,
269
- and refreshes both City projections. A focused, behavior-preserving Deslop pass
270
- runs before both Cities are refreshed again. Genesis then requests one concise
271
- user-facing summary using conversation context only; that final phase is not
272
- Stack-configurable and cannot schedule more cleanup. A project with no selected
273
- Stack uses only the `genesis-deslop` skill. Selected Stack components enrich
274
- Post-change work, reconciliation, and cleanup with general Guidance, and enrich
275
- cleanup further with technology-specific Deslop guidance.
264
+ Before changing project files for a user-requested implementation, Codex runs
265
+ `genesis hook authorize`. At the end of an authorized turn that changed
266
+ Git-visible project files, the Stop hook asks Codex for bounded follow-up turns.
267
+ Unmarked turns finish normally even if another process changes the same
268
+ checkout. When the selected Stack contributes `Post-change` work, all
269
+ contributions compose deterministically into one optional code-changing turn.
270
+ Genesis then reconciles the Blueprint and affected Program explanations from
271
+ the complete changed-path set and actual Git diff, and refreshes both City
272
+ projections. A focused, behavior-preserving Deslop pass runs before both Cities
273
+ are refreshed again; its response is the concise user-facing completion. A
274
+ project with no selected Stack uses only the `genesis-deslop` skill. Selected
275
+ Stack components enrich Post-change work, reconciliation, and cleanup with
276
+ general Guidance, and enrich cleanup further with technology-specific Deslop
277
+ guidance.
276
278
 
277
279
  The hook never starts another Codex process. Its small per-session phase is
278
280
  limited to `implementation -> optional post-change -> reconcile -> deslop ->
279
- summary -> done`, so it cannot keep continuing recursively. The summary prompt
280
- forbids repository inspection, tools, commands, tests, and edits, and the next
281
- Stop always clears the sequence rather than scheduling more work. Empty
282
- Post-change content is skipped rather than producing an empty turn.
283
- Question-only and unchanged turns finish normally.
281
+ done`, so it cannot keep continuing recursively. The Stop after Deslop clears
282
+ the sequence rather than scheduling another model turn. Empty Post-change
283
+ content is skipped rather than producing an empty turn. Question, explanation,
284
+ review, diagnosis-only, and unchanged turns finish normally.
284
285
 
285
286
  Projects can customize a selected technology through its matching file, such
286
287
  as `genesis/stack/jskit.md`:
@@ -77,20 +77,21 @@ command itself and follows its returned prompt in the same conversation. Other
77
77
  hosts can call `adoptProject()` and send its `prompt` directly.
78
78
 
79
79
  Codex can instead use the project-local hooks installed by `genesis init`.
80
- Those hooks inject a short operating guide, record whether the current turn
81
- changed project files, then request bounded continuations. Selected
80
+ Those hooks inject a short operating guide and record the Git-visible state when
81
+ each user prompt begins. Before editing for an explicit user implementation
82
+ request, Codex marks that turn with `genesis hook authorize`. Only an authorized
83
+ turn whose project files changed can request bounded continuations; unmarked
84
+ turns finish normally even if another process changes the checkout. Selected
82
85
  `Post-change` contributions compose into one optional code-changing turn;
83
86
  Genesis then reconciles Blueprint and affected Program explanations from the
84
- complete Git-visible change set and Deslops the implementation. One final
85
- summary prompt then asks for a concise user-facing account from conversation
86
- context only, forbids tools and edits, and cannot schedule more cleanup. The
87
- hooks invoke the same Genesis project operations and never start another agent
88
- process. A per-session phase permits only `implementation -> optional
89
- post-change -> reconcile -> deslop -> summary -> done`. Empty Post-change
90
- content skips that phase. Genesis refreshes both City projections after
91
- reconciliation so Deslop sees the current explanation and function inventory,
92
- then refreshes them again after Deslop in case cleanup changed the
93
- implementation map.
87
+ complete Git-visible change set and Deslops the implementation. The Deslop
88
+ response is the concise user-facing completion. The hooks invoke the same
89
+ Genesis project operations and never start another agent process. A per-session
90
+ phase permits only `implementation -> optional post-change -> reconcile ->
91
+ deslop -> done`. Empty Post-change content skips that phase. Genesis refreshes
92
+ both City projections after reconciliation so Deslop sees the current
93
+ explanation and function inventory, then refreshes them again after Deslop in
94
+ case cleanup changed the implementation map.
94
95
 
95
96
  Once Codex identifies relevant source, `genesis context <path...>` returns only
96
97
  Program modules citing those paths plus the effective engineering approach and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis-compiler",
3
- "version": "1.2.18",
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": {
@@ -31,7 +31,6 @@
31
31
  "prompts/adopt.txt",
32
32
  "prompts/describe.txt",
33
33
  "prompts/deslop.txt",
34
- "prompts/final-summary.txt",
35
34
  "prompts/program.txt",
36
35
  "prompts/reconcile.txt",
37
36
  "prompts/review.txt",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesis",
3
- "version": "1.2.18",
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
@@ -19,3 +21,8 @@ changed path or appears related to the preceding implementation.
19
21
 
20
22
  Do not broaden the product change. Run focused checks when useful and summarize
21
23
  what became simpler, files changed, and checks actually run.
24
+
25
+ Your response from this turn is the final user-facing answer for the original
26
+ request. Make it concise and self-contained. Lead with the outcome, include the
27
+ important implemented behavior and any genuine blocker or unfinished work, and
28
+ do not mention automatic phases, hooks, or these instructions.
@@ -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
@@ -19,6 +19,7 @@ import {
19
19
  verify,
20
20
  } from './index.js';
21
21
  import {
22
+ authorizeCodexTurn,
22
23
  codexAdoptionRecommendation,
23
24
  codexSessionContext,
24
25
  completeCodexTurn,
@@ -131,8 +132,8 @@ function parseCommand(argv) {
131
132
  'Command inspect requires environment or section <name>.',
132
133
  );
133
134
  }
134
- } else if (command === 'hook' && (operands.length !== 1 || !['begin', 'discover', 'end', 'session', 'stop'].includes(operands[0]))) {
135
- fail('CLI_HOOK_ACTION_REQUIRED', 'Command hook requires exactly one of: discover, session, begin, stop, end.');
135
+ } else if (command === 'hook' && (operands.length !== 1 || !['authorize', 'begin', 'discover', 'end', 'session', 'stop'].includes(operands[0]))) {
136
+ fail('CLI_HOOK_ACTION_REQUIRED', 'Command hook requires exactly one of: discover, session, begin, authorize, stop, end.');
136
137
  } else if (!['adopt', 'context', 'hook', 'index', 'inspect', 'prompt'].includes(command) && operands.length > 0) {
137
138
  fail('CLI_EXTRA_ARGUMENT', `Command ${command} accepts no arguments.`);
138
139
  }
@@ -256,6 +257,7 @@ function writeResult(command, result) {
256
257
  }
257
258
  if (command === 'hook') {
258
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}`);
259
261
  else if (result.kind === 'stop') line(process.stdout, JSON.stringify(result.output));
260
262
  return;
261
263
  }
@@ -373,6 +375,9 @@ async function execute({ command, operands, options }, { signal } = {}) {
373
375
  if (operands[0] === 'session') {
374
376
  return { kind: 'session', ...await codexSessionContext({ projectRoot }) };
375
377
  }
378
+ if (operands[0] === 'authorize') {
379
+ return { kind: 'authorize', ...await authorizeCodexTurn({ projectRoot }) };
380
+ }
376
381
  const input = await hookInput();
377
382
  if (operands[0] === 'begin') {
378
383
  return { kind: 'begin', ...await recordCodexTurn({ input, projectRoot }) };
@@ -8,7 +8,6 @@ const assets = {
8
8
  describe: new URL('prompts/describe.txt', root),
9
9
  reconcile: new URL('prompts/reconcile.txt', root),
10
10
  deslop: new URL('prompts/deslop.txt', root),
11
- finalSummary: new URL('prompts/final-summary.txt', root),
12
11
  program: new URL('prompts/program.txt', root),
13
12
  blueprint: new URL('prompts/blueprint.txt', root),
14
13
  review: new URL('prompts/review.txt', root),
@@ -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,8 +159,10 @@ 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.',
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.',
160
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.',
161
- '- After a code-changing turn, Genesis may request separate post-change, explanation, cleanup, and final-summary turns.',
165
+ '- After an authorized code-changing turn, Genesis may request separate post-change, explanation, and cleanup turns.',
162
166
  `Engineering profile: ${engineering.profile?.id || 'invalid; run \`genesis check\`'}.`,
163
167
  `Selected Stack components: ${stackStatus}.`,
164
168
  '',
@@ -235,6 +239,12 @@ async function hookStatePath(projectRoot, input) {
235
239
  return path.resolve(projectRoot, gitPath);
236
240
  }
237
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
+
238
248
  function writeTurnState(statePath, state) {
239
249
  return writeFileAtomic(statePath, stableJson(state), { mode: 0o600 });
240
250
  }
@@ -246,13 +256,25 @@ async function refreshProjectIndex(projectRoot) {
246
256
  export async function recordCodexTurn({ input, projectRoot } = {}) {
247
257
  const root = (await gitContext(projectRoot || input?.cwd)).repositoryRoot;
248
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);
249
266
  const state = {
250
267
  schemaVersion: HOOK_STATE_SCHEMA_VERSION,
251
- turnId: hookId(input, 'turn_id'),
268
+ turnId,
252
269
  phase: 'implementation',
253
- snapshot: await repositorySnapshot(root),
270
+ implementationAuthorized: false,
271
+ repositories,
272
+ snapshot: repositories.length > 0 && existing?.snapshot
273
+ ? existing.snapshot
274
+ : await repositorySnapshot(root),
254
275
  };
255
276
  await writeTurnState(statePath, state);
277
+ await writeHookLocator(input, root);
256
278
  return { status: 'recorded' };
257
279
  }
258
280
 
@@ -263,6 +285,99 @@ async function readTurnState(statePath) {
263
285
  }
264
286
  }
265
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
+
323
+ export async function authorizeCodexTurn({
324
+ projectRoot = process.cwd(),
325
+ sessionId = process.env.CODEX_SESSION_ID,
326
+ } = {}) {
327
+ if (!sessionId) {
328
+ throw new GenesisError(
329
+ 'CODEX_SESSION_ID_MISSING',
330
+ 'genesis hook authorize requires the current Codex session.',
331
+ );
332
+ }
333
+ const input = { session_id: sessionId };
334
+ const root = (await gitContext(projectRoot)).repositoryRoot;
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);
350
+ const state = await readTurnState(statePath);
351
+ if (!state || state.schemaVersion !== HOOK_STATE_SCHEMA_VERSION || state.phase !== 'implementation') {
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
+ );
356
+ }
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
+ };
379
+ }
380
+
266
381
  async function committedPaths(projectRoot, before, after) {
267
382
  if (before === after) return [];
268
383
  if (!after) return ['repository history changed'];
@@ -306,72 +421,117 @@ function changedPathLines(changedPaths) {
306
421
  ];
307
422
  }
308
423
 
309
- async function reconciliationContinuation(projectRoot, changedPaths) {
310
- 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([
311
481
  readInstalledAsset('reconcile'),
312
- optionalStack(projectRoot),
313
- optionalEngineering(projectRoot),
482
+ knownContexts || repositoryContexts(repositories),
314
483
  ]);
315
484
  return [
316
485
  'This is the automatic Genesis explanation turn for the preceding implementation turn.',
317
- '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.',
318
487
  '',
319
- 'CHANGED PROJECT PATHS',
488
+ 'REPOSITORIES AND CHANGED PROJECT PATHS',
320
489
  '',
321
- ...changedPathLines(changedPaths),
490
+ ...changedRepositoryLines(repositories),
322
491
  '',
323
492
  instructions.trim(),
324
493
  '',
325
- 'ENGINEERING APPROACH',
494
+ 'REPOSITORY GUIDANCE',
326
495
  '',
327
- engineering.guidance,
328
- ...(stack?.guidance ? ['', 'SELECTED STACK GUIDANCE', '', stack.guidance] : []),
496
+ ...contexts.flatMap((context) => repositoryGuidanceLines(context)),
329
497
  ].join('\n');
330
498
  }
331
499
 
332
- function postChangeContinuation(changedPaths, stack, engineering) {
500
+ function postChangeContinuation(repositories, contexts) {
333
501
  return [
334
502
  'This is the one automatic Stack Post-change turn for the preceding implementation.',
335
- '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.',
336
504
  'Perform only the composed Post-change work. Do not broaden the task, reconcile Genesis explanations, or perform Deslop yet.',
337
505
  'Do not repeat or schedule another Post-change turn. If none of the configured work applies, make no changes and respond minimally.',
338
506
  '',
339
- 'CHANGED PROJECT PATHS',
340
- '',
341
- ...changedPathLines(changedPaths),
342
- '',
343
- 'COMPOSED STACK POST-CHANGE WORK',
507
+ 'REPOSITORIES AND CHANGED PROJECT PATHS',
344
508
  '',
345
- stack.postChange,
509
+ ...changedRepositoryLines(repositories),
346
510
  '',
347
- 'ENGINEERING APPROACH',
511
+ 'REPOSITORY GUIDANCE AND POST-CHANGE WORK',
348
512
  '',
349
- engineering.guidance,
350
- ...(stack.guidance ? ['', 'SELECTED STACK GUIDANCE', '', stack.guidance] : []),
513
+ ...contexts.flatMap((context) => repositoryGuidanceLines(context, { postChange: true })),
351
514
  ].join('\n');
352
515
  }
353
516
 
354
- async function deslopContinuation(projectRoot, changedPaths) {
355
- const [instructions, stack, engineering] = await Promise.all([
517
+ async function deslopContinuation(repositories) {
518
+ const [instructions, contexts] = await Promise.all([
356
519
  readInstalledAsset('deslop'),
357
- optionalStack(projectRoot),
358
- optionalEngineering(projectRoot),
520
+ repositoryContexts(repositories),
359
521
  ]);
360
522
  return [
361
523
  'This is the final automatic Genesis Deslop turn for the preceding implementation.',
362
- '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.',
363
525
  '',
364
- 'CHANGED PROJECT PATHS',
526
+ 'REPOSITORIES AND CHANGED PROJECT PATHS',
365
527
  '',
366
- ...changedPathLines(changedPaths),
528
+ ...changedRepositoryLines(repositories),
367
529
  '',
368
530
  instructions.trim(),
369
531
  '',
370
- 'ENGINEERING APPROACH',
532
+ 'REPOSITORY GUIDANCE',
371
533
  '',
372
- engineering.guidance,
373
- ...(stack?.guidance ? ['', 'SELECTED STACK GUIDANCE', '', stack.guidance] : []),
374
- ...(stack?.deslop ? ['', 'SELECTED STACK CLEANUP GUIDANCE', '', stack.deslop] : []),
534
+ ...contexts.flatMap((context) => repositoryGuidanceLines(context, { deslop: true })),
375
535
  '',
376
536
  'PROGRAM CITATION MAINTENANCE',
377
537
  '',
@@ -382,8 +542,8 @@ async function deslopContinuation(projectRoot, changedPaths) {
382
542
  ].join('\n');
383
543
  }
384
544
 
385
- async function finalSummaryContinuation() {
386
- return (await readInstalledAsset('finalSummary')).trim();
545
+ async function refreshProjectIndexes(repositories) {
546
+ await Promise.all(repositories.map(({ projectRoot }) => refreshProjectIndex(projectRoot)));
387
547
  }
388
548
 
389
549
  export async function completeCodexTurn({ input, projectRoot } = {}) {
@@ -392,95 +552,99 @@ export async function completeCodexTurn({ input, projectRoot } = {}) {
392
552
  const state = await readTurnState(statePath);
393
553
  if (!state || state.schemaVersion !== HOOK_STATE_SCHEMA_VERSION) return {};
394
554
 
555
+ if (state.implementationAuthorized !== 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
+ });
566
+ return {};
567
+ }
568
+
395
569
  if (state.phase === 'implementation') {
396
570
  if (input?.stop_hook_active === true || state.turnId !== hookId(input, 'turn_id')) {
397
- await rm(statePath, { force: true });
571
+ await removeTurnState(statePath, input);
398
572
  return {};
399
573
  }
400
- const changedPaths = await changedProjectPaths(root, state.snapshot, await repositorySnapshot(root));
401
- if (changedPaths.length === 0) {
402
- await rm(statePath, { force: true });
574
+ const repositories = await changedRepositories(registeredRepositories(state, root));
575
+ if (repositories.length === 0) {
576
+ await removeTurnState(statePath, input);
403
577
  return {};
404
578
  }
405
- const [stack, engineering] = await Promise.all([
406
- optionalStack(root),
407
- optionalEngineering(root),
408
- ]);
409
- const postChange = Boolean(stack?.postChange?.trim());
579
+ const contexts = await repositoryContexts(repositories);
580
+ const postChange = contexts.some(({ stack }) => Boolean(stack?.postChange?.trim()));
410
581
  await writeTurnState(statePath, {
411
582
  ...state,
412
583
  phase: postChange ? 'post-change' : 'reconcile',
413
- changedPaths,
584
+ repositories,
414
585
  });
415
586
  return {
416
587
  decision: 'block',
417
588
  reason: postChange
418
- ? postChangeContinuation(changedPaths, stack, engineering)
419
- : await reconciliationContinuation(root, changedPaths),
589
+ ? postChangeContinuation(repositories, contexts)
590
+ : await reconciliationContinuation(repositories, contexts),
420
591
  };
421
592
  }
422
593
 
423
594
  if (input?.stop_hook_active !== true) {
424
- await rm(statePath, { force: true });
595
+ await removeTurnState(statePath, input);
425
596
  return {};
426
597
  }
427
598
 
428
599
  if (state.phase === 'post-change') {
429
- const currentPaths = await changedProjectPaths(root, state.snapshot, await repositorySnapshot(root));
430
- const changedPaths = normalizedPaths([...state.changedPaths, ...currentPaths]).sort();
431
- if (changedPaths.length === 0) {
432
- await rm(statePath, { force: true });
600
+ const repositories = await changedRepositories(registeredRepositories(state, root));
601
+ if (repositories.length === 0) {
602
+ await removeTurnState(statePath, input);
433
603
  return {};
434
604
  }
435
605
  await writeTurnState(statePath, {
436
606
  ...state,
437
607
  phase: 'reconcile',
438
- changedPaths,
608
+ repositories,
439
609
  });
440
610
  return {
441
611
  decision: 'block',
442
- reason: await reconciliationContinuation(root, changedPaths),
612
+ reason: await reconciliationContinuation(repositories),
443
613
  };
444
614
  }
445
615
 
446
616
  if (state.phase === 'reconcile') {
447
- const currentPaths = await changedProjectPaths(root, state.snapshot, await repositorySnapshot(root));
448
- const changedPaths = normalizedPaths([...state.changedPaths, ...currentPaths]).sort();
449
- if (changedPaths.length === 0) {
450
- await refreshProjectIndex(root);
451
- 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);
452
622
  return {};
453
623
  }
454
- await refreshProjectIndex(root);
624
+ await refreshProjectIndexes(repositories);
455
625
  await writeTurnState(statePath, {
456
626
  ...state,
457
627
  phase: 'deslop',
458
- changedPaths,
628
+ repositories,
459
629
  });
460
630
  return {
461
631
  decision: 'block',
462
- reason: await deslopContinuation(root, changedPaths),
632
+ reason: await deslopContinuation(repositories),
463
633
  };
464
634
  }
465
635
 
466
636
  if (state.phase === 'deslop') {
467
- await refreshProjectIndex(root);
468
- await writeTurnState(statePath, {
469
- ...state,
470
- phase: 'summary',
471
- });
472
- return {
473
- decision: 'block',
474
- reason: await finalSummaryContinuation(),
475
- };
637
+ await refreshProjectIndexes(registeredRepositories(state, root));
638
+ await removeTurnState(statePath, input);
639
+ return {};
476
640
  }
477
641
 
478
- await rm(statePath, { force: true });
642
+ await removeTurnState(statePath, input);
479
643
  return {};
480
644
  }
481
645
 
482
646
  export async function discardCodexTurn({ input, projectRoot } = {}) {
483
647
  const root = (await gitContext(projectRoot || input?.cwd)).repositoryRoot;
484
- await rm(await hookStatePath(root, input), { force: true });
648
+ await removeTurnState(await hookStatePath(root, input), input);
485
649
  return { status: 'discarded' };
486
650
  }
@@ -1,12 +0,0 @@
1
- This is the final automatic user-facing summary for the original request.
2
-
3
- Use only the existing conversation context from the implementation and its
4
- follow-up turns. Do not inspect the repository or codebase, call tools, run
5
- commands or tests, edit files, reconcile explanations, or perform cleanup.
6
-
7
- Write one concise, self-contained answer to the user. Lead with the outcome.
8
- Include the important behavior that changed, checks that were actually run,
9
- useful human verification steps, and any genuine blocker or unfinished work.
10
- Do not mention automatic phases, hooks, internal follow-up turns, or these
11
- instructions. Do not claim checks, commits, pushes, or deployments that are not
12
- already established in the conversation. After the answer, stop.