genesis-compiler 1.10.18 → 1.10.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.
@@ -1,5 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
 
3
+ import { PASSES } from './passes.js';
4
+
3
5
  const root = new URL('../../', import.meta.url);
4
6
  const assets = {
5
7
  start: new URL('prompts/start.txt', root),
@@ -9,8 +11,7 @@ const assets = {
9
11
  adopt: new URL('prompts/adopt.txt', root),
10
12
  work: new URL('prompts/work.txt', root),
11
13
  describe: new URL('prompts/describe.txt', root),
12
- deslop: new URL('prompts/deslop.txt', root),
13
- 'ui-pass': new URL('prompts/ui-pass.txt', root),
14
+ ...Object.fromEntries(PASSES.map(({ task }) => [task, new URL(`prompts/${task}.txt`, root)])),
14
15
  program: new URL('prompts/program.txt', root),
15
16
  blueprint: new URL('prompts/blueprint.txt', root),
16
17
  review: new URL('prompts/review.txt', root),
@@ -2,6 +2,7 @@ export const GENESIS_CONTRACTS = Object.freeze({
2
2
  subsystems: 'genesis.subsystems.v0',
3
3
  projectInspection: 'genesis.project-inspection.v1',
4
4
  promptTasks: 'genesis.prompt-tasks.v1',
5
+ passes: 'genesis.passes.v1',
5
6
  templates: 'genesis.templates.v1',
6
7
  templateApplication: 'genesis.template-application.v1',
7
8
  collaboration: 'genesis.collaboration.v1',
@@ -0,0 +1,61 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { fail } from './errors.js';
5
+ import { gitContext } from './git.js';
6
+ import { PASSES } from './passes.js';
7
+ import { inspectProjectFormatAtRoot } from './project-format.js';
8
+ import { stableJson, writeFileAtomic } from './utils.js';
9
+
10
+ // When each pass last finished, committed with the project so every clone and host sees the same record:
11
+ // `{ "<pass id>": "<UTC time>" }`. The pass's own last step writes it, so it lands in the pass's commit and a pass
12
+ // that never finished is never recorded.
13
+ export const PASS_RECORD_PATH = 'genesis/passes.json';
14
+
15
+ const PASS_IDS = PASSES.map(({ task }) => task);
16
+ const UTC_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u;
17
+
18
+ function invalidRecord(problem) {
19
+ fail('PASS_RECORD_INVALID', `${PASS_RECORD_PATH} ${problem}. It maps pass ids (${PASS_IDS.join(', ')}) to UTC times such as 2026-01-31T09:30:00Z; fix or remove the entry.`, { path: PASS_RECORD_PATH });
20
+ }
21
+
22
+ /** The project's pass record: each recorded pass id with the UTC time it last finished. A missing file records none. */
23
+ export async function readPassRecord(root) {
24
+ let source;
25
+ try {
26
+ source = await readFile(path.join(root, PASS_RECORD_PATH), 'utf8');
27
+ } catch (error) {
28
+ if (error?.code === 'ENOENT') return {};
29
+ throw error;
30
+ }
31
+ let record;
32
+ try {
33
+ record = JSON.parse(source);
34
+ } catch (error) {
35
+ invalidRecord(`is not JSON (${error.message})`);
36
+ }
37
+ if (record === null || typeof record !== 'object' || Array.isArray(record)) invalidRecord('is not a JSON object');
38
+ for (const [id, time] of Object.entries(record)) {
39
+ if (!PASS_IDS.includes(id)) invalidRecord(`names ${JSON.stringify(id)}, which is not a pass`);
40
+ if (typeof time !== 'string' || !UTC_TIME.test(time) || Number.isNaN(Date.parse(time))) {
41
+ invalidRecord(`records ${JSON.stringify(time)} for ${id}, which is not a UTC time`);
42
+ }
43
+ }
44
+ return record;
45
+ }
46
+
47
+ /** Records that `pass` finished now, as its last step. */
48
+ export async function recordProjectPassDone({ projectRoot, pass, now = new Date() } = {}) {
49
+ if (!PASS_IDS.includes(pass)) {
50
+ fail('PASS_UNKNOWN', `${JSON.stringify(pass)} is not a pass; use one of ${PASS_IDS.join(', ')}.`);
51
+ }
52
+ const root = (await gitContext(projectRoot)).repositoryRoot;
53
+ const format = await inspectProjectFormatAtRoot(root);
54
+ if (format.status !== 'current') {
55
+ fail('PASS_RECORD_PROJECT_REQUIRED', `Passes are recorded in a current Genesis project; this one is ${format.status} (next action: ${format.action}).`);
56
+ }
57
+ const lastRun = now.toISOString().replace(/\.\d{3}Z$/u, 'Z');
58
+ const record = { ...await readPassRecord(root), [pass]: lastRun };
59
+ await writeFileAtomic(path.join(root, PASS_RECORD_PATH), stableJson(record));
60
+ return { status: 'recorded', pass, lastRun, path: PASS_RECORD_PATH, changedFiles: [PASS_RECORD_PATH] };
61
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The passes: operations a person asks for by name over a scope of the project, never run because a turn finished.
3
+ * Each is a prompt task (`task`) with a workflow skill (`genesis-<task>`) and a Stack section (`section`), whose prose
4
+ * the selected components add to the task's prompt under `heading`. `request` is the request when none is given.
5
+ * A host lists the passes with inspectPasses() and decides itself when to offer one: `recommend` is a hint
6
+ * (`when`: `after-change`, `first-version` or `periodic`), never a schedule.
7
+ */
8
+ export const PASSES = Object.freeze([
9
+ {
10
+ task: 'deslop',
11
+ title: 'Deslop',
12
+ description: 'Simplifies changes without changing what they do: no repeated helpers, unclear code or code that ignores the project\'s patterns.',
13
+ recommend: { when: 'after-change', hint: 'after each saved change' },
14
+ request: 'Deslop your own changes for the current task.',
15
+ section: 'Deslop',
16
+ field: 'deslop',
17
+ heading: 'SELECTED STACK CLEANUP GUIDANCE',
18
+ },
19
+ {
20
+ task: 'ui-pass',
21
+ title: 'UI pass',
22
+ description: 'Makes screens follow the design rules of the project\'s technology exactly, without changing what they do.',
23
+ recommend: { when: 'after-change', hint: 'after a change to the screens' },
24
+ request: 'UI pass your own changes for the current task.',
25
+ section: 'UI pass',
26
+ field: 'uiPass',
27
+ heading: 'SELECTED STACK UI PASS GUIDANCE',
28
+ },
29
+ {
30
+ task: 'identity-pass',
31
+ title: 'Visual identity',
32
+ description: 'Gives the app a look of its own: one seed colour, artwork drawn for its content, a logo, empty states, a loading screen and a sign-in page.',
33
+ recommend: { when: 'first-version', hint: 'once a first working version exists' },
34
+ request: 'Give the whole app its visual identity.',
35
+ section: 'Identity pass',
36
+ field: 'identityPass',
37
+ heading: 'SELECTED STACK IDENTITY PASS GUIDANCE',
38
+ },
39
+ {
40
+ task: 'quality-pass',
41
+ title: 'Quality pass',
42
+ description: 'Makes the app hold up in use: loading, empty and error states, edge cases, refusals the person understands and accessibility, checked by using it.',
43
+ recommend: { when: 'periodic', hint: 'after a stretch of changes, about every few weeks of active work' },
44
+ request: 'Quality pass the whole app.',
45
+ section: 'Quality pass',
46
+ field: 'qualityPass',
47
+ heading: 'SELECTED STACK QUALITY PASS GUIDANCE',
48
+ },
49
+ {
50
+ task: 'data-pass',
51
+ title: 'Data pass',
52
+ description: 'Makes data hold up at real sizes: queries that grow with the rows read, enforced constraints, safe migrations and indexes for real filters, tested at realistic sizes.',
53
+ recommend: { when: 'periodic', hint: 'after a stretch of changes, about every few weeks of active work' },
54
+ request: 'Data pass the whole app.',
55
+ section: 'Data pass',
56
+ field: 'dataPass',
57
+ heading: 'SELECTED STACK DATA PASS GUIDANCE',
58
+ },
59
+ ].map((pass) => Object.freeze({ ...pass, recommend: Object.freeze(pass.recommend) })));
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { isDeepStrictEqual } from 'node:util';
4
4
 
5
+ import { CORE_SKILL_NAMES } from './agent-skills.js';
5
6
  import { readBlueprint } from './blueprint.js';
6
7
  import { readCollaboration } from './collaboration.js';
7
8
  import { GENESIS_DERIVED_ARTIFACTS_GITIGNORE_LINES } from './code-index.js';
@@ -29,7 +30,7 @@ export const GENESIS_BOOTSTRAP_PATHS = Object.freeze([
29
30
  '.claude/settings.json',
30
31
  '.opencode/plugins/genesis-project-guidance.js',
31
32
  '.agents/skills/.genesis-managed.json',
32
- ...['genesis-project', 'genesis-program', 'genesis-deslop', 'genesis-ui-pass'].flatMap((name) => [
33
+ ...CORE_SKILL_NAMES.flatMap((name) => [
33
34
  `.claude/skills/${name}`,
34
35
  `.agents/skills/${name}/SKILL.md`,
35
36
  `.agents/skills/${name}/agents/openai.yaml`,
@@ -22,14 +22,15 @@ import { stableJson } from './utils.js';
22
22
  import { classifyProjectKind } from './project-files.js';
23
23
  import { inspectProjectFormatAtRoot } from './project-format.js';
24
24
  import { inspectProject } from './project-inspection.js';
25
+ import { PASSES } from './passes.js';
26
+ import { PASS_RECORD_PATH, readPassRecord } from './pass-record.js';
25
27
 
26
- const TASKS = new Set(['start', 'adopt', 'work', 'deslop', 'ui-pass', 'program', 'blueprint', 'describe', 'review']);
28
+ const TASKS = new Set(['start', 'adopt', 'work', ...PASSES.map(({ task }) => task), 'program', 'blueprint', 'describe', 'review']);
27
29
  const DEFAULT_REQUEST = {
28
30
  start: 'Start a conversation about this project.',
29
31
  adopt: 'Import the existing project into a truthful Genesis project contract.',
30
32
  work: 'Implement the product intent expressed by the current Blueprint.',
31
- deslop: 'Deslop your own changes for the current task.',
32
- 'ui-pass': 'UI pass your own changes for the current task.',
33
+ ...Object.fromEntries(PASSES.map(({ task, request }) => [task, request])),
33
34
  program: 'Refresh the complete useful Program for the code that exists now.',
34
35
  describe: 'Create or refresh the complete Blueprint and useful Program for the codebase that exists now.',
35
36
  review: 'Review the complete useful relationship between Blueprint, code, Program, and tests.',
@@ -132,8 +133,7 @@ function renderPrompt({
132
133
  context,
133
134
  skills = '',
134
135
  guidance = '',
135
- cleanup = '',
136
- uiPass = '',
136
+ passGuidance = null,
137
137
  postChange = '',
138
138
  adoption = '',
139
139
  collaborationGuidance = '',
@@ -157,12 +157,30 @@ function renderPrompt({
157
157
  ...(adoption ? ['', 'SELECTED STACK ADOPTION GUIDANCE', '', adoption] : []),
158
158
  ...(skills ? ['', 'AVAILABLE AGENT SKILLS', '', skills] : []),
159
159
  ...(postChange ? ['', 'SELECTED STACK POST-CHANGE GUIDANCE', '', postChange] : []),
160
- ...(cleanup ? ['', 'SELECTED STACK CLEANUP GUIDANCE', '', cleanup] : []),
161
- ...(uiPass ? ['', 'SELECTED STACK UI PASS GUIDANCE', '', uiPass] : []),
160
+ ...(passGuidance?.text ? ['', passGuidance.heading, '', passGuidance.text] : []),
161
+ ...(passGuidance ? ['', 'PASS RECORD', '', passRecordStep(passGuidance.task)] : []),
162
162
  '',
163
163
  ].join('\n');
164
164
  }
165
165
 
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;
170
+ }
171
+
172
+ // Every pass prompt ends with recording the pass, so the record lands in the pass's own commit.
173
+ function passRecordStep(task) {
174
+ return [
175
+ 'Only once this pass has finished, make your last step the Genesis',
176
+ `\`passes done ${task}\` operation, invoked as`,
177
+ `\`.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',
180
+ 'stopped early, was abandoned, or still has failing checks.',
181
+ ].join('\n');
182
+ }
183
+
166
184
  function collaborationPromptContext(collaboration) {
167
185
  return {
168
186
  path: collaboration.path,
@@ -410,19 +428,47 @@ async function generateStartPrompt({
410
428
 
411
429
  /**
412
430
  * The prompt tasks Genesis renders, each with whether the selected Stack adds its own guidance to that task (Adoption,
413
- * Post-change, Deslop or UI pass prose). A host offers a Stack-dependent operation, such as an automatic UI pass,
431
+ * Post-change or a pass's prose). A host offers a Stack-dependent operation, such as an automatic UI pass,
414
432
  * from this answer instead of inferring it from component ids.
415
433
  */
416
434
  export async function inspectProjectPromptTasks({ projectRoot, stackPackages = [] } = {}) {
417
435
  const root = (await gitContext(projectRoot)).repositoryRoot;
418
436
  const stack = await readStack(root, { stackPackages });
419
- const lanes = { adopt: stack.adoption, work: stack.postChange, deslop: stack.deslop, 'ui-pass': stack.uiPass };
437
+ const lanes = {
438
+ adopt: stack.adoption,
439
+ work: stack.postChange,
440
+ ...Object.fromEntries(PASSES.map(({ task, field }) => [task, stack[field]])),
441
+ };
420
442
  return {
421
443
  contract: GENESIS_CONTRACTS.promptTasks,
422
444
  tasks: [...TASKS].map((task) => ({ task, stackGuidance: Boolean(lanes[task]) })),
423
445
  };
424
446
  }
425
447
 
448
+ /**
449
+ * 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).
453
+ */
454
+ export async function inspectProjectPasses({ projectRoot, stackPackages = [] } = {}) {
455
+ const root = (await gitContext(projectRoot)).repositoryRoot;
456
+ const [stack, record] = await Promise.all([readStack(root, { stackPackages }), readPassRecord(root)]);
457
+ return {
458
+ 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,
468
+ })),
469
+ };
470
+ }
471
+
426
472
  export async function generateProjectPrompt({
427
473
  environment = process.env,
428
474
  hiddenStackPieces = [],
@@ -592,8 +638,7 @@ export async function generateProjectPrompt({
592
638
  guidance: stack.guidance,
593
639
  skills: renderAgentSkillCatalog(projectSkills.skills),
594
640
  postChange: task === 'work' ? stack.postChange : '',
595
- cleanup: task === 'deslop' ? stack.deslop : '',
596
- uiPass: task === 'ui-pass' ? stack.uiPass : '',
641
+ passGuidance: passPromptGuidance(task, stack),
597
642
  });
598
643
  return {
599
644
  status: 'ready',
@@ -2,6 +2,7 @@ import { GenesisError } from './errors.js';
2
2
  import { parseStackCityPresentationLines } from './stack-city-presentation.js';
3
3
  import { parseStackEnvironmentDefaultLines } from './stack-environment-defaults.js';
4
4
  import { parseStackEnvironmentFileLines } from './stack-environment-files.js';
5
+ import { PASSES } from './passes.js';
5
6
  import { opaqueStackSections } from './stack-section.js';
6
7
  import { parseStackVerificationLines } from './stack-verification.js';
7
8
  import { normalizeSource } from './utils.js';
@@ -15,15 +16,16 @@ const STACK_CUSTOMIZATION_FIELDS = new Map([
15
16
  ['Guidance', 'guidance'],
16
17
  ['Adoption', 'adoption'],
17
18
  ['Post-change', 'postChange'],
18
- ['Deslop', 'deslop'],
19
- ['UI pass', 'uiPass'],
19
+ ...PASSES.map(({ section, field }) => [section, field]),
20
20
  ]);
21
+ const CUSTOMIZATION_FIELD_LIST = [...STACK_CUSTOMIZATION_FIELDS.keys()]
22
+ .map((name, index, names) => (index === names.length - 1 ? `or ${name}` : name))
23
+ .join(', ');
21
24
  const STACK_PIECE_SECTIONS = new Set([
22
25
  'Adoption',
23
26
  'City regions',
24
27
  'Conflicts',
25
28
  'Description',
26
- 'Deslop',
27
29
  'Environment defaults',
28
30
  'Environment files',
29
31
  'Guidance',
@@ -32,8 +34,8 @@ const STACK_PIECE_SECTIONS = new Set([
32
34
  'Requires',
33
35
  'Resources',
34
36
  'Skill',
35
- 'UI pass',
36
37
  'Verification',
38
+ ...PASSES.map(({ section }) => section),
37
39
  ]);
38
40
 
39
41
  export function normalizeStackPieceId(value, {
@@ -117,12 +119,12 @@ function customizationSections(source, piecePath) {
117
119
  if (/^#{1,3}\s+/u.test(line)) invalid(piecePath, `Unknown customization heading: ${line.trim()}.`);
118
120
  if (field) content.push(line);
119
121
  else if (line.trim()) {
120
- invalid(piecePath, 'Customization content must be below Description, Guidance, Adoption, Post-change, Deslop, or UI pass.');
122
+ invalid(piecePath, `Customization content must be below ${CUSTOMIZATION_FIELD_LIST}.`);
121
123
  }
122
124
  }
123
125
  finishField();
124
126
  if (Object.values(result).every((values) => Object.keys(values).length === 0)) {
125
- invalid(piecePath, 'Stack customization must add or override Description, Guidance, Adoption, Post-change, Deslop, or UI pass.');
127
+ invalid(piecePath, `Stack customization must add or override ${CUSTOMIZATION_FIELD_LIST}.`);
126
128
  }
127
129
  return result;
128
130
  }
@@ -330,8 +332,6 @@ export function parseStackPieceSource(value, {
330
332
  const guidance = textSection(all, 'Guidance');
331
333
  const adoption = textSection(all, 'Adoption');
332
334
  const postChange = textSection(all, 'Post-change');
333
- const deslop = textSection(all, 'Deslop');
334
- const uiPass = textSection(all, 'UI pass');
335
335
  if (!description) invalid(piecePath, 'Stack piece needs Description.');
336
336
  const verificationCommands = parseStackVerificationLines(
337
337
  all.get('Verification') || [],
@@ -366,8 +366,7 @@ export function parseStackPieceSource(value, {
366
366
  environmentDefaults,
367
367
  environmentFiles: environmentFiles || [],
368
368
  extensions: opaqueStackSections(all, STACK_PIECE_SECTIONS),
369
- deslop,
370
- uiPass,
369
+ ...Object.fromEntries(PASSES.map(({ section, field }) => [field, textSection(all, section)])),
371
370
  };
372
371
  }
373
372
 
@@ -415,7 +414,9 @@ export function applyStackPieceCustomization(piece, customization) {
415
414
  customization.override.postChange,
416
415
  customization.add.postChange,
417
416
  ),
418
- deslop: customizedText(piece.deslop, customization.override.deslop, customization.add.deslop),
419
- uiPass: customizedText(piece.uiPass, customization.override.uiPass, customization.add.uiPass),
417
+ ...Object.fromEntries(PASSES.map(({ field }) => [
418
+ field,
419
+ customizedText(piece[field], customization.override[field], customization.add[field]),
420
+ ])),
420
421
  };
421
422
  }
@@ -38,6 +38,7 @@ import {
38
38
  parseStackPieceCustomizationSource,
39
39
  parseStackResourceLines,
40
40
  } from './stack-piece.js';
41
+ import { PASSES } from './passes.js';
41
42
  import { STACK_PATH } from './paths.js';
42
43
  import { sha256, stableJson, writeFileAtomic } from './utils.js';
43
44
 
@@ -413,8 +414,7 @@ async function readStackWithContractMode(projectRoot, {
413
414
  guidance: composedProse(components, 'guidance'),
414
415
  adoption: composedProse(components, 'adoption'),
415
416
  postChange: composedProse(components, 'postChange'),
416
- deslop: composedProse(components, 'deslop'),
417
- uiPass: composedProse(components, 'uiPass'),
417
+ ...Object.fromEntries(PASSES.map(({ field }) => [field, composedProse(components, field)])),
418
418
  };
419
419
  }
420
420
 
package/src/index.js CHANGED
@@ -12,7 +12,8 @@ import {
12
12
  } from './index/collaboration.js';
13
13
  import { GENESIS_CONTRACTS } from './index/contracts.js';
14
14
  import { contextForProjectPaths } from './index/context.js';
15
- import { generateProjectPrompt, inspectProjectPromptTasks } from './index/prompt.js';
15
+ import { generateProjectPrompt, inspectProjectPasses, inspectProjectPromptTasks } from './index/prompt.js';
16
+ import { recordProjectPassDone } from './index/pass-record.js';
16
17
  import {
17
18
  inspectProjectEngineering,
18
19
  listEngineeringProfileCatalog,
@@ -196,6 +197,15 @@ export function inspectPromptTasks(options) {
196
197
  return inspectProjectPromptTasks(options);
197
198
  }
198
199
 
200
+ export function inspectPasses(options) {
201
+ return inspectProjectPasses(options);
202
+ }
203
+
204
+ /** Records in genesis/passes.json that the pass `pass` finished now; a pass's own last step. */
205
+ export function recordPassDone(options) {
206
+ return recordProjectPassDone(options);
207
+ }
208
+
199
209
  export function getContext({ paths, projectRoot = process.cwd(), stackPackages = [] } = {}) {
200
210
  return contextForProjectPaths({ paths, projectRoot, stackPackages });
201
211
  }