pi-crew 0.9.48 → 0.9.50

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 (86) hide show
  1. package/AGENTS.md +18 -0
  2. package/CHANGELOG.md +314 -0
  3. package/dist/build-meta.json +70 -42
  4. package/dist/index.mjs +505 -430
  5. package/dist/index.mjs.map +4 -4
  6. package/docs/decisions/2026-07-24-oidc-trusted-publishing.md +112 -0
  7. package/package.json +2 -3
  8. package/skills/.gitkeep +0 -0
  9. package/skills/distill-persona/BUILD-NOTES.md +55 -0
  10. package/skills/distill-persona/SKILL.md +550 -0
  11. package/skills/distill-persona/UPGRADE-LOG-RESEARCH-SKILLS.md +100 -0
  12. package/skills/distill-persona/references/coverage-manifest.md +65 -0
  13. package/skills/distill-persona/references/cross-skill-differentiation.md +12 -0
  14. package/skills/distill-persona/references/description-discipline.md +6 -0
  15. package/skills/distill-persona/references/diagnostic-path.md +25 -0
  16. package/skills/distill-persona/references/distillation-field-synthesis-pass2.md +59 -0
  17. package/skills/distill-persona/references/distillation-field-synthesis.md +108 -0
  18. package/skills/distill-persona/references/fidelity-rubric.md +19 -0
  19. package/skills/distill-persona/references/field-models.md +20 -0
  20. package/skills/distill-persona/references/handoff.md +42 -0
  21. package/skills/distill-persona/references/optional-body-sections.md +9 -0
  22. package/skills/distill-persona/references/registry-routing.md +11 -0
  23. package/skills/distill-persona/references/research/lesson-memory-shortcut.md +33 -0
  24. package/skills/distill-persona/references/research/r1-a-examples.md +23 -0
  25. package/skills/distill-persona/references/research/r1-b-scripts.md +26 -0
  26. package/skills/distill-persona/references/research/r1-c-human-readme.md +31 -0
  27. package/skills/distill-persona/references/research/r1-d-tests.md +28 -0
  28. package/skills/distill-persona/references/research/r1-verification.md +36 -0
  29. package/skills/distill-persona/references/research/r2-low-yield.md +26 -0
  30. package/skills/distill-persona/references/self-upgrade-directive.md +20 -0
  31. package/skills/distill-persona/references/taste-principles.md +8 -0
  32. package/skills/distill-persona/references/topic-variant.md +13 -0
  33. package/skills/distill-persona/references/update-mode.md +7 -0
  34. package/skills/distill-persona/scripts/fidelity_eval.py +244 -0
  35. package/skills/distill-persona/scripts/validate-run.mjs +297 -0
  36. package/skills/distill-persona/scripts/validate-skill-structure.mjs +177 -0
  37. package/skills/distill-software/BUILD-NOTES.md +56 -0
  38. package/skills/distill-software/SKILL.md +363 -0
  39. package/skills/distill-software/references/handoff.md +47 -0
  40. package/skills/distill-software/scripts/code_dna.py +290 -0
  41. package/skills/research/DISTILLATION-PROCESS-CHECKLIST.md +120 -0
  42. package/skills/research/EXCAVATION-CHECKLIST.md +142 -0
  43. package/skills/research/FIDELITY.md +180 -0
  44. package/skills/research/SKILL.md +432 -0
  45. package/skills/research/references/anti-patterns.md +184 -0
  46. package/skills/research/references/fidelity.md +241 -0
  47. package/skills/research/references/handoff.md +48 -0
  48. package/skills/research/references/research-protocol.md +162 -0
  49. package/skills/research/references/source-inventory.md +135 -0
  50. package/skills/research/references/verified-models.md +163 -0
  51. package/skills/research/scripts/__pycache__/safe_io.cpython-312.pyc +0 -0
  52. package/skills/research/scripts/code_dna.py +233 -0
  53. package/skills/research/scripts/emit_run_summary.py +142 -0
  54. package/skills/research/scripts/safe_io.py +314 -0
  55. package/skills/research/scripts/source_evaluator.py +234 -0
  56. package/skills/research/scripts/validate-skill-structure.mjs +177 -0
  57. package/skills/research/scripts/verify_citations.py +225 -0
  58. package/skills/security-priority.json +28 -0
  59. package/src/config/config.ts +1 -0
  60. package/src/config/role-tools.ts +6 -3
  61. package/src/config/types.ts +8 -0
  62. package/src/extension/crew-cleanup.ts +18 -1
  63. package/src/extension/crew-vibes/index.ts +11 -2
  64. package/src/extension/register.ts +1 -1
  65. package/src/extension/registration/command-registration.ts +1 -0
  66. package/src/extension/registration/commands.ts +7 -3
  67. package/src/extension/registration/lifecycle-handlers.ts +1 -3
  68. package/src/extension/registration/ui.ts +4 -0
  69. package/src/extension/registration/viewers.ts +3 -0
  70. package/src/extension/team-tool/run.ts +7 -6
  71. package/src/runtime/background-runner.ts +11 -16
  72. package/src/runtime/chain-runner.ts +3 -2
  73. package/src/runtime/heartbeat-watcher.ts +28 -1
  74. package/src/runtime/pipeline-runner.ts +8 -7
  75. package/src/runtime/task-runner.ts +165 -119
  76. package/src/schema/config-schema.ts +1 -0
  77. package/src/ui/live-run-sidebar.ts +2 -0
  78. package/src/ui/mascot.ts +11 -9
  79. package/src/ui/render-coalescer.ts +9 -0
  80. package/src/ui/run-snapshot-cache.ts +10 -11
  81. package/src/ui/terminal-status.ts +5 -0
  82. package/src/ui/widget/index.ts +3 -5
  83. package/src/ui/widget/widget-types.ts +0 -1
  84. package/src/utils/gh-protocol.ts +9 -8
  85. package/workflows/distill.workflow.md +198 -0
  86. package/assets/runner-spritesheet.png +0 -0
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+ // validate-run.mjs — machine-checked RUN-completion gate for distillation runs.
3
+ //
4
+ // Complements validate-skill-structure.mjs (which checks the OUTPUT skill's STRUCTURE —
5
+ // frontmatter, sections, anti-drift tables). This checks the RUN: did the agent produce
6
+ // every required process artifact + fire every gate? Both must pass before claiming done:
7
+ // validate-skill-structure.mjs <skill-dir> → output skill structure
8
+ // validate-run.mjs <run-dir> → run completeness (this script)
9
+ //
10
+ // ALSO checks Phase 4 APPLY evidence (software + persona flavors): APPLY-LOG.md must
11
+ // exist at run-dir root, documenting what was edited in the TARGET (SKILL.md is the
12
+ // intermediate essence, not the deliverable — distillation = source → essence → APPLY).
13
+ //
14
+ // Run BEFORE claiming done. If you feel tempted to skip a phase to save effort,
15
+ // THAT is exactly when you must run the gate. A skipped gate = a failed run.
16
+ //
17
+ // Usage:
18
+ // node validate-run.mjs <run-dir> → distillation run completeness
19
+ // node validate-run.mjs <skill-dir> --build → engine-skill build completeness
20
+ // (--build: skips APPLY-LOG, alias-tolerant evidence,
21
+ // AND Phase 2.7/5.5 checks — engine builds have no target-apply)
22
+ //
23
+ // Exit codes: 0 = ALL-GREEN (run complete — may ship); 1 = NOT READY (produce missing artifacts, re-run).
24
+
25
+ import { readFileSync, existsSync, statSync, readdirSync } from 'node:fs';
26
+ import { join, basename } from 'node:path';
27
+
28
+ const runDir = process.argv.find((a) => !a.startsWith('-') && a !== process.argv[0] && a !== process.argv[1]);
29
+ if (!runDir || !existsSync(runDir) || !statSync(runDir).isDirectory()) {
30
+ console.error('Usage: validate-run.mjs <run-dir>');
31
+ console.error(' <run-dir> = directory holding the distillation artifacts (SKILL.md, FIDELITY.md, …)');
32
+ process.exit(2);
33
+ }
34
+
35
+ const isBuild = process.argv.includes('--build');
36
+ // Mode auto-detection (default mode; --build stays Capture and skips these):
37
+ // APPLY mode = APPLY-LOG.md present at run-dir root → SKILL.md optional (deliverable = target transformed)
38
+ // CAPTURE mode = no APPLY-LOG.md → SKILL.md + FIDELITY required (current behavior)
39
+ const applyLogExists = existsSync(join(runDir, 'APPLY-LOG.md'));
40
+ const isApplyMode = !isBuild && applyLogExists;
41
+ const isCaptureMode = !isBuild && !applyLogExists;
42
+
43
+ const failures = [];
44
+ const passes = [];
45
+ const check = (label, ok, detail = '') => {
46
+ (ok ? passes : failures).push(ok ? ` ✓ ${label}` : ` ✗ ${label}${detail ? ' — ' + detail : ''}`);
47
+ };
48
+
49
+ const readText = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : null);
50
+
51
+ // count list/table items under a heading (mirrors validate-skill-structure.mjs)
52
+ function countListItems(haystack, headingRe, windowChars = 2000) {
53
+ const m = haystack.match(headingRe);
54
+ if (!m) return { found: false, count: 0 };
55
+ const after = haystack.slice(m.index);
56
+ const section = after.match(/([\s\S]*?)\n##(?=[^#])/);
57
+ const block = section ? section[1] : after.slice(0, windowChars);
58
+ const listItems = (block.match(/^\s*(?:\d+[.)]|[-*])\s+\S/gm) || []).length;
59
+ const tableRows = (block.match(/^\s*\|(?![\s:|-]+\|?\s*$).+\|/gm) || []).length;
60
+ return { found: true, count: listItems + tableRows };
61
+ }
62
+
63
+ // hint: does a file exist at the flat research/ path? (artifact-scattering signal)
64
+ const flatHint = (name) => existsSync(join(runDir, 'research', name)) ? ' (found at research/ — artifact scattering; canonical path is references/research/)' : '';
65
+
66
+ // --- SKILL.md ---
67
+ // Apply mode: optional (deliverable = target transformed + APPLY-LOG, NOT a skill file).
68
+ // Capture/build mode: required at run-dir root (NOT scattered elsewhere).
69
+ const skillPath = join(runDir, 'SKILL.md');
70
+ const skillText = readText(skillPath);
71
+ if (isApplyMode && !skillText) {
72
+ passes.push(' ℹ no SKILL.md — correct for Apply mode (deliverable = target transformed + APPLY-LOG)');
73
+ } else {
74
+ check('SKILL.md exists at run-dir root (NOT scattered elsewhere)', !!skillText,
75
+ !skillText ? 'missing — did you install it to ~/.pi/agent/skills/ early? Keep it in run-dir until ALL-GREEN' : '');
76
+ }
77
+
78
+ let isSoftware = false;
79
+ let isPersona = false;
80
+ let isTopic = false; // research/topic flavor — its 'apply' is a validated report; no APPLY-LOG required
81
+
82
+ if (skillText) {
83
+ const fm = skillText.match(/^---\n([\s\S]*?)\n---/);
84
+ check('SKILL.md: frontmatter --- block present', !!fm, 'no --- block at top');
85
+ const fmText = fm ? fm[1] : '';
86
+
87
+ const PLACEHOLDERS = [/<person>/i, /<target>/i, /<topic>/i, /<field>/i, /TODO/i, /TBD/i, /XXX/i, /YYYY-MM-DD/i, /\.\.\.\s*<\/?/i];
88
+ const found = PLACEHOLDERS.filter((re) => re.test(skillText));
89
+ check('SKILL.md: no unresolved placeholder text', found.length === 0,
90
+ found.map((re) => skillText.match(re)?.[0]).filter(Boolean).join(', '));
91
+
92
+ // flavor auto-detect
93
+ isSoftware = /target:\s*(software|codebase|engineer)/i.test(fmText) || /code-?dna|代码表达DNA|code expression/i.test(skillText);
94
+ isPersona = !isSoftware && (/target:\s*(person|topic)/i.test(fmText) || /mental.?model/i.test(skillText));
95
+ isTopic = /target:\s*topic/i.test(fmText); // exclude topic/research flavor from APPLY checks
96
+
97
+ if (isSoftware) {
98
+ check('[software] Code-DNA section present', /代码表达DNA|Code Expression-DNA|code.?dna/i.test(skillText), 'missing 代码表达DNA/code-DNA section');
99
+ check('[software] toolchain matrix present', /toolchain|eslint|oxlint|biome|tsconfig|deno|rustfmt/i.test(skillText), 'no toolchain detection');
100
+ check('[software] distilled_against anchor present', /distilled_against/i.test(fmText) || /distilled_against/i.test(skillText), 'no distilled_against staleness anchor');
101
+ }
102
+
103
+ if (isPersona) {
104
+ check('[persona] mental-models section present', /mental.?model|心智模型|核心心智模型/i.test(skillText), 'no mental-models section');
105
+ const boundary = countListItems(skillText, /诚实边界|honest boundar(?:y|ies)/i);
106
+ check('[persona] honest-boundaries ≥3 items', boundary.count >= 3, boundary.found ? `found ${boundary.count} items` : 'section missing');
107
+ }
108
+ }
109
+
110
+ // --- Fallback flavor detection (when SKILL.md is absent/scattered) ---
111
+ // Needed because isSoftware/isPersona are only set inside if(skillText); a run that
112
+ // skipped APPLY often also has no SKILL.md at the run-dir root (it was installed early
113
+ // or never written there). Detect software flavor from research/CODE-DNA.md so the
114
+ // APPLY-evidence checks still fire for the all-too-common 'wrote SKILL, skipped APPLY' case.
115
+ if (!isSoftware && !isPersona && !isTopic) {
116
+ if (existsSync(join(runDir, 'references', 'research', 'CODE-DNA.md')) ||
117
+ existsSync(join(runDir, 'research', 'CODE-DNA.md')) ||
118
+ /code-?dna/i.test(basename(runDir))) {
119
+ isSoftware = true;
120
+ }
121
+ }
122
+
123
+ // --- APPLY-LOG.md (Phase 3 APPLY evidence — Apply mode only) ---
124
+ // Apply mode (APPLY-LOG.md present): the deliverable is the target transformed — these
125
+ // checks are REQUIRED. Capture mode (--build or no APPLY-LOG): skipped (no target-apply).
126
+ // Gated on isApplyMode (not flavor) so it fires even when SKILL.md is absent in Apply mode.
127
+ if (isApplyMode) {
128
+ const applyLogPath = join(runDir, 'APPLY-LOG.md');
129
+ const applyText = readText(applyLogPath);
130
+ check('APPLY-LOG.md exists at run-dir root', !!applyText,
131
+ !applyText ? 'Phase 4 APPLY missing — SKILL.md is intermediate, not the deliverable. Distillation = source → essence → APPLY to target. A standalone SKILL.md = NOT complete.' : '');
132
+ if (applyText) {
133
+ // count list items + table rows across the whole file (mirrors countListItems)
134
+ const applyItems = (applyText.match(/^\s*(?:\d+[.)]|[-*])\s+\S/gm) || []).length
135
+ + (applyText.match(/^\s*\|(?![\s:|-]+\|?\s*$).+\|/gm) || []).length;
136
+ check('APPLY-LOG: ≥3 applied items', applyItems >= 3, `found ${applyItems} item(s) — need ≥3 concrete edits in the target`);
137
+ const pathRefs = /\/[\w.-]+\.\w{1,4}|src\/|AGENTS\.md|scripts\/|package\.json|tsconfig/i.test(applyText);
138
+ check('APPLY-LOG: references target file paths', pathRefs, 'no file-path-like tokens found — add paths proving real edits (not just claims)');
139
+ const verified = /test|typecheck|build|bundle|lint|pass|green|✅|97\/97|all-green/i.test(applyText);
140
+ check('APPLY-LOG: verification evidence (test/typecheck/bundle/lint pass)', verified, 'no verification keyword found — add test/build/lint pass evidence');
141
+ }
142
+ }
143
+
144
+ // --- FIDELITY.md ---
145
+ // --build mode: accept references/fidelity.md as an alias (workflow produces it there)
146
+ const fidText = isBuild
147
+ ? (readText(join(runDir, 'FIDELITY.md')) || readText(join(runDir, 'references', 'fidelity.md')))
148
+ : readText(join(runDir, 'FIDELITY.md'));
149
+ const fidLabel = isBuild ? 'FIDELITY.md exists (root or references/fidelity.md)' : 'FIDELITY.md exists at run-dir root';
150
+ check(fidLabel, !!fidText, !fidText ? 'missing' : '');
151
+ if (fidText) {
152
+ const totalMatch = fidText.match(/(?:总分|total)[::\s*]*\*{0,2}([0-9]+)\s*\*?\s*(?:\/|/|\sout\sof\s)\s*\*?\s*100/i);
153
+ check('FIDELITY.md: /100 total score present', !!totalMatch, totalMatch ? `=${totalMatch[1]}/100` : 'no /100 score found');
154
+ const qCount = (fidText.match(/(?:Q[1-5]|问题[1-5]|question\s*[1-5])/gi) || []).length;
155
+ check('FIDELITY.md: ≥5 test-question references', qCount >= 5, `found ${qCount} question refs`);
156
+ check('FIDELITY.md: flags single-agent/self-score caveat',
157
+ /单\s*agent|single.?agent|self.?score|upper.?bound|independent/i.test(fidText),
158
+ 'add single-agent upper-bound caveat');
159
+ }
160
+
161
+ // --- DISTILLATION-PROCESS-CHECKLIST.md ---
162
+ const procText = readText(join(runDir, 'DISTILLATION-PROCESS-CHECKLIST.md'));
163
+ check('DISTILLATION-PROCESS-CHECKLIST.md exists', !!procText, !procText ? 'missing — every phase + the 3-round deep-dive gate must be tracked' : '');
164
+ if (procText) {
165
+ const dangling = (procText.match(/\|\s*[⬜⏳][^|]*\|/gu) || []).length;
166
+ check('process: no dangling ⬜/⏳ phase rows (every phase completed)', dangling === 0, dangling ? `${dangling} phase(s) not done` : '');
167
+ check('process: deep-dive round-log table present', /round log|\| *Round.*New findings/i.test(procText), 'add the round-log table');
168
+ check('process: 3-empty-rounds gate fired', /gate fires|3 consecutive (empty|zero)|GATE FIRES/i.test(procText),
169
+ 'record ≥3 consecutive zero-new rounds before declaring a phase done');
170
+ }
171
+
172
+ // --- EXCAVATION-CHECKLIST.md ---
173
+ const excText = readText(join(runDir, 'EXCAVATION-CHECKLIST.md'));
174
+ check('EXCAVATION-CHECKLIST.md exists', !!excText, !excText ? 'missing — Phase 1 protocol requires one' : '');
175
+ if (excText) {
176
+ const danglingExc = (excText.match(/\|\s*[⬜⏳][^|]*\|/gu) || []).length;
177
+ check('excavation: no dangling ⬜/⏳ rows (every part resolved)', danglingExc === 0, danglingExc ? `${danglingExc} row(s) not resolved` : '');
178
+ const ratioMatch = excText.match(/memory-ratio[:\s]*([0-9]+)\s*%/i);
179
+ if (ratioMatch) {
180
+ const ratio = parseInt(ratioMatch[1], 10);
181
+ check('excavation: 🧠 memory-ratio ≤30%', ratio <= 30, `declared ${ratio}% (>30% = recap, not distillation)`);
182
+ } else {
183
+ check('excavation: declares memory-ratio', false, 'add "🧠 memory-ratio: NN% (X/Y)" header');
184
+ }
185
+ const proofCells = (excText.match(/"[^"]{8,}"/g) || []).length;
186
+ check('excavation: ≥1 proof-of-read (verbatim quote cell)', proofCells >= 1, `${proofCells} quote cell(s)`);
187
+ }
188
+
189
+ // --- references/research/ artifacts (canonical path — solves scattering) ---
190
+ const researchDir = join(runDir, 'references', 'research');
191
+
192
+ if (isBuild) {
193
+ // --- BUILD MODE: alias-tolerant evidence checks ---
194
+ // Engine skills use different file names than distillation runs.
195
+ // Accept canonical names OR common aliases OR name/content regex matches.
196
+ const refsDir = join(runDir, 'references');
197
+
198
+ // Helper: find a file under references/ or references/research/ by name regex or content regex
199
+ function findRef(nameRe, contentRe) {
200
+ const dirs = [refsDir, researchDir].filter((d) => existsSync(d) && statSync(d).isDirectory());
201
+ for (const d of dirs) {
202
+ for (const f of readdirSync(d)) {
203
+ const fp = join(d, f);
204
+ if (!existsSync(fp) || !statSync(fp).isFile()) continue;
205
+ if (nameRe && nameRe.test(f)) return fp;
206
+ if (contentRe) {
207
+ const txt = readText(fp);
208
+ if (txt && contentRe.test(txt)) return fp;
209
+ }
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+
215
+ // 1. Coverage evidence
216
+ const covPath =
217
+ existsSync(join(researchDir, 'COVERAGE-MANIFEST.md')) ? join(researchDir, 'COVERAGE-MANIFEST.md')
218
+ : existsSync(join(refsDir, 'source-inventory.md')) ? join(refsDir, 'source-inventory.md')
219
+ : existsSync(join(refsDir, 'coverage-manifest.md')) ? join(refsDir, 'coverage-manifest.md')
220
+ : findRef(null, /UNCOVERED.*COVERED|covered.*parts/i);
221
+ const covTextB = covPath ? readText(covPath) : null;
222
+ check('coverage evidence present (COVERAGE-MANIFEST | source-inventory | coverage-manifest | content-match)',
223
+ !!covTextB, !covTextB ? 'missing — no coverage evidence found under references/' : `found: ${covPath}`);
224
+ if (covTextB) {
225
+ const uncovered = (covTextB.match(/\|\s*UNCOVERED[^|]*\|/gi) || []).length;
226
+ check('coverage: no dangling UNCOVERED rows', uncovered === 0, uncovered ? `${uncovered} part(s) not covered` : '');
227
+ }
228
+
229
+ // 2. V5/citation-verify evidence
230
+ const v5Path =
231
+ existsSync(join(researchDir, 'V5-VERIFICATION.md')) ? join(researchDir, 'V5-VERIFICATION.md')
232
+ : existsSync(join(refsDir, 'verified-models.md')) ? join(refsDir, 'verified-models.md')
233
+ : findRef(/v5|verified|citation/i, null);
234
+ check('V5/citation-verify evidence present (V5-VERIFICATION | verified-models | name-match)',
235
+ !!v5Path, !v5Path ? 'missing — no V5/citation evidence found under references/' : `found: ${v5Path}`);
236
+
237
+ // 3. Effectiveness-gate evidence
238
+ const effPath =
239
+ existsSync(join(researchDir, 'EFFECTIVENESS-VERIFICATION.md')) ? join(researchDir, 'EFFECTIVENESS-VERIFICATION.md')
240
+ : existsSync(join(refsDir, 'effectiveness-gate.md')) ? join(refsDir, 'effectiveness-gate.md')
241
+ : existsSync(join(refsDir, 'three-filter.md')) ? join(refsDir, 'three-filter.md')
242
+ : findRef(/effectiveness|three-filter|gate/i, null);
243
+ // FIDELITY.md validates the built skill reproduces the target — for an engine-skill BUILD that IS effectiveness
244
+ // evidence. The pre-apply effectiveness-gate.md is a forward-looking workflow artifact, not retroactively required
245
+ // for hand-built engine skills (research/persona/software predate Phase 2.6).
246
+ const effViaFidelity = !effPath && !!fidText;
247
+ check('effectiveness-gate evidence present (EFFECTIVENESS-VERIFICATION | effectiveness-gate | three-filter | FIDELITY.md)',
248
+ !!effPath || effViaFidelity, (!effPath && !effViaFidelity) ? 'missing — no effectiveness evidence found under references/ or FIDELITY.md' : `found: ${effPath || 'FIDELITY.md'}`);
249
+ } else {
250
+ // --- DEFAULT MODE: strict canonical paths (UNCHANGED — byte-for-byte equivalent) ---
251
+ const covText = readText(join(researchDir, 'COVERAGE-MANIFEST.md'));
252
+ check('references/research/COVERAGE-MANIFEST.md exists', !!covText, !covText ? 'missing' + flatHint('COVERAGE-MANIFEST.md') : '');
253
+ if (covText) {
254
+ const uncovered = (covText.match(/\|\s*UNCOVERED[^|]*\|/gi) || []).length;
255
+ check('coverage: no dangling UNCOVERED rows', uncovered === 0, uncovered ? `${uncovered} part(s) not covered` : '');
256
+ }
257
+
258
+ const v5Exists = existsSync(join(researchDir, 'V5-VERIFICATION.md'));
259
+ check('references/research/V5-VERIFICATION.md exists', v5Exists, !v5Exists ? 'missing' + flatHint('V5-VERIFICATION.md') : '');
260
+
261
+ const effExists = existsSync(join(researchDir, 'EFFECTIVENESS-VERIFICATION.md'));
262
+ check('references/research/EFFECTIVENESS-VERIFICATION.md exists', effExists, !effExists ? 'missing' + flatHint('EFFECTIVENESS-VERIFICATION.md') : '');
263
+ }
264
+
265
+ // --- Phase 2.7 + 5.5 anti-lazy gate checks (APPLY mode only) ---
266
+ // apply-plan + scrutinize are APPLY-mode artifacts (they assume a target to apply to).
267
+ // CAPTURE mode (skill-build: --build, or no APPLY-LOG = persona/workflow) has no target-apply → skip.
268
+ if (isApplyMode) {
269
+ // Phase 2.7 output: references/apply-plan.md
270
+ const applyPlanPath = join(runDir, 'references', 'apply-plan.md');
271
+ const applyPlanText = readText(applyPlanPath);
272
+ check('references/apply-plan.md exists (Phase 2.7 plan-approval gate output)', !!applyPlanText,
273
+ !applyPlanText ? 'missing — Phase 2.7 requires a plan table output' : '');
274
+ if (applyPlanText) {
275
+ const hasApproval = /APPROVED|LOW-YIELD DEFENSE/i.test(applyPlanText);
276
+ check('apply-plan: approval or defense present (APPROVED | LOW-YIELD DEFENSE)', hasApproval,
277
+ !hasApproval ? 'Phase 2.7 plan-approval gate not respected — present the plan and get approval (interactive) or write a LOW-YIELD DEFENSE (autonomous).' : '');
278
+ }
279
+
280
+ // Phase 5.5 output: SCRUTINIZE-REPORT.md at run-dir root
281
+ check('SCRUTINIZE-REPORT.md exists at run-dir root (Phase 5.5 adversarial scrutinize)', existsSync(join(runDir, 'SCRUTINIZE-REPORT.md')),
282
+ !existsSync(join(runDir, 'SCRUTINIZE-REPORT.md')) ? 'Phase 5.5 adversarial scrutinize not run — laziness unaudited.' : '');
283
+ }
284
+
285
+ // --- Report ---
286
+ console.log(`\nvalidate-run: ${basename(runDir)}`);
287
+ console.log(`path: ${runDir}\n`);
288
+ passes.forEach((l) => console.log(l));
289
+ failures.forEach((l) => console.log(l));
290
+ const flavorTag = isSoftware ? ' [software]' : isPersona ? (isTopic ? ' [topic]' : ' [persona]') : '';
291
+ const modeTag = isBuild
292
+ ? ' [build mode — APPLY-LOG + strict-run checks skipped]'
293
+ : isApplyMode
294
+ ? ' [apply mode — SKILL.md optional]'
295
+ : ' [capture mode]';
296
+ console.log(`\n${passes.length} pass, ${failures.length} fail — ${failures.length === 0 ? '✅ ALL-GREEN (may ship)' : '🔴 NOT READY (produce the missing artifacts, then re-run)'}${flavorTag}${modeTag}\n`);
297
+ process.exit(failures.length === 0 ? 0 : 1);
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ // validate-skill-structure.mjs — structural invariant checker for generated distill skills.
3
+ // Implements F10 (awesome-persona-distill-skills finding): hard-fail if any structural assertion fails.
4
+ // Run AFTER Phase 3 build, BEFORE Phase 4 behavioral fidelity. Self-contained (stdlib only).
5
+ //
6
+ // Usage:
7
+ // node validate-skill-structure.mjs <path-to-SKILL.md>
8
+ // node validate-skill-structure.mjs <skill-dir>
9
+ //
10
+ // Exit codes: 0 = all assertions pass (all-green → may ship); 1 = one or more failed (iterate).
11
+
12
+ import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs';
13
+ import { join, dirname, basename } from 'node:path';
14
+
15
+ const target = process.argv.find((a) => !a.startsWith('-') && a !== process.argv[0] && a !== process.argv[1]);
16
+ if (!target) {
17
+ console.error('Usage: validate-skill-structure.mjs <path-to-SKILL.md | skill-dir> [--engine]');
18
+ process.exit(2);
19
+ }
20
+
21
+ // Resolve to a SKILL.md path
22
+ let skillPath = target;
23
+ if (statSync(target).isDirectory()) {
24
+ skillPath = join(target, 'SKILL.md');
25
+ }
26
+ if (!existsSync(skillPath)) {
27
+ console.error(`✗ Not found: ${skillPath}`);
28
+ process.exit(2);
29
+ }
30
+
31
+ const src = readFileSync(skillPath, 'utf8');
32
+ const dir = dirname(skillPath);
33
+ const name = basename(dir);
34
+ const isEngine = process.argv.includes('--engine');
35
+ const PLACEHOLDERS = [/<person>/i, /<target>/i, /<topic>/i, /<field>/i, /TODO/i, /TBD/i, /XXX/i, /YYYY-MM-DD/i, /\.\.\.\s*<\/?/i];
36
+
37
+ const failures = [];
38
+ const passes = [];
39
+ const check = (label, ok, detail = '') => {
40
+ (ok ? passes : failures).push(ok ? ` ✓ ${label}` : ` ✗ ${label}${detail ? ' — ' + detail : ''}`);
41
+ };
42
+
43
+ // --- Frontmatter ---
44
+ const fm = src.match(/^---\n([\s\S]*?)\n---/);
45
+ check('frontmatter block present', !!fm, 'no --- block at top');
46
+ const fmText = fm ? fm[1] : '';
47
+ const fmField = (key) => {
48
+ const m = fmText.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
49
+ return m ? m[1].trim() : null;
50
+ };
51
+ const hasFm = (key) => new RegExp(`^${key}:`, 'm').test(fmText);
52
+
53
+ check('frontmatter: name', hasFm('name'));
54
+ check('frontmatter: description', hasFm('description'));
55
+ const desc = fmField('description');
56
+ check('frontmatter: description ≤1 sentence (one terminal punct or one clause)',
57
+ desc ? desc.split(/[.。!!??]/).filter(Boolean).length <= 2 : false,
58
+ desc ? `got: "${desc.slice(0, 60)}…"` : 'missing');
59
+ check('frontmatter: triggers', hasFm('triggers') || hasFm('trigger'), 'no triggers field');
60
+ if (!isEngine) check('frontmatter: distilled (staleness date)', hasFm('distilled') || hasFm('调研时间'), 'no distilled/调研时间 staleness anchor');
61
+ const distilled = fmField('distilled') || fmField('调研时间');
62
+ check('frontmatter: distilled is valid date (YYYY-MM-DD)',
63
+ !distilled || /^\d{4}-\d{2}-\d{2}/.test(distilled),
64
+ distilled ? `got "${distilled}"` : '');
65
+ if (!isEngine) check('frontmatter: target (person|topic|software)', hasFm('target'), 'no target field');
66
+
67
+ // --- Body sections ---
68
+ check('Agentic Protocol section present', /回答工作流|Agentic Protocol/i.test(src));
69
+ check('Agentic Protocol Step 1', /Step 1|第一步|步骤 1|### 1\b/i.test(src));
70
+ check('Agentic Protocol Step 2', /Step 2|第二步|步骤 2|### 2\b/i.test(src));
71
+ check('Agentic Protocol Step 3', /Step 3|第三步|步骤 3|### 3\b/i.test(src));
72
+
73
+ // honest boundaries: count items (numbered list or bullets) under an honest-boundaries heading
74
+ function countListItems(haystack, headingRe, windowChars = 2000) {
75
+ const m = haystack.match(headingRe);
76
+ if (!m) return { found: false, count: 0, block: '' };
77
+ const after = haystack.slice(m.index);
78
+ const section = after.match(/([\s\S]*?)\n##(?=[^#])/);
79
+ const block = section ? section[1] : after.slice(0, windowChars);
80
+ // count BOTH list items AND table data rows (rows with | content |, excluding separator rows)
81
+ const listItems = (block.match(/^\s*(?:\d+[.)]|[-*])\s+\S/gm) || []).length;
82
+ const tableRows = (block.match(/^\s*\|(?![\s:|-]+\|?\s*$).+\|/gm) || []).length;
83
+ return { found: true, count: listItems + tableRows, block };
84
+ }
85
+ if (!isEngine) {
86
+ const boundary = countListItems(src, /诚实边界|honest boundar(?:y|ies)/i);
87
+ check('honest boundaries (M11) ≥3', boundary.count >= 3, `found ${boundary.count} items`);
88
+ }
89
+
90
+ // --- Anti-drift tables (M9a 内在张力, M9b 反例黑名单, M12 fallback tree) — Darwin gap #2: validator previously skipped these
91
+ if (!isEngine) {
92
+ const tension = countListItems(src, /M9a|内在张力|inner tension|internal tension/i);
93
+ check('内在张力 (M9a) ≥3 tension pairs', tension.count >= 3, tension.found ? `found ${tension.count} items` : 'section missing');
94
+
95
+ const blacklist = countListItems(src, /M9b|反例黑名单|anti.?pattern blacklist/i);
96
+ check('反例黑名单 (M9b) ≥7 rows', blacklist.count >= 7, blacklist.found ? `found ${blacklist.count} rows` : 'section missing');
97
+
98
+ const fallback = countListItems(src, /M12|失败模式.*[Ff]allback|[Ff]allback\s*树/i);
99
+ check('失败模式Fallback树 (M12) ≥8 rows', fallback.count >= 8, fallback.found ? `found ${fallback.count} rows` : 'section missing');
100
+ }
101
+
102
+ // --- FIDELITY.md companion artifact (Darwin gap #3: ship-gate requires it but nothing checked)
103
+ const fidelityPath = join(dir, 'FIDELITY.md');
104
+ if (existsSync(fidelityPath)) {
105
+ const fm2 = readFileSync(fidelityPath, 'utf8');
106
+ const totalMatch = fm2.match(/(?:总分|total)[::\s*]*\*{0,2}([0-9]+)\s*\*?\s*(?:\/|/|\sout\sof\s)\s*\*?\s*100/i);
107
+ check('FIDELITY.md total score present', !!totalMatch, totalMatch ? `=${totalMatch[1]}/100` : 'no /100 score found');
108
+ const qCount = (fm2.match(/(?:Q[1-5]|问题[1-5]|question\s*[1-5])/gi) || []).length;
109
+ check('FIDELITY.md ≥5 test questions', qCount >= 5, `found ${qCount} question refs`);
110
+ check('FIDELITY.md flags single-agent/self-score caveat', /单\s*agent|single.?agent|self.?score|upper.?bound|independent/i.test(fm2), 'add single-agent upper-bound caveat');
111
+ } else if (!isEngine) {
112
+ check('FIDELITY.md companion present', false, 'no FIDELITY.md in skill dir');
113
+ }
114
+
115
+ // --- EXCAVATION-CHECKLIST.md (Phase 1 protocol: track + verify each part was really read)
116
+ const checklistPath = join(dir, 'EXCAVATION-CHECKLIST.md');
117
+ if (existsSync(checklistPath)) {
118
+ const cl = readFileSync(checklistPath, 'utf8');
119
+ // dangling rows: status ⬜ not-started or ⏳ reading left at ship time = silently skipped/forgotten
120
+ const dangling = (cl.match(/\|\s*[⬜⏳][^|]*\|/gu) || []).length;
121
+ check('checklist: no dangling ⬜/⏳ rows (every part resolved)', dangling === 0, dangling ? `${dangling} row(s) not-started/reading — resolve to ✅📄/⏭/🧠` : '');
122
+ // memory ratio: parse "memory-ratio: NN%" if declared
123
+ const ratioMatch = cl.match(/memory-ratio[:\s]*([0-9]+)\s*%/i);
124
+ if (ratioMatch) {
125
+ const ratio = parseInt(ratioMatch[1], 10);
126
+ check('checklist: 🧠 memory-ratio ≤30%', ratio <= 30, `declared ${ratio}% (>30% = recap, not distillation)`);
127
+ } else {
128
+ check('checklist: declares memory-ratio', false, 'add "🧠 memory-ratio: NN% (X/Y findings)" header line');
129
+ }
130
+ // proof-of-read present: at least one verbatim-quote-with-location cell (heuristic — a ✅ row should carry a quote)
131
+ const proofCells = (cl.match(/"[^"]{8,}"[^|]*/g) || []).length;
132
+ check('checklist: ≥1 proof-of-read (verbatim quote in a ✅ row)', proofCells >= 1, `${proofCells} quote cell(s) found`);
133
+ } else if (!isEngine) {
134
+ check('EXCAVATION-CHECKLIST.md present', false, 'no excavation checklist — Phase 1 protocol requires one');
135
+ }
136
+
137
+ // --- DISTILLATION-PROCESS-CHECKLIST.md (whole-pipeline tracking + the 3-empty-rounds deep-dive gate)
138
+ const procPath = join(dir, 'DISTILLATION-PROCESS-CHECKLIST.md');
139
+ if (existsSync(procPath)) {
140
+ const pc = readFileSync(procPath, 'utf8');
141
+ // dangling phases: ⬜/⏳ left in the phase-progress table at ship = a phase skipped
142
+ const danglingPhases = (pc.match(/\|\s*[⬜⏳][^|]*\|/gu) || []).length;
143
+ check('process: no dangling ⬜/⏳ phases (every phase completed)', danglingPhases === 0, danglingPhases ? `${danglingPhases} phase(s) not done` : '');
144
+ // deep-dive round log present
145
+ check('process: deep-dive round log present', /round log|\| *Round.*New findings/i.test(pc), 'add the round-log table');
146
+ // 3-empty-rounds gate recorded (≥3 consecutive zero-new rounds before leaving a phase)
147
+ const gateFired = /gate fires|3 consecutive (empty|zero)|GATE FIRES/i.test(pc);
148
+ check('process: 3-empty-rounds gate recorded (≥3 zero-new rounds)', gateFired, 'record a round-log row marked gate-fired before declaring any research phase done');
149
+ } else if (!isEngine) {
150
+ check('DISTILLATION-PROCESS-CHECKLIST.md present', false, 'no process checklist — every phase + the 3-round deep-dive gate must be tracked');
151
+ }
152
+
153
+ // --- Placeholders ---
154
+ const foundPlaceholders = PLACEHOLDERS.filter((re) => re.test(src));
155
+ if (!isEngine) check('no unresolved placeholder text', foundPlaceholders.length === 0,
156
+ foundPlaceholders.map((re) => src.match(re)?.[0]).filter(Boolean).join(', '));
157
+
158
+ // --- Self-containment (F9): references/sources should not dangle outside the dir ---
159
+ const refsDir = join(dir, 'references');
160
+ if (!isEngine) check('self-contained: no external repo paths in body (e.g. 07-调研/)',
161
+ !/[\w/-]*调研与分析?\/|src\/|source\//i.test(src.replace(/```[\s\S]*?```/g, '')),
162
+ 'body references paths outside skill dir');
163
+
164
+ // --- Software-specific assertions (if target: software) ---
165
+ if (/target:\s*software/i.test(fmText) || /code-dna|代码表达DNA|code expression/i.test(src)) {
166
+ check('[software] Code Expression-DNA section present', /代码表达DNA|Code Expression-DNA|code.?dna/i.test(src));
167
+ check('[software] toolchain matrix present', /toolchain|eslint|oxlint|biome|tsconfig/i.test(src));
168
+ check('[software] distilled_against commit anchor', hasFm('distilled_against') || /distilled_against/i.test(src));
169
+ }
170
+
171
+ // --- Report ---
172
+ console.log(`\nvalidate-skill-structure: ${name}`);
173
+ console.log(`path: ${skillPath}\n`);
174
+ passes.forEach((l) => console.log(l));
175
+ failures.forEach((l) => console.log(l));
176
+ console.log(`\n${passes.length} pass, ${failures.length} fail — ${failures.length === 0 ? '✅ ALL-GREEN (may ship)' : '🔴 NOT READY (iterate Phase 2→3)'}${isEngine ? ' [engine mode — generated-skill checks skipped]' : ''}\n`);
177
+ process.exit(failures.length === 0 ? 0 : 1);
@@ -0,0 +1,56 @@
1
+ # distill-software — Build Notes
2
+
3
+ Sibling of `distill-persona`, specialized for software. Inherits the base methodology (6 phases, Phase 2.6 extraction verification, F2' framework-answerable-edge fidelity, exhaustive-sweep mode + coverage manifest + diminishing-returns gate, self-correction meta-loop) — does NOT duplicate it; only specifies what's different for software.
4
+
5
+ ## Decision → source trace
6
+ | Decision | From |
7
+ |---|---|
8
+ | 3 flavors (engineer persona / codebase conventions / domain expertise) | SOFTWARE-DISTILLATION-DEEP-DIVE §A |
9
+ | Code-Expression-DNA 12-axis grid (measurable, not vibes) | SOFTWARE-DISTILLATION-DEEP-DIVE §C |
10
+ | pi-langsrv-native research (symbol/call-graph) — the software differentiator vs nuwa's web-only | SOFTWARE-DISTILLATION-DEEP-DIVE §E + nuwa F1 (portability) |
11
+ | `language` + `distilled_against` staleness anchors in frontmatter | nuwa review F2'/software — staleness is the #1 software honesty failure |
12
+ | Phase 2.6 V1 strengthened (quirk-vs-principle flag) | SOFTWARE-DISTILLATION-DEEP-DIVE §J (overfit-to-quirk anti-pattern) |
13
+ | F2' third category applied to version-recency ("API newer than distilled_against") | f2-experiment (validated n=3) |
14
+ | `code_dna.py` wired INTO Agentic Protocol Step 2 (not orphaned) | nuwa mrbeast review F13 |
15
+ | Tests-as-invariants + CI/lint-as-enforced-conventions + dep manifests (3 extra streams) | SOFTWARE-DISTILLATION-DEEP-DIVE §B |
16
+ | 口癖 = lint `no-restricted-syntax` entries | SOFTWARE-DISTILLATION-DEEP-DIVE §C |
17
+ | software-specific anti-patterns (8) | SOFTWARE-DISTILLATION-DEEP-DIVE §J |
18
+
19
+ ## Operational script — `scripts/code_dna.py`
20
+ - Stdlib-only, Python 3.9+. Measures the code-Expression-DNA axes on a target file/dir → markdown report.
21
+ - **Tested** on nuwa-skill Python scripts (mid-comments/loose-errors/typed/snake_case 79%) and on itself (terse/strict-errors/typed) — produces differentiated, sensible fingerprints.
22
+ - Axes measured: naming distribution + prefix tally · function length (median/p90, Python) · comment density + why-vs-what ratio · error-handling pattern (raise/except/return-null/.ok) · type-strictness (typed vs any) · 8-axis style-tag grid.
23
+ - **Wired into Agentic Protocol** (F13): Step 2 runs `code_dna.py` on collected target code, reads the report, applies mental models. Never orphaned.
24
+ - Shares `scripts/fidelity_eval.py` with distill-persona for Phase 4.
25
+
26
+ ## Dogfood run — oh-my-pi (self-correction meta-loop fired)
27
+ Ran distill-software on `oh-my-pi` (TypeScript Pi-toolkit, pnpm monorepo). Output: `~/source/my_pi/source/oh-my-pi-DISTILL/oh-my-pi-conventions.md` (8 triple-verified engineering models + code-DNA + tooling philosophy + honest boundaries). The run **exposed and fixed** skill gaps:
28
+ 1. **code_dna.py TS why-counter bug** — counted why-keywords on ALL lines, not just comments → why-ratio >100% (nonsense). Fixed (count within comment lines only).
29
+ 2. **🔴 Toolchain non-portability** — skill hardcoded `eslint.config.*`/`biome.json`; oh-my-pi uses **OXC (oxlint/oxfmt)**. The 口癖-mining command matched nothing. Fixed: generalized to a **toolchain matrix** (eslint · biome · **oxc** · deno · rustfmt) in SKILL.md (2 places) + code_dna.py. Added: tsconfig strict flags are often the real type-strictness DNA (mine them too).
30
+ 3. **Added 3 extra streams** (3 → 6): **release/shipping-pipeline** (release/publish/pre-commit scripts + changeset + turbo DAG — the most engineering-dense code), **risk/security-posture** (T0-T4 risk-tier + untrusted-data model — policy, not code), **agent-instruction governance** (meta-convention: how the repo changes its own AGENTS.md).
31
+ 4. **Extended existing streams**: test-infrastructure patterns (mock factories, source-alias testing, coverage thresholds), workspace orchestration (turbo DAG, workspace:*, package tiers), tool-philosophy rationale.
32
+ 5. **Added structural/testability DNA** to the code-DNA section — DI seams (factory `provider?`), `safeXxx` never-throw contract, `index-helpers` extraction-for-testability — invisible to static identifier counting but the most important conventions.
33
+ **Net**: the skill is materially more complete after one real run. The meta-loop works as designed (real run → gaps → fix → re-run clean). code_dna.py re-tested on oh-my-pi: parses + produces correct toolchain-matrix 口癖 block.
34
+
35
+ **Meta-loop #2 (sdk/pi-checkpoint sweep, closing coverage to 100%)**: the sdk sweep REFINED 5/8 models (no contradictions) and surfaced 2 more gaps → fixed: **+concurrency/coordination stream** (FS locks/stale-detection/heartbeat, **on-disk vs in-memory interop** — a module loaded as multiple copies across packages MUST coordinate on-disk) · **+platform-hardening stream** (Windows reserved names, rename/remove retry, path-safety) · **sharpened the Result model to a 3-tier spectrum** (`raw` / `locked*` may-throw / `safe*` never-throw+reason-coded — was wrongly collapsed to a binary) · added schema-version-literal pinning + mock-mirrors-decision-tree to structural-DNA. Total extra streams now **8** (was 3 → 6 → 8).
36
+
37
+ **Completion (per self-defined milestone — all criteria met)**:
38
+ 1. ✅ Coverage 100% — every content-bearing part swept (docs/extensions/internal/scripts/**sdk**/.pi/themes/enforced-config/CHANGELOG/PR-template). Findings: `oh-my-pi-DISTILL/{oh-my-pi-conventions.md, 05-sdk-checkpoint.md}` + 4 batch research outputs.
39
+ 2. ✅ Triple-verification on all 8 models (recur across docs+code+enforced; generative; exclusive).
40
+ 3. ✅ Phase 2.6 V1-V4 (codebase-conventions, not persona-content; each changes a decision).
41
+ 4. ✅ Installable skill built: `oh-my-pi-DISTILL/oh-my-pi-conventions-SKILL.md` (loadable, staleness-anchored).
42
+ 5. ✅ Phase 4 fidelity (framework-answerable novel edge — "add a new pi-snapshot extension" — skill gives complete consistent guidance via all 8 models; can be agent-verified for full rigor).
43
+ 6. ✅ No HIGH skill-gaps blocking (meta-loop closed: toolchain matrix + 8 streams + Result-tier + structural-DNA).
44
+ → **oh-my-pi distillation COMPLETE.** Skill `distill-software` improved by 2 meta-loop rounds. Remaining (low) gaps noted: TS function-length needs LSP; per-batch research files 01-04 not persisted as separate files (captured in synthesis); schema-versioning + mock-quality could be promoted to named axes later.
45
+
46
+ ## Known gaps (honest)
47
+ - **TS/JS function-body length not measured** (body-split unreliable for arrow funcs) — use LSP/tree-sitter for accurate TS length. Python length measured.
48
+ - **Deep analysis only for Python + TS/JS**; other languages get naming + comment density only (generic fallback).
49
+ - **口癖 (forbidden patterns) requires manual lint-config mining** — the script prints the `rg` command; it doesn't parse eslint/biome configs yet. (Could add a parser.)
50
+ - **n=1 language per run** — multi-language monorepos need per-language passes (or `--lang` per dir).
51
+ - **Not yet validated end-to-end on a real software distillation** (no example output skill exists, unlike distill-persona which dogfooded on the 3 repos). First real use will likely trigger the self-correction meta-loop.
52
+
53
+ ## Relationship / reuse
54
+ - `distill-persona` = the base (person 6-stream + topic exhaustive-sweep + verification + F2' + meta-loop + 10 field models).
55
+ - `distill-software` = the software specialization (this skill). Reuses the base's phases where silent; specializes sources/DNA/tooling/staleness.
56
+ - A future `distill-software-pi-crew` specialization would pin pi-crew/git/pi-langsrv concretely + ship a per-codebase `_dna.py` derived from that codebase's models.