genesis-compiler 1.10.21 → 1.10.23

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.
Files changed (36) hide show
  1. package/README.md +37 -7
  2. package/docs/prompt-integration.md +13 -2
  3. package/docs/stack-components.md +1 -1
  4. package/package.json +6 -2
  5. package/plugins/genesis/.codex-plugin/plugin.json +1 -1
  6. package/prompts/master-pass.txt +17 -0
  7. package/prompts/pass-deep.txt +77 -0
  8. package/prompts/pass-report.txt +15 -0
  9. package/prompts/review.txt +2 -1
  10. package/skills/genesis-data-pass/SKILL.md +24 -7
  11. package/skills/genesis-deslop/SKILL.md +25 -3
  12. package/skills/genesis-identity-pass/SKILL.md +20 -3
  13. package/skills/genesis-master-pass/SKILL.md +132 -0
  14. package/skills/genesis-master-pass/agents/openai.yaml +4 -0
  15. package/skills/genesis-project/references/sanity-checks.md +53 -12
  16. package/skills/genesis-quality-pass/SKILL.md +21 -3
  17. package/skills/genesis-review/SKILL.md +120 -0
  18. package/skills/genesis-review/agents/openai.yaml +4 -0
  19. package/skills/genesis-security-pass/SKILL.md +21 -3
  20. package/skills/genesis-speed-pass/SKILL.md +21 -3
  21. package/skills/genesis-ui-pass/SKILL.md +21 -3
  22. package/skills/genesis-ux-pass/SKILL.md +20 -3
  23. package/src/cli-commands.js +3 -1
  24. package/src/cli.js +12 -4
  25. package/src/index/agent-skills.js +4 -3
  26. package/src/index/assets.js +4 -1
  27. package/src/index/code-index.js +2 -1
  28. package/src/index/contracts.js +1 -0
  29. package/src/index/master-pass.js +76 -0
  30. package/src/index/pass-record.js +101 -24
  31. package/src/index/pass-support.js +4 -1
  32. package/src/index/passes.js +55 -1
  33. package/src/index/project-files.js +2 -1
  34. package/src/index/prompt.js +221 -37
  35. package/src/index/source-size.js +62 -0
  36. package/src/index.js +5 -1
@@ -1,3 +1,5 @@
1
+ import path from 'node:path';
2
+
1
3
  import { readInstalledAsset } from './assets.js';
2
4
  import { inspectProjectSkills, renderAgentSkillCatalog } from './agent-skills.js';
3
5
  import { buildProjectIndex, MACHINE_CITY_PATH, PROGRAM_CITY_PATH } from './code-index.js';
@@ -22,11 +24,16 @@ import { stableJson } from './utils.js';
22
24
  import { classifyProjectKind } from './project-files.js';
23
25
  import { inspectProjectFormatAtRoot } from './project-format.js';
24
26
  import { inspectProject } from './project-inspection.js';
25
- import { PASSES } from './passes.js';
26
- import { PASS_RECORD_PATH, passRecordCommit, readPassRecord } from './pass-record.js';
27
+ import { masterPassSection, masterRecordStep, masterSelection } from './master-pass.js';
28
+ import { baseRequest, deepRequest, MASTER_PASS, PASSES } from './passes.js';
29
+ import { inspectPassRecord, passRecordCommit, passRecordPath, readPassRecord, unknownRecordMessage } from './pass-record.js';
27
30
  import { projectPassSupport } from './pass-support.js';
31
+ import { changedSince, projectSourceLines, sourceSize } from './source-size.js';
32
+ import { runGitText } from './process.js';
28
33
 
29
- const TASKS = new Set(['start', 'adopt', 'work', ...PASSES.map(({ task }) => task), 'program', 'blueprint', 'describe', 'review']);
34
+ const TASKS = new Set([
35
+ 'start', 'adopt', 'work', ...PASSES.map(({ task }) => task), MASTER_PASS.task, 'program', 'blueprint', 'describe',
36
+ ]);
30
37
  /** Every prompt task Genesis renders, the passes' tasks among them. */
31
38
  export const PROMPT_TASKS = Object.freeze([...TASKS]);
32
39
  // Each task's request when none is given. A pass task asked for bare runs as its pass, except Deslop and the UI pass,
@@ -38,6 +45,7 @@ const DEFAULT_REQUEST = {
38
45
  ...Object.fromEntries(PASSES.map(({ task, request }) => [task, request])),
39
46
  deslop: 'Deslop your own changes for the current task.',
40
47
  'ui-pass': 'UI pass your own changes for the current task.',
48
+ [MASTER_PASS.task]: MASTER_PASS.request,
41
49
  program: 'Refresh the complete useful Program for the code that exists now.',
42
50
  describe: 'Create or refresh the complete Blueprint and useful Program for the codebase that exists now.',
43
51
  review: 'Review the complete useful relationship between Blueprint, code, Program, and tests.',
@@ -166,15 +174,26 @@ function renderPrompt({
166
174
  ...(postChange ? ['', 'SELECTED STACK POST-CHANGE GUIDANCE', '', postChange] : []),
167
175
  ...(passGuidance?.text ? ['', passGuidance.heading, '', passGuidance.text] : []),
168
176
  ...(passGuidance?.run ? ['', 'PASS RUN', '', passRunScope(passGuidance.title, passGuidance.run)] : []),
169
- ...(passGuidance?.record ? ['', 'PASS RECORD', '', passRecordStep(passGuidance.task)] : []),
177
+ ...(passGuidance?.deep ? ['', 'DEEP RUN', '', passGuidance.deep] : []),
178
+ ...(passGuidance?.master ? ['', 'MASTER PASS', '', passGuidance.master] : []),
179
+ ...(passGuidance?.report ? ['', 'PASS REPORT', '', passGuidance.report.trim()] : []),
180
+ ...(passGuidance?.record ? ['', 'PASS RECORD', '', passGuidance.record] : []),
170
181
  '',
171
182
  ].join('\n');
172
183
  }
173
184
 
174
- // The runs a pass offers, each with the request Genesis writes for it: the whole app, always, and, while the commit of
175
- // the pass's last completed run is in HEAD's history, the changes since that commit judged against the whole app.
176
- // Either run records the pass, so a whole run followed by runs since the last one covers every change.
177
- function passRuns(pass, since) {
185
+ // A run's label with its size, in the files and lines its checklist starts from, for a person to see what it takes.
186
+ function sized(label, size) {
187
+ if (!size) return label;
188
+ const text = `${size.files.toLocaleString('en-US')} files, ${size.lines.toLocaleString('en-US')} lines`;
189
+ return label.endsWith(')') ? `${label.slice(0, -1)}; ${text})` : `${label} (${text})`;
190
+ }
191
+
192
+ // The runs a pass offers, each with the request Genesis writes for it: the whole app, always; while the commit of the
193
+ // pass's last completed run is in HEAD's history, the changes since that commit judged against the whole app; and,
194
+ // when the project's own Genesis runs it, the whole app deeply, item by item, sized in a listing by `size`. Every run
195
+ // records the pass, so a whole or deep run followed by runs since the last one covers every change.
196
+ function passRuns(pass, since, deep, size = null) {
178
197
  return [
179
198
  { id: 'whole', label: 'Whole app', request: pass.request },
180
199
  ...(since ? [{
@@ -184,21 +203,72 @@ function passRuns(pass, since) {
184
203
  commit: since.commit,
185
204
  commitsSince: since.commitsSince,
186
205
  }] : []),
206
+ ...(deep ? [{ id: 'deep', label: sized('Deep', size), request: deepRequest(pass), ...size }] : []),
187
207
  ];
188
208
  }
189
209
 
190
- async function projectPassRuns(root, pass, lastRun) {
191
- return passRuns(pass, await passRecordCommit(root, pass.task, lastRun));
210
+ async function projectPassRuns(root, pass, lastRun, deep, size = null) {
211
+ return passRuns(pass, await passRecordCommit(root, pass.task, lastRun), deep, size);
212
+ }
213
+
214
+ // The Master pass's runs: the whole app, and the changes since its last completed run, each with the request for every
215
+ // pass it offers and, as `selectedRequest`, the request for the passes a person chooses, `{passes}` standing for their
216
+ // names separated by commas. In a listing each is sized: the whole app's own source, or the files the changes touch.
217
+ function masterRuns(since, whole = null, changed = null) {
218
+ return passRuns(MASTER_PASS, since, false).map((run) => {
219
+ const size = run.id === 'whole' ? whole : changed;
220
+ return {
221
+ ...run,
222
+ label: sized(run.label, size),
223
+ selectedRequest: run.request.replace(MASTER_PASS.title, `${MASTER_PASS.title} ({passes})`),
224
+ ...size,
225
+ };
226
+ });
227
+ }
228
+
229
+ // The commit of the Master pass's last completed run, and the commits since it, or null.
230
+ function masterSince(root, record) {
231
+ return passRecordCommit(root, MASTER_PASS.task, record[MASTER_PASS.task]?.lastRun ?? null);
232
+ }
233
+
234
+ // A request's last paragraph: a host may frame Genesis's request with its own paragraphs before it.
235
+ function lastParagraph(request) {
236
+ return request.trim().split(/\n[ \t]*\n/u).at(-1).trim();
192
237
  }
193
238
 
194
239
  // The run a request asks for: one of the pass's runs whose request it is, alone or as the last paragraph after a host's
195
240
  // own framing. Genesis wrote those requests, so nothing is read into other wording: any other request, such as one
196
241
  // commit or the agent's own changes, runs the task and is never the pass.
197
242
  function requestedRun(runs, request) {
198
- const last = request.trim().split(/\n[ \t]*\n/u).at(-1).trim();
243
+ const last = lastParagraph(request);
199
244
  return runs.find((run) => run.request === last) ?? null;
200
245
  }
201
246
 
247
+ // A run over the changes since a base commit a host chooses: the request is the pass's base request, or its deep form
248
+ // when `deep`, with a commit hash in place of `{base}`, alone or as the last paragraph after a host's own framing. A
249
+ // base that is not a commit of this repository is refused, so the run never starts from nothing.
250
+ async function baseRun(root, pass, request, deep) {
251
+ const last = lastParagraph(request);
252
+ const templates = [['base', baseRequest(pass)], ...(deep ? [['deep-base', baseRequest(pass, true)]] : [])];
253
+ for (const [id, template] of templates) {
254
+ const [before, after] = template.split('{base}');
255
+ if (!last.startsWith(before) || !last.endsWith(after) || last.length < before.length + after.length) continue;
256
+ const commit = last.slice(before.length, last.length - after.length);
257
+ const found = /^[0-9a-f]{7,64}$/u.test(commit) && await runGitText(root, [
258
+ 'rev-parse', '--verify', '--quiet', `${commit}^{commit}`,
259
+ ]).catch((error) => {
260
+ if (error?.code === 'GIT_EXEC_FAILED') return '';
261
+ throw error;
262
+ });
263
+ if (!found) {
264
+ throw new GenesisError('PASS_REQUEST_INVALID', `The ${pass.title} cannot start from "${commit}": `
265
+ + 'name a commit of this repository by its hash.', { pass: pass.task });
266
+ }
267
+ return { id, request: last, commit };
268
+ }
269
+ return null;
270
+ }
271
+
202
272
  const OWN_SOURCE = 'Generated output, build artifacts, vendored or copied third-party code, dependency directories and '
203
273
  + 'lockfiles are never part of it. This scope replaces the task\'s default scope.';
204
274
 
@@ -208,42 +278,97 @@ function passRunScope(title, run) {
208
278
  return `This request is a ${title} run over the whole app: the project's own source in every subsystem, whoever `
209
279
  + `changed it and whenever. ${OWN_SOURCE}`;
210
280
  }
211
- return `This request is a ${title} run over the changes since commit ${run.commit}, the commit of its last completed `
212
- + `run, judged against the whole app. Start from those changes: \`git diff ${run.commit}\` and the files it touches. `
281
+ if (run.id === 'deep') {
282
+ return `This request is a deep ${title} run over the whole app: the project's own source in every subsystem, `
283
+ + `whoever changed it and whenever, worked through item by item as DEEP RUN says. ${OWN_SOURCE}`;
284
+ }
285
+ const from = {
286
+ 'since-last': 'the commit of its last completed run',
287
+ 'deep-since': `the commit of the last completed ${MASTER_PASS.title}`,
288
+ base: 'the base its request names',
289
+ 'deep-base': 'the base its request names',
290
+ }[run.id];
291
+ if (run.id === 'deep-since' || run.id === 'deep-base') {
292
+ return `This request is a deep ${title} run over the changes since commit ${run.commit}, ${from}, judged against `
293
+ + 'the whole app and worked through item by item as DEEP RUN says. Start from those changes: '
294
+ + `\`git diff ${run.commit}\` and the files it touches. ${OWN_SOURCE}`;
295
+ }
296
+ return `This request is a ${title} run over the changes since commit ${run.commit}, ${from}, judged against the `
297
+ + `whole app. Start from those changes: \`git diff ${run.commit}\` and the files it touches. `
213
298
  + 'Then judge them against the rest of the project\'s own source, because what they duplicate, what they should '
214
299
  + 'reuse and the checks they lack show only there, and make the pass\'s changes wherever that leads: the diff is '
215
300
  + `where the run starts, not its limit. ${OWN_SOURCE}`;
216
301
  }
217
302
 
218
- // A pass task's selected Stack prose under its own heading, the pass run its request asks for, and whether its prompt
219
- // records the pass: only for one of the pass's own runs, and only when the project's own Genesis runs and records it.
220
- // Null for other tasks.
221
- function passPromptGuidance(task, stack, request, support, runs) {
222
- const pass = PASSES.find((entry) => entry.task === task);
303
+ // Where a deep run keeps its checklist, and a Master pass its ledger: in Git's own directory for this worktree, which is
304
+ // never committed or pushed, so they survive an interrupted run and are found again when it continues.
305
+ async function ledgerPath(root, name) {
306
+ return path.resolve(root, await runGitText(root, ['rev-parse', '--git-path', `genesis/passes/${name}.md`]));
307
+ }
308
+
309
+ // How a deep run works, for every pass alike, written once in prompts/pass-deep.txt; each pass's skill adds only its
310
+ // lens's checklist items.
311
+ async function deepRunGuidance(root, task) {
312
+ const [checklist, guidance] = await Promise.all([ledgerPath(root, task), readInstalledAsset('pass-deep')]);
313
+ return `Keep this run's checklist at \`${checklist}\`.\n\n${guidance.trim()}`;
314
+ }
315
+
316
+ // A pass task's selected Stack prose under its own heading, the pass run its request asks for with a deep run's
317
+ // guidance, what every report of the pass states (whatever its scope, so no report overstates its coverage), and its
318
+ // record step: only for one of the pass's own runs, never a run over a base a host chose, and only when the project's
319
+ // own Genesis runs and records it. Null for other tasks.
320
+ function passPromptGuidance({ pass, stack, run, support, report, deep }) {
223
321
  if (!pass) return null;
224
- const run = requestedRun(runs, request);
322
+ const recorded = run && !['base', 'deep-base'].includes(run.id) && support.unavailableFor(pass) === null;
225
323
  return {
226
- task,
227
324
  title: pass.title,
228
325
  heading: pass.heading,
229
326
  text: stack[pass.field],
230
327
  run,
231
- record: Boolean(run) && support.unavailableFor(pass) === null,
328
+ deep,
329
+ report,
330
+ record: recorded ? passRecordStep(pass.task, Boolean(deep)) : '',
232
331
  };
233
332
  }
234
333
 
235
- // A pass's prompt ends with recording the pass, so the record lands in the pass's own commit.
236
- function passRecordStep(task) {
334
+ // A pass's prompt ends with recording the pass, so the record lands in the pass's own commit: with its other changes,
335
+ // left uncommitted, or, for a deep run, which commits its work as it goes, as the run's last commit.
336
+ function passRecordStep(task, committed) {
237
337
  return [
238
338
  'Only once this pass has finished, make your last step the Genesis',
239
339
  `\`passes done ${task}\` operation, invoked as`,
240
340
  `\`.agents/skills/genesis-${task}/SKILL.md\` says. It records the time in`,
241
- `\`${PASS_RECORD_PATH}\`; leave that change uncommitted with the pass's other`,
242
- 'changes, so the record lands in the same commit. Do not run it for a pass that',
341
+ ...(committed ? [
342
+ `\`${passRecordPath(task)}\`; commit that change as the run's last commit, so the`,
343
+ 'record lands in the run\'s own history. Do not run it for a pass that',
344
+ ] : [
345
+ `\`${passRecordPath(task)}\`; leave that change uncommitted with the pass's other`,
346
+ 'changes, so the record lands in the same commit. Do not run it for a pass that',
347
+ ]),
243
348
  'stopped early, was abandoned, or still has failing checks.',
244
349
  ].join('\n');
245
350
  }
246
351
 
352
+ // The passes a Master pass runs: every pass the project's own Genesis runs, in the Master order.
353
+ function masterOffered(support) {
354
+ return MASTER_PASS.order
355
+ .map((task) => PASSES.find((pass) => pass.task === task))
356
+ .filter((pass) => support.unavailableFor(pass) === null);
357
+ }
358
+
359
+ // A Master pass's prompt: the passes its request selects, in order, each with its own Deep run's request, the ledger
360
+ // that lets an interrupted Master pass continue, and its record step. A request that is not one of its runs, or names
361
+ // a pass the project does not offer, is refused here, before anything else is read.
362
+ async function masterPromptGuidance({ root, request, support }) {
363
+ const record = await readPassRecord(root);
364
+ const offered = masterOffered(support);
365
+ const { run, passes } = masterSelection(lastParagraph(request), masterRuns(await masterSince(root, record)), offered);
366
+ return {
367
+ master: masterPassSection({ run, passes, ledger: await ledgerPath(root, MASTER_PASS.task) }),
368
+ record: masterRecordStep(),
369
+ };
370
+ }
371
+
247
372
  function collaborationPromptContext(collaboration) {
248
373
  return {
249
374
  path: collaboration.path,
@@ -512,9 +637,15 @@ export async function inspectProjectPromptTasks({ projectRoot, stackPackages = [
512
637
  * The passes a person can run, so a host lists them without naming any: each with its prompt task as `id`, a title,
513
638
  * a one-line description, a recommendation hint the host interprets itself, the pass's own request (the whole app),
514
639
  * its workflow skill, whether the selected Stack adds its own guidance to it, when it last finished (`lastRun`, a UTC
515
- * time from genesis/passes.json, or null), why the project's own Genesis cannot run it (`unavailable`, naming the
640
+ * time from the pass record, or null), why the project's own Genesis cannot run it (`unavailable`, naming the
516
641
  * update, or null) and the runs it offers (`runs`, none when unavailable), each with a label and the request a host
517
- * sends for it as written. The top-level `unavailable` says why that Genesis runs no pass at all, or is null.
642
+ * sends for it as written. `sinceBase` holds the requests of a run over the changes since a base commit the host
643
+ * chooses, `{base}` standing for the commit: `request`, and `deep` when the project's own Genesis runs Deep runs (or
644
+ * null); Genesis never records those runs. It is null when the pass cannot run there, except for a self-contained pass,
645
+ * whose prompt needs nothing from that Genesis. The top-level `unavailable` says why that Genesis runs no pass at all,
646
+ * or is null.
647
+ * `master` is the Master pass: the passes it runs, in its order, when it last finished and with which passes, why it
648
+ * cannot run, and its runs, each with the request for every pass and `selectedRequest` for the passes a person chooses.
518
649
  */
519
650
  export async function inspectProjectPasses({ projectRoot, stackPackages = [] } = {}) {
520
651
  const root = (await gitContext(projectRoot)).repositoryRoot;
@@ -523,6 +654,13 @@ export async function inspectProjectPasses({ projectRoot, stackPackages = [] } =
523
654
  readPassRecord(root),
524
655
  projectPassSupport(root),
525
656
  ]);
657
+ const masterUnavailable = support.unavailableFor(MASTER_PASS);
658
+ const masterRecord = record[MASTER_PASS.task] ?? null;
659
+ // One walk of the project's own source sizes every deep run and the Master pass.
660
+ const sources = support.deep ? await projectSourceLines(root, stack) : null;
661
+ const whole = sources && sourceSize(sources);
662
+ const since = masterUnavailable ? null : await masterSince(root, record);
663
+ const changed = since && sources && sourceSize(sources, await changedSince(root, since.commit));
526
664
  return {
527
665
  contract: GENESIS_CONTRACTS.passes,
528
666
  unavailable: support.unavailable,
@@ -539,9 +677,25 @@ export async function inspectProjectPasses({ projectRoot, stackPackages = [] } =
539
677
  stackGuidance: Boolean(stack[pass.field]),
540
678
  lastRun,
541
679
  unavailable,
542
- runs: unavailable ? [] : await projectPassRuns(root, pass, lastRun),
680
+ runs: unavailable ? [] : await projectPassRuns(root, pass, lastRun, support.deep, whole),
681
+ sinceBase: unavailable && !pass.selfContained ? null : {
682
+ request: baseRequest(pass),
683
+ deep: !unavailable && support.deep ? baseRequest(pass, true) : null,
684
+ },
543
685
  };
544
686
  })),
687
+ master: {
688
+ id: MASTER_PASS.task,
689
+ title: MASTER_PASS.title,
690
+ description: MASTER_PASS.description,
691
+ request: MASTER_PASS.request,
692
+ skill: `genesis-${MASTER_PASS.task}`,
693
+ lastRun: masterRecord?.lastRun ?? null,
694
+ lastPasses: masterRecord?.passes ?? null,
695
+ unavailable: masterUnavailable,
696
+ passes: masterUnavailable ? [] : masterOffered(support).map(({ task }) => task),
697
+ runs: masterUnavailable ? [] : masterRuns(since, whole, changed),
698
+ },
545
699
  };
546
700
  }
547
701
 
@@ -559,23 +713,44 @@ export async function generateProjectPrompt({
559
713
  }
560
714
  const root = (await gitContext(projectRoot)).repositoryRoot;
561
715
  const userRequest = requestText(request, task);
562
- // A pass task's prompt sends the agent to the project's own Genesis skill for it, so that Genesis must have it.
716
+ // A pass task's prompt sends the agent to the project's own Genesis skill for it, so that Genesis must have it; a
717
+ // Master pass runs other passes deeply through that Genesis, so it must run them too. A self-contained pass's prompt
718
+ // needs no skill, so without it the task still runs, with no run of its own but one over a base.
563
719
  const pass = PASSES.find((entry) => entry.task === task);
564
- const passSupport = pass ? await projectPassSupport(root) : null;
565
- if (pass && !passSupport.has(pass)) {
720
+ const master = task === MASTER_PASS.task;
721
+ const passSupport = pass || master ? await projectPassSupport(root) : null;
722
+ const ownRuns = Boolean(pass) && passSupport.has(pass);
723
+ if (pass && !ownRuns && !pass.selfContained) {
566
724
  throw new GenesisError('PASS_UNAVAILABLE', passSupport.unavailableFor(pass), { pass: task });
567
725
  }
568
- // The pass's runs, to recognise one of their requests; a malformed record leaves only the whole run, with a warning.
726
+ if (master && passSupport.unavailableFor(MASTER_PASS)) {
727
+ throw new GenesisError('PASS_UNAVAILABLE', passSupport.unavailableFor(MASTER_PASS), { pass: task });
728
+ }
729
+ // The pass's runs, to recognise one of their requests; a malformed record leaves only the whole and deep runs, with a
730
+ // warning. A Master pass since its last completed run runs each pass deeply over the same changes, so that request is
731
+ // the pass's too. Any other request may be a run over a base the host chose.
569
732
  const passWarnings = [];
570
- let runs = pass ? passRuns(pass, null) : [];
571
- if (pass) {
733
+ let runs = ownRuns ? passRuns(pass, null, passSupport.deep) : [];
734
+ if (ownRuns) {
572
735
  try {
573
- runs = await projectPassRuns(root, pass, (await readPassRecord(root))[task] ?? null);
736
+ const { record, unknown } = await inspectPassRecord(root);
737
+ if (unknown.length) {
738
+ passWarnings.push(asDiagnostic(new GenesisError('PASS_RECORD_UNKNOWN', unknownRecordMessage(unknown))));
739
+ }
740
+ runs = await projectPassRuns(root, pass, record[task] ?? null, passSupport.deep);
741
+ const since = passSupport.deep ? await masterSince(root, record) : null;
742
+ if (since) runs.push({ id: 'deep-since', request: deepRequest(pass, since.commit), commit: since.commit });
574
743
  } catch (error) {
575
744
  if (error?.code !== 'PASS_RECORD_INVALID') throw error;
576
745
  passWarnings.push(asDiagnostic(error));
577
746
  }
578
747
  }
748
+ const run = pass
749
+ ? requestedRun(runs, userRequest) ?? await baseRun(root, pass, userRequest, ownRuns && passSupport.deep)
750
+ : null;
751
+ const masterGuidance = master
752
+ ? await masterPromptGuidance({ root, request: userRequest, support: passSupport })
753
+ : null;
579
754
  const openingInspection = task === 'start' ? await inspectProject({ projectRoot: root, stackPackages }) : null;
580
755
  if (openingInspection) {
581
756
  const inspection = openingInspection;
@@ -585,10 +760,12 @@ export async function generateProjectPrompt({
585
760
  warnings: inspection.diagnostics, verificationCommands: [] };
586
761
  }
587
762
  }
588
- const [instructions, collaboration, engineering] = await Promise.all([
763
+ const [instructions, collaboration, engineering, passReport, deepGuidance] = await Promise.all([
589
764
  readInstalledAsset(task),
590
765
  collaborationForPrompt(root),
591
766
  readEngineering(root),
767
+ pass || master ? readInstalledAsset('pass-report') : '',
768
+ ['deep', 'deep-since', 'deep-base'].includes(run?.id) ? deepRunGuidance(root, task) : '',
592
769
  ]);
593
770
  const sessionPrompt = stableSessionPromptParts({
594
771
  collaboration,
@@ -732,7 +909,14 @@ export async function generateProjectPrompt({
732
909
  guidance: stack.guidance,
733
910
  skills: renderAgentSkillCatalog(projectSkills.skills),
734
911
  postChange: task === 'work' ? stack.postChange : '',
735
- passGuidance: passPromptGuidance(task, stack, userRequest, passSupport, runs),
912
+ passGuidance: masterGuidance ? { ...masterGuidance, report: passReport } : passPromptGuidance({
913
+ pass,
914
+ stack,
915
+ run,
916
+ support: passSupport,
917
+ report: passReport,
918
+ deep: deepGuidance,
919
+ }),
736
920
  });
737
921
  return {
738
922
  status: 'ready',
@@ -0,0 +1,62 @@
1
+ import { lstat, readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { isProjectContentPath } from './paths.js';
5
+ import { runGit } from './process.js';
6
+ import { visiblePaths } from './project-files.js';
7
+
8
+ // Written by package managers, never the project's own source.
9
+ const LOCKFILES = new Set([
10
+ 'Cargo.lock', 'Gemfile.lock', 'Package.resolved', 'Pipfile.lock', 'Podfile.lock', 'bun.lock', 'bun.lockb',
11
+ 'composer.lock', 'go.sum', 'gradle.lockfile', 'mix.lock', 'npm-shrinkwrap.json', 'package-lock.json',
12
+ 'packages.lock.json', 'pnpm-lock.yaml', 'poetry.lock', 'pubspec.lock', 'uv.lock', 'yarn.lock',
13
+ ]);
14
+ const BATCH = 32;
15
+
16
+ /** The folders the selected Stack discovers packages in, such as dependency folders it declares. */
17
+ export function stackPackageRoots(stack) {
18
+ return stack.packageDiscovery.discoveries.flatMap((discovery) => discovery.manifests
19
+ .map((manifest) => path.posix.join(path.posix.dirname(manifest), discovery.packageRoot)));
20
+ }
21
+
22
+ function lineCount(content) {
23
+ let lines = 0;
24
+ for (let index = content.indexOf(10); index !== -1; index = content.indexOf(10, index + 1)) lines += 1;
25
+ return content.length > 0 && content.at(-1) !== 10 ? lines + 1 : lines;
26
+ }
27
+
28
+ /**
29
+ * The project's own source a pass covers, each file with its line count: Git-visible text files outside Genesis's
30
+ * own files, the Stack's package folders and lockfiles. One walk, one Git listing and one read per file, whatever is
31
+ * sized from it, so a listing costs what the project's files cost, never that times its passes.
32
+ * @returns {Promise<Map<string, number>>}
33
+ */
34
+ export async function projectSourceLines(root, stack) {
35
+ const packageRoots = stackPackageRoots(stack);
36
+ const files = (await visiblePaths(root)).filter((file) => isProjectContentPath(file)
37
+ && !LOCKFILES.has(path.posix.basename(file))
38
+ && !packageRoots.some((directory) => file === directory || file.startsWith(`${directory}/`)));
39
+ const lines = new Map();
40
+ for (let index = 0; index < files.length; index += BATCH) {
41
+ await Promise.all(files.slice(index, index + BATCH).map(async (file) => {
42
+ const full = path.join(root, file);
43
+ // A link, folder or binary file is not source an agent reads line by line.
44
+ if (!(await lstat(full)).isFile()) return;
45
+ const content = await readFile(full);
46
+ if (!content.includes(0)) lines.set(file, lineCount(content));
47
+ }));
48
+ }
49
+ return new Map([...lines].sort(([a], [b]) => (a < b ? -1 : 1)));
50
+ }
51
+
52
+ /** The number of files and lines among `sources`, of all of them or of only `files`. */
53
+ export function sourceSize(sources, files = [...sources.keys()]) {
54
+ const sized = files.filter((file) => sources.has(file));
55
+ return { files: sized.length, lines: sized.reduce((total, file) => total + sources.get(file), 0) };
56
+ }
57
+
58
+ /** The files the changes since `commit` touch, as `git diff <commit>` shows them. */
59
+ export async function changedSince(root, commit) {
60
+ const { stdout } = await runGit(root, ['diff', '--name-only', '-z', commit, '--']);
61
+ return stdout.toString('utf8').split('\0').filter(Boolean);
62
+ }
package/src/index.js CHANGED
@@ -202,7 +202,11 @@ export function inspectPasses(options) {
202
202
  return inspectProjectPasses(options);
203
203
  }
204
204
 
205
- /** Records in genesis/passes.json that the pass `pass` finished now; a pass's own last step. */
205
+ /**
206
+ * Records that the pass `pass` finished now, in genesis/passes.json for the passes Genesis 1.10.21 knew and in
207
+ * genesis/passes-added.json for every pass added since; a pass's own last step. The Master pass also names the passes
208
+ * it ran (`passes`), which it records with it.
209
+ */
206
210
  export function recordPassDone(options) {
207
211
  return recordProjectPassDone(options);
208
212
  }