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.
- package/README.md +37 -7
- package/docs/prompt-integration.md +13 -2
- package/docs/stack-components.md +1 -1
- package/package.json +6 -2
- package/plugins/genesis/.codex-plugin/plugin.json +1 -1
- package/prompts/master-pass.txt +17 -0
- package/prompts/pass-deep.txt +77 -0
- package/prompts/pass-report.txt +15 -0
- package/prompts/review.txt +2 -1
- package/skills/genesis-data-pass/SKILL.md +24 -7
- package/skills/genesis-deslop/SKILL.md +25 -3
- package/skills/genesis-identity-pass/SKILL.md +20 -3
- package/skills/genesis-master-pass/SKILL.md +132 -0
- package/skills/genesis-master-pass/agents/openai.yaml +4 -0
- package/skills/genesis-project/references/sanity-checks.md +53 -12
- package/skills/genesis-quality-pass/SKILL.md +21 -3
- package/skills/genesis-review/SKILL.md +120 -0
- package/skills/genesis-review/agents/openai.yaml +4 -0
- package/skills/genesis-security-pass/SKILL.md +21 -3
- package/skills/genesis-speed-pass/SKILL.md +21 -3
- package/skills/genesis-ui-pass/SKILL.md +21 -3
- package/skills/genesis-ux-pass/SKILL.md +20 -3
- package/src/cli-commands.js +3 -1
- package/src/cli.js +12 -4
- package/src/index/agent-skills.js +4 -3
- package/src/index/assets.js +4 -1
- package/src/index/code-index.js +2 -1
- package/src/index/contracts.js +1 -0
- package/src/index/master-pass.js +76 -0
- package/src/index/pass-record.js +101 -24
- package/src/index/pass-support.js +4 -1
- package/src/index/passes.js +55 -1
- package/src/index/project-files.js +2 -1
- package/src/index/prompt.js +221 -37
- package/src/index/source-size.js +62 -0
- package/src/index.js +5 -1
package/src/index/pass-record.js
CHANGED
|
@@ -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>" }
|
|
13
|
-
//
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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,
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
/**
|
|
49
|
-
export async function
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
60
|
-
await
|
|
61
|
-
|
|
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
|
|
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
|
|
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
|
};
|
package/src/index/passes.js
CHANGED
|
@@ -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
|
-
|
|
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', '--', '.']),
|