genesis-compiler 1.10.20 → 1.10.22

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 (43) hide show
  1. package/README.md +74 -16
  2. package/docs/prompt-integration.md +19 -5
  3. package/docs/stack-components.md +22 -5
  4. package/package.json +11 -1
  5. package/plugins/genesis/.codex-plugin/plugin.json +1 -1
  6. package/prompts/deslop.txt +3 -0
  7. package/prompts/master-pass.txt +17 -0
  8. package/prompts/pass-deep.txt +77 -0
  9. package/prompts/pass-report.txt +15 -0
  10. package/prompts/review.txt +2 -1
  11. package/prompts/security-pass.txt +26 -0
  12. package/prompts/speed-pass.txt +28 -0
  13. package/prompts/ui-pass.txt +3 -1
  14. package/prompts/ux-pass.txt +25 -0
  15. package/skills/genesis-data-pass/SKILL.md +29 -4
  16. package/skills/genesis-deslop/SKILL.md +47 -6
  17. package/skills/genesis-identity-pass/SKILL.md +29 -4
  18. package/skills/genesis-master-pass/SKILL.md +132 -0
  19. package/skills/genesis-master-pass/agents/openai.yaml +4 -0
  20. package/skills/genesis-quality-pass/SKILL.md +30 -4
  21. package/skills/genesis-review/SKILL.md +120 -0
  22. package/skills/genesis-review/agents/openai.yaml +4 -0
  23. package/skills/genesis-security-pass/SKILL.md +297 -0
  24. package/skills/genesis-security-pass/agents/openai.yaml +4 -0
  25. package/skills/genesis-speed-pass/SKILL.md +351 -0
  26. package/skills/genesis-speed-pass/agents/openai.yaml +4 -0
  27. package/skills/genesis-ui-pass/SKILL.md +38 -7
  28. package/skills/genesis-ux-pass/SKILL.md +276 -0
  29. package/skills/genesis-ux-pass/agents/openai.yaml +4 -0
  30. package/src/cli-commands.js +3 -1
  31. package/src/cli.js +14 -4
  32. package/src/index/agent-skills.js +53 -29
  33. package/src/index/assets.js +4 -1
  34. package/src/index/code-index.js +2 -1
  35. package/src/index/contracts.js +2 -0
  36. package/src/index/master-pass.js +76 -0
  37. package/src/index/pass-record.js +124 -22
  38. package/src/index/pass-support.js +71 -0
  39. package/src/index/passes.js +92 -5
  40. package/src/index/project-files.js +2 -1
  41. package/src/index/prompt.js +306 -28
  42. package/src/index/source-size.js +62 -0
  43. package/src/index.js +6 -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';
@@ -12,7 +14,7 @@ import { readStack, stackPromptContext } from './stack.js';
12
14
  import { listStackCatalogPieces } from './stack-catalog.js';
13
15
  import { normalizeStackPieceId } from './stack-piece.js';
14
16
  import { GENESIS_CONTRACTS } from './contracts.js';
15
- import { GenesisError } from './errors.js';
17
+ import { asDiagnostic, GenesisError } from './errors.js';
16
18
  import { gitContext } from './git.js';
17
19
  import { inspectProgram } from './program.js';
18
20
  import { inspectVerification } from './project-state.js';
@@ -22,15 +24,28 @@ 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, 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';
30
+ import { projectPassSupport } from './pass-support.js';
31
+ import { changedSince, projectSourceLines, sourceSize } from './source-size.js';
32
+ import { runGitText } from './process.js';
27
33
 
28
- 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
+ ]);
37
+ /** Every prompt task Genesis renders, the passes' tasks among them. */
38
+ export const PROMPT_TASKS = Object.freeze([...TASKS]);
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,
40
+ // which bare work on the agent's own changes for the current task; their passes, like every pass, cover the whole app.
29
41
  const DEFAULT_REQUEST = {
30
42
  start: 'Start a conversation about this project.',
31
43
  adopt: 'Import the existing project into a truthful Genesis project contract.',
32
44
  work: 'Implement the product intent expressed by the current Blueprint.',
33
45
  ...Object.fromEntries(PASSES.map(({ task, request }) => [task, request])),
46
+ deslop: 'Deslop your own changes for the current task.',
47
+ 'ui-pass': 'UI pass your own changes for the current task.',
48
+ [MASTER_PASS.task]: MASTER_PASS.request,
34
49
  program: 'Refresh the complete useful Program for the code that exists now.',
35
50
  describe: 'Create or refresh the complete Blueprint and useful Program for the codebase that exists now.',
36
51
  review: 'Review the complete useful relationship between Blueprint, code, Program, and tests.',
@@ -158,29 +173,202 @@ function renderPrompt({
158
173
  ...(skills ? ['', 'AVAILABLE AGENT SKILLS', '', skills] : []),
159
174
  ...(postChange ? ['', 'SELECTED STACK POST-CHANGE GUIDANCE', '', postChange] : []),
160
175
  ...(passGuidance?.text ? ['', passGuidance.heading, '', passGuidance.text] : []),
161
- ...(passGuidance ? ['', 'PASS RECORD', '', passRecordStep(passGuidance.task)] : []),
176
+ ...(passGuidance?.run ? ['', 'PASS RUN', '', passRunScope(passGuidance.title, passGuidance.run)] : []),
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] : []),
162
181
  '',
163
182
  ].join('\n');
164
183
  }
165
184
 
166
- // A pass task's selected Stack prose under its own heading; null for every other task.
167
- function passPromptGuidance(task, stack) {
168
- const pass = PASSES.find((entry) => entry.task === task);
169
- return pass ? { task, heading: pass.heading, text: stack[pass.field] } : null;
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})`;
170
190
  }
171
191
 
172
- // Every pass prompt ends with recording the pass, so the record lands in the pass's own commit.
173
- function passRecordStep(task) {
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) {
197
+ return [
198
+ { id: 'whole', label: 'Whole app', request: pass.request },
199
+ ...(since ? [{
200
+ id: 'since-last',
201
+ label: `Since last completed (${since.commit}, ${since.commitsSince} ${since.commitsSince === 1 ? 'commit' : 'commits'})`,
202
+ request: `${pass.title}: the changes since commit ${since.commit}, judged against the whole app.`,
203
+ commit: since.commit,
204
+ commitsSince: since.commitsSince,
205
+ }] : []),
206
+ ...(deep ? [{ id: 'deep', label: sized('Deep', size), request: deepRequest(pass), ...size }] : []),
207
+ ];
208
+ }
209
+
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();
237
+ }
238
+
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
240
+ // own framing. Genesis wrote those requests, so nothing is read into other wording: any other request, such as one
241
+ // commit or the agent's own changes, runs the task and is never the pass.
242
+ function requestedRun(runs, request) {
243
+ const last = lastParagraph(request);
244
+ return runs.find((run) => run.request === last) ?? null;
245
+ }
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
+
272
+ const OWN_SOURCE = 'Generated output, build artifacts, vendored or copied third-party code, dependency directories and '
273
+ + 'lockfiles are never part of it. This scope replaces the task\'s default scope.';
274
+
275
+ // What a pass run covers, for every pass alike.
276
+ function passRunScope(title, run) {
277
+ if (run.id === 'whole') {
278
+ return `This request is a ${title} run over the whole app: the project's own source in every subsystem, whoever `
279
+ + `changed it and whenever. ${OWN_SOURCE}`;
280
+ }
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. `
298
+ + 'Then judge them against the rest of the project\'s own source, because what they duplicate, what they should '
299
+ + 'reuse and the checks they lack show only there, and make the pass\'s changes wherever that leads: the diff is '
300
+ + `where the run starts, not its limit. ${OWN_SOURCE}`;
301
+ }
302
+
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 }) {
321
+ if (!pass) return null;
322
+ const recorded = run && !['base', 'deep-base'].includes(run.id) && support.unavailableFor(pass) === null;
323
+ return {
324
+ title: pass.title,
325
+ heading: pass.heading,
326
+ text: stack[pass.field],
327
+ run,
328
+ deep,
329
+ report,
330
+ record: recorded ? passRecordStep(pass.task, Boolean(deep)) : '',
331
+ };
332
+ }
333
+
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) {
174
337
  return [
175
338
  'Only once this pass has finished, make your last step the Genesis',
176
339
  `\`passes done ${task}\` operation, invoked as`,
177
340
  `\`.agents/skills/genesis-${task}/SKILL.md\` says. It records the time in`,
178
- `\`${PASS_RECORD_PATH}\`; leave that change uncommitted with the pass's other`,
179
- '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
+ ]),
180
348
  'stopped early, was abandoned, or still has failing checks.',
181
349
  ].join('\n');
182
350
  }
183
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
+
184
372
  function collaborationPromptContext(collaboration) {
185
373
  return {
186
374
  path: collaboration.path,
@@ -447,25 +635,67 @@ export async function inspectProjectPromptTasks({ projectRoot, stackPackages = [
447
635
 
448
636
  /**
449
637
  * The passes a person can run, so a host lists them without naming any: each with its prompt task as `id`, a title,
450
- * a one-line description, a recommendation hint the host interprets itself, the request used when none is given, its
451
- * workflow skill, whether the selected Stack adds its own guidance to it, and when it last finished (`lastRun`, a UTC
452
- * time from genesis/passes.json, or null).
638
+ * a one-line description, a recommendation hint the host interprets itself, the pass's own request (the whole app),
639
+ * its workflow skill, whether the selected Stack adds its own guidance to it, when it last finished (`lastRun`, a UTC
640
+ * time from the pass record, or null), why the project's own Genesis cannot run it (`unavailable`, naming the
641
+ * update, or null) and the runs it offers (`runs`, none when unavailable), each with a label and the request a host
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.
453
649
  */
454
650
  export async function inspectProjectPasses({ projectRoot, stackPackages = [] } = {}) {
455
651
  const root = (await gitContext(projectRoot)).repositoryRoot;
456
- const [stack, record] = await Promise.all([readStack(root, { stackPackages }), readPassRecord(root)]);
652
+ const [stack, record, support] = await Promise.all([
653
+ readStack(root, { stackPackages }),
654
+ readPassRecord(root),
655
+ projectPassSupport(root),
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));
457
664
  return {
458
665
  contract: GENESIS_CONTRACTS.passes,
459
- passes: PASSES.map(({ task, title, description, recommend, request, field }) => ({
460
- id: task,
461
- title,
462
- description,
463
- recommend: { ...recommend },
464
- request,
465
- skill: `genesis-${task}`,
466
- stackGuidance: Boolean(stack[field]),
467
- lastRun: record[task] ?? null,
666
+ unavailable: support.unavailable,
667
+ passes: await Promise.all(PASSES.map(async (pass) => {
668
+ const lastRun = record[pass.task] ?? null;
669
+ const unavailable = support.unavailableFor(pass);
670
+ return {
671
+ id: pass.task,
672
+ title: pass.title,
673
+ description: pass.description,
674
+ recommend: { ...pass.recommend },
675
+ request: pass.request,
676
+ skill: `genesis-${pass.task}`,
677
+ stackGuidance: Boolean(stack[pass.field]),
678
+ lastRun,
679
+ unavailable,
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
+ },
685
+ };
468
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
+ },
469
699
  };
470
700
  }
471
701
 
@@ -483,6 +713,44 @@ export async function generateProjectPrompt({
483
713
  }
484
714
  const root = (await gitContext(projectRoot)).repositoryRoot;
485
715
  const userRequest = requestText(request, task);
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.
719
+ const pass = PASSES.find((entry) => entry.task === task);
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) {
724
+ throw new GenesisError('PASS_UNAVAILABLE', passSupport.unavailableFor(pass), { pass: task });
725
+ }
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.
732
+ const passWarnings = [];
733
+ let runs = ownRuns ? passRuns(pass, null, passSupport.deep) : [];
734
+ if (ownRuns) {
735
+ try {
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 });
743
+ } catch (error) {
744
+ if (error?.code !== 'PASS_RECORD_INVALID') throw error;
745
+ passWarnings.push(asDiagnostic(error));
746
+ }
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;
486
754
  const openingInspection = task === 'start' ? await inspectProject({ projectRoot: root, stackPackages }) : null;
487
755
  if (openingInspection) {
488
756
  const inspection = openingInspection;
@@ -492,10 +760,12 @@ export async function generateProjectPrompt({
492
760
  warnings: inspection.diagnostics, verificationCommands: [] };
493
761
  }
494
762
  }
495
- const [instructions, collaboration, engineering] = await Promise.all([
763
+ const [instructions, collaboration, engineering, passReport, deepGuidance] = await Promise.all([
496
764
  readInstalledAsset(task),
497
765
  collaborationForPrompt(root),
498
766
  readEngineering(root),
767
+ pass || master ? readInstalledAsset('pass-report') : '',
768
+ ['deep', 'deep-since', 'deep-base'].includes(run?.id) ? deepRunGuidance(root, task) : '',
499
769
  ]);
500
770
  const sessionPrompt = stableSessionPromptParts({
501
771
  collaboration,
@@ -610,6 +880,7 @@ export async function generateProjectPrompt({
610
880
  ...missing,
611
881
  ...projectSkills.diagnostics,
612
882
  ...index.diagnostics,
883
+ ...passWarnings,
613
884
  ];
614
885
  const context = {
615
886
  task,
@@ -638,7 +909,14 @@ export async function generateProjectPrompt({
638
909
  guidance: stack.guidance,
639
910
  skills: renderAgentSkillCatalog(projectSkills.skills),
640
911
  postChange: task === 'work' ? stack.postChange : '',
641
- passGuidance: passPromptGuidance(task, stack),
912
+ passGuidance: masterGuidance ? { ...masterGuidance, report: passReport } : passPromptGuidance({
913
+ pass,
914
+ stack,
915
+ run,
916
+ support: passSupport,
917
+ report: passReport,
918
+ deep: deepGuidance,
919
+ }),
642
920
  });
643
921
  return {
644
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
@@ -40,6 +40,7 @@ export { inspectAgentIntegrations, syncAgentIntegrations } from './index/agent-i
40
40
  export { listParsers, installParsers, verifyParsers, parserEnvironment } from './index/code-indexers/parsers.js';
41
41
  export { listTemplates } from './index/template-catalog.js';
42
42
  export { applyTemplate } from './index/template-project.js';
43
+ export { PROMPT_TASKS } from './index/prompt.js';
43
44
  import {
44
45
  HOST_CONTEXT_RESOLVER_DATA_ENV,
45
46
  HOST_CONTEXT_RESOLVER_ENV,
@@ -201,7 +202,11 @@ export function inspectPasses(options) {
201
202
  return inspectProjectPasses(options);
202
203
  }
203
204
 
204
- /** 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
+ */
205
210
  export function recordPassDone(options) {
206
211
  return recordProjectPassDone(options);
207
212
  }