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/prompt.js
CHANGED
|
@@ -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 {
|
|
26
|
-
import {
|
|
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([
|
|
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?.
|
|
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
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
|
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.
|
|
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
|
|
565
|
-
|
|
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
|
-
|
|
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 =
|
|
571
|
-
if (
|
|
733
|
+
let runs = ownRuns ? passRuns(pass, null, passSupport.deep) : [];
|
|
734
|
+
if (ownRuns) {
|
|
572
735
|
try {
|
|
573
|
-
|
|
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:
|
|
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
|
-
/**
|
|
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
|
}
|