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
@@ -3,28 +3,50 @@ import path from 'node:path';
3
3
 
4
4
  import { fail } from './errors.js';
5
5
  import { gitContext } from './git.js';
6
- import { PASSES } from './passes.js';
6
+ import { MASTER_PASS, PASSES } from './passes.js';
7
7
  import { runGitText } from './process.js';
8
8
  import { inspectProjectFormatAtRoot } from './project-format.js';
9
9
  import { stableJson, writeFileAtomic } from './utils.js';
10
10
 
11
11
  // When each pass last finished, committed with the project so every clone and host sees the same record:
12
- // `{ "<pass id>": "<UTC time>" }`. The pass's own last step writes it, so it lands in the pass's commit and a pass
13
- // that never finished is never recorded.
12
+ // `{ "<pass id>": "<UTC time>" }`, and the Master pass's time with the passes it ran,
13
+ // `"master-pass": { "lastRun": "<UTC time>", "passes": ["<pass id>", ...] }`. The pass's own last step writes it, so
14
+ // it lands in the pass's commit and a pass that never finished is never recorded.
15
+ // Two files, so that a Genesis older than a pass never meets its record: genesis-compiler 1.10.21 and earlier read only
16
+ // `genesis/passes.json` and refuse an id they do not know, so that file keeps the passes they know, and every pass
17
+ // added since, the Master pass among them, is recorded in `genesis/passes-added.json`. This Genesis keeps an entry it
18
+ // does not know, which a newer Genesis wrote, and leaves it unused, so a pass added later never strands it.
14
19
  export const PASS_RECORD_PATH = 'genesis/passes.json';
20
+ export const ADDED_PASS_RECORD_PATH = 'genesis/passes-added.json';
15
21
 
22
+ const FIRST_PASS_IDS = Object.freeze([
23
+ 'deslop', 'ui-pass', 'identity-pass', 'quality-pass', 'data-pass', 'security-pass', 'ux-pass', 'speed-pass',
24
+ ]);
16
25
  const PASS_IDS = PASSES.map(({ task }) => task);
26
+ const RECORDED_IDS = [...PASS_IDS, MASTER_PASS.task];
17
27
  const UTC_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u;
18
28
 
19
- function invalidRecord(problem) {
20
- 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 });
29
+ /** The file that records `pass`. */
30
+ export function passRecordPath(pass) {
31
+ return FIRST_PASS_IDS.includes(pass) ? PASS_RECORD_PATH : ADDED_PASS_RECORD_PATH;
21
32
  }
22
33
 
23
- /** The project's pass record: each recorded pass id with the UTC time it last finished. A missing file records none. */
24
- export async function readPassRecord(root) {
34
+ function invalidRecord(file, problem) {
35
+ fail('PASS_RECORD_INVALID', `${file} ${problem}. It maps pass ids to UTC times such as 2026-01-31T09:30:00Z, and ${MASTER_PASS.task} to { "lastRun": <UTC time>, "passes": [<pass id>, ...] }; fix or remove the entry.`, { path: file });
36
+ }
37
+
38
+ const utcTime = (value) => typeof value === 'string' && UTC_TIME.test(value) && !Number.isNaN(Date.parse(value));
39
+
40
+ // The passes a Master pass ran: pass ids, each once, at least one. A record may name a pass a newer Genesis added; a
41
+ // Master pass recorded here names only passes this Genesis has.
42
+ const passList = (value, known = null) => Array.isArray(value) && value.length > 0
43
+ && new Set(value).size === value.length
44
+ && value.every((id) => typeof id === 'string' && (!known || known.includes(id)));
45
+
46
+ async function readRecordFile(root, file) {
25
47
  let source;
26
48
  try {
27
- source = await readFile(path.join(root, PASS_RECORD_PATH), 'utf8');
49
+ source = await readFile(path.join(root, file), 'utf8');
28
50
  } catch (error) {
29
51
  if (error?.code === 'ENOENT') return {};
30
52
  throw error;
@@ -33,38 +55,92 @@ export async function readPassRecord(root) {
33
55
  try {
34
56
  record = JSON.parse(source);
35
57
  } catch (error) {
36
- invalidRecord(`is not JSON (${error.message})`);
58
+ invalidRecord(file, `is not JSON (${error.message})`);
37
59
  }
38
- if (record === null || typeof record !== 'object' || Array.isArray(record)) invalidRecord('is not a JSON object');
39
- for (const [id, time] of Object.entries(record)) {
40
- if (!PASS_IDS.includes(id)) invalidRecord(`names ${JSON.stringify(id)}, which is not a pass`);
41
- if (typeof time !== 'string' || !UTC_TIME.test(time) || Number.isNaN(Date.parse(time))) {
42
- invalidRecord(`records ${JSON.stringify(time)} for ${id}, which is not a UTC time`);
60
+ if (record === null || typeof record !== 'object' || Array.isArray(record)) invalidRecord(file, 'is not a JSON object');
61
+ return record;
62
+ }
63
+
64
+ /**
65
+ * The project's pass record, from both files: each recorded pass id with the UTC time it last finished, and the Master
66
+ * pass's time with the passes it ran; and, as `unknown`, the entries this Genesis does not know, which it keeps and
67
+ * does not use. Missing files record none.
68
+ * @returns {Promise<{ record: object, unknown: { id: string, path: string }[] }>}
69
+ */
70
+ export async function inspectPassRecord(root) {
71
+ const record = {};
72
+ const unknown = [];
73
+ for (const file of [PASS_RECORD_PATH, ADDED_PASS_RECORD_PATH]) {
74
+ for (const [id, value] of Object.entries(await readRecordFile(root, file))) {
75
+ if (!RECORDED_IDS.includes(id)) {
76
+ unknown.push({ id, path: file });
77
+ continue;
78
+ }
79
+ if (passRecordPath(id) !== file) invalidRecord(file, `records ${id}, which belongs in ${passRecordPath(id)}; move the entry there`);
80
+ if (id === MASTER_PASS.task) {
81
+ if (!utcTime(value?.lastRun) || !passList(value?.passes) || Object.keys(value).length !== 2) {
82
+ invalidRecord(file, `records ${JSON.stringify(value)} for ${id}, which is not a UTC time with the passes it ran`);
83
+ }
84
+ } else if (!utcTime(value)) {
85
+ invalidRecord(file, `records ${JSON.stringify(value)} for ${id}, which is not a UTC time`);
86
+ }
87
+ record[id] = value;
43
88
  }
44
89
  }
45
- return record;
90
+ return { record, unknown };
46
91
  }
47
92
 
48
- /** Records that `pass` finished now, as its last step. */
49
- export async function recordProjectPassDone({ projectRoot, pass, now = new Date() } = {}) {
50
- if (!PASS_IDS.includes(pass)) {
51
- fail('PASS_UNKNOWN', `${JSON.stringify(pass)} is not a pass; use one of ${PASS_IDS.join(', ')}.`);
93
+ /** The project's pass record: each pass id this Genesis knows with its last finished run. */
94
+ export async function readPassRecord(root) {
95
+ return (await inspectPassRecord(root)).record;
96
+ }
97
+
98
+ /** What a reader says about entries it does not know: a newer Genesis wrote them, and updating Genesis uses them. */
99
+ export function unknownRecordMessage(unknown) {
100
+ const names = unknown.map(({ id, path: file }) => `${id} (${file})`).join(', ');
101
+ return `The pass record names passes this Genesis does not know: ${names}. A newer Genesis recorded them; they are `
102
+ + 'kept as they are. Update genesis-compiler to the release that has them to use those records.';
103
+ }
104
+
105
+ /**
106
+ * Records that `pass` finished now, as its last step; the Master pass records the passes it ran with it, in the order
107
+ * it runs them. Entries this Genesis does not know stay as they are.
108
+ */
109
+ export async function recordProjectPassDone({ projectRoot, pass, passes = [], now = new Date() } = {}) {
110
+ if (!RECORDED_IDS.includes(pass)) {
111
+ fail('PASS_UNKNOWN', `${JSON.stringify(pass)} is not a pass this Genesis knows; use one of ${RECORDED_IDS.join(', ')}, or update genesis-compiler to the release that added it.`);
112
+ }
113
+ const master = pass === MASTER_PASS.task;
114
+ if (master ? !passList(passes, PASS_IDS) : passes.length > 0) {
115
+ fail('PASS_RECORD_PASSES_INVALID', master
116
+ ? `The ${MASTER_PASS.title} records the passes it ran, each once: name at least one of ${PASS_IDS.join(', ')}.`
117
+ : `Only the ${MASTER_PASS.title} records the passes it ran; record ${pass} on its own.`);
52
118
  }
53
119
  const root = (await gitContext(projectRoot)).repositoryRoot;
54
120
  const format = await inspectProjectFormatAtRoot(root);
55
121
  if (format.status !== 'current') {
56
122
  fail('PASS_RECORD_PROJECT_REQUIRED', `Passes are recorded in a current Genesis project; this one is ${format.status} (next action: ${format.action}).`);
57
123
  }
124
+ await inspectPassRecord(root);
125
+ const file = passRecordPath(pass);
58
126
  const lastRun = now.toISOString().replace(/\.\d{3}Z$/u, 'Z');
59
- const record = { ...await readPassRecord(root), [pass]: lastRun };
60
- await writeFileAtomic(path.join(root, PASS_RECORD_PATH), stableJson(record));
61
- return { status: 'recorded', pass, lastRun, path: PASS_RECORD_PATH, changedFiles: [PASS_RECORD_PATH] };
127
+ const ran = MASTER_PASS.order.filter((id) => passes.includes(id));
128
+ const entries = { ...await readRecordFile(root, file), [pass]: master ? { lastRun, passes: ran } : lastRun };
129
+ await writeFileAtomic(path.join(root, file), stableJson(entries));
130
+ return {
131
+ status: 'recorded',
132
+ pass,
133
+ lastRun,
134
+ ...(master ? { passes: ran } : {}),
135
+ path: file,
136
+ changedFiles: [file],
137
+ };
62
138
  }
63
139
 
64
140
  /**
65
141
  * The commit that recorded a pass's last completed run, which is the pass's own commit, and the commits since it on
66
142
  * HEAD; null when the run is not committed or that commit is no longer in HEAD's history. Git keeps it: the commit
67
- * that added this exact entry to the record file, so nothing else is stored.
143
+ * that added this exact entry to the pass's record file (for the Master pass, its time), so nothing else is stored.
68
144
  * @param {string} root the project's repository root
69
145
  * @param {string} pass the pass id
70
146
  * @param {string | null} lastRun the run's UTC time, from readPassRecord
@@ -79,7 +155,8 @@ export async function passRecordCommit(root, pass, lastRun) {
79
155
  if (error?.code === 'GIT_EXEC_FAILED') return null;
80
156
  throw error;
81
157
  }
82
- const commit = await runGitText(root, ['log', '-1', '--format=%h', `-S"${pass}": "${lastRun}"`, 'HEAD', '--', PASS_RECORD_PATH]);
158
+ const entry = pass === MASTER_PASS.task ? `"lastRun": "${lastRun}"` : `"${pass}": "${lastRun}"`;
159
+ const commit = await runGitText(root, ['log', '-1', '--format=%h', `-S${entry}`, 'HEAD', '--', passRecordPath(pass)]);
83
160
  if (!commit) return null;
84
161
  const commitsSince = Number(await runGitText(root, ['rev-list', '--count', `${commit}..HEAD`]));
85
162
  return { commit, commitsSince };
@@ -25,7 +25,8 @@ async function readManifest(directory) {
25
25
  * Which of this Genesis's passes the project's own Genesis runs. A pass's prompt sends the agent to that Genesis's
26
26
  * workflow skill, and its PASS RECORD step to that Genesis's `passes done`, so that Genesis alone decides, from what
27
27
  * it ships: it runs passes when its package declares this pass-run contract (`genesisContracts.passRuns`), and it has
28
- * a pass when its skills folder holds the pass's workflow skill. A project pinned to a release without either, or
28
+ * a pass when its skills folder holds the pass's workflow skill, and it runs each pass's Deep run when its package also
29
+ * declares the deep-run contract (`genesisContracts.deepRuns`). A project pinned to a release without either, or
29
30
  * whose pinned release is not installed, is told plainly, with the step that fixes it; nothing else stands in for
30
31
  * that release.
31
32
  * @param {string} projectRoot the project's repository root
@@ -62,6 +63,8 @@ export async function projectPassSupport(projectRoot) {
62
63
  unavailable,
63
64
  /** Whether the project's Genesis has the pass's task, which its prompt needs even when no pass is recorded. */
64
65
  has,
66
+ /** Whether the project's Genesis runs each pass's Deep run: its package declares this deep-run contract. */
67
+ deep: unavailable === null && own.genesisContracts.deepRuns === GENESIS_CONTRACTS.deepRuns,
65
68
  /** Why the project's Genesis cannot run this pass, naming the update; null when it can. */
66
69
  unavailableFor: (pass) => unavailable ?? (has(pass) ? null : `${named} has no ${pass.title}. ${update}`),
67
70
  };
@@ -6,7 +6,11 @@
6
6
  * The task's request when none is given is the task's own (prompt.js), which for Deslop and the UI pass stays the
7
7
  * agent's own changes.
8
8
  * A host lists the passes with inspectPasses() and decides itself when to offer one: `recommend` is a hint
9
- * (`when`: `after-change`, `first-version` or `periodic`), never a schedule.
9
+ * (`when`: `after-change`, `first-version` or `periodic`), never a schedule. A host may also run a pass over the
10
+ * changes since a base commit it chooses, such as where a piece of work started (baseRequest); Genesis never records
11
+ * those runs, so they never move the pass's last completed run.
12
+ * `selfContained` marks a pass whose prompt carries all its instructions, without its skill: it was a task before it
13
+ * was a pass, so it still runs as that task, and over a base, where the project's own Genesis lacks the pass.
10
14
  */
11
15
  export const PASSES = Object.freeze([
12
16
  {
@@ -89,4 +93,54 @@ export const PASSES = Object.freeze([
89
93
  field: 'speedPass',
90
94
  heading: 'SELECTED STACK SPEED PASS GUIDANCE',
91
95
  },
96
+ {
97
+ task: 'review',
98
+ title: 'Review',
99
+ description: 'Checks the work against what the app should do: a test that would fail without each behavior, nothing weakened or skipped, and gaps between intent, code and explanation, changing only tests.',
100
+ recommend: { when: 'after-change', hint: 'after each saved change' },
101
+ request: 'Review the whole app.',
102
+ section: 'Review',
103
+ field: 'review',
104
+ heading: 'SELECTED STACK REVIEW GUIDANCE',
105
+ selfContained: true,
106
+ },
92
107
  ].map((pass) => Object.freeze({ ...pass, recommend: Object.freeze(pass.recommend) })));
108
+
109
+ /**
110
+ * The Master pass: the passes run deeply, one after another, each as its own Deep run, committed and tested before the
111
+ * next starts. It runs every pass the project offers, or the ones a request names, always in `order`, whatever order
112
+ * they were named in: the review first, so every later pass starts from tested work, then security, so later passes
113
+ * build on the access it fixes, and Deslop last, so it cleans up after what the others changed. Every pass has a place
114
+ * in `order`; a pass added later takes one there.
115
+ */
116
+ export const MASTER_PASS = Object.freeze({
117
+ task: 'master-pass',
118
+ title: 'Master pass',
119
+ description: 'Runs every pass deeply, one after another, or the passes you choose, committing and running the whole test suite after each.',
120
+ request: 'Master pass the whole app.',
121
+ order: Object.freeze([
122
+ 'review', 'security-pass', 'data-pass', 'quality-pass', 'speed-pass', 'ux-pass', 'identity-pass', 'ui-pass', 'deslop',
123
+ ]),
124
+ });
125
+
126
+ /**
127
+ * The request of a pass's Deep run: its pass request deeply, or, for a Master pass since its last completed run, the
128
+ * changes since that commit deeply, judged against the whole app.
129
+ * @param {{ title: string, request: string }} pass
130
+ * @param {string | null} [commit] the commit of the Master pass's last completed run, for a run since it
131
+ */
132
+ export function deepRequest(pass, commit = null) {
133
+ return commit
134
+ ? `${pass.title} deeply: the changes since commit ${commit}, judged against the whole app.`
135
+ : pass.request.replace(/\.$/u, ' deeply.');
136
+ }
137
+
138
+ /**
139
+ * The request of a run over the changes since a base commit a host chooses, judged against the whole app, or of its
140
+ * deep form; `{base}` stands for the commit. Genesis never records these runs.
141
+ * @param {{ title: string }} pass
142
+ * @param {boolean} [deep]
143
+ */
144
+ export function baseRequest(pass, deep = false) {
145
+ return `${pass.title}${deep ? ' deeply' : ''}: the changes since base commit {base}, judged against the whole app.`;
146
+ }
@@ -4,7 +4,8 @@ import { runGit } from './process.js';
4
4
  import { pathState } from './utils.js';
5
5
  import { inspectProjectContent } from './project-inspection.js';
6
6
 
7
- async function visiblePaths(projectRoot) {
7
+ /** Git-visible paths: tracked and untracked files that are not ignored or deleted, sorted. */
8
+ export async function visiblePaths(projectRoot) {
8
9
  const [visible, deleted] = await Promise.all([
9
10
  runGit(projectRoot, ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '.']),
10
11
  runGit(projectRoot, ['ls-files', '-z', '--deleted', '--', '.']),