geneseed 2.0.0
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/CHANGELOG.md +938 -0
- package/DESIGN.md +1423 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +48 -0
- package/README.md +336 -0
- package/SETUP.md +1027 -0
- package/SHIPPED.md +70 -0
- package/adapters/bob/README.md +100 -0
- package/adapters/claude-code/README.md +101 -0
- package/adapters/claude-code/settings.json +47 -0
- package/adapters/claude-code/skills/geneseed/SKILL.md +72 -0
- package/adapters/copilot/README.md +76 -0
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +434 -0
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +158 -0
- package/adapters/opencode/README.md +582 -0
- package/adapters/opencode/agents/orchestrator.md +25 -0
- package/adapters/opencode/opencode.json +44 -0
- package/adapters/opencode/plugins/geneseed-activity.js +430 -0
- package/adapters/opencode/plugins/geneseed-context.js +876 -0
- package/adapters/opencode/plugins/geneseed-guard.js +290 -0
- package/adapters/opencode/plugins/geneseed-learn.js +396 -0
- package/adapters/opencode/plugins/geneseed-notify.js +142 -0
- package/adapters/opencode/plugins/geneseed-ponytail.js +148 -0
- package/adapters/opencode/plugins/geneseed-workflow.js +183 -0
- package/adapters/opencode/workflows/_runtime.js +277 -0
- package/adapters/opencode/workflows/council.js +64 -0
- package/adapters/opencode/workflows/dispatch.js +89 -0
- package/adapters/opencode/workflows/research-plan-implement.js +43 -0
- package/adapters/opencode/workflows/review.js +57 -0
- package/bin/geneseed-cli.mjs +412 -0
- package/bin/geneseed-hook.mjs +129 -0
- package/bin/geneseed.mjs +1086 -0
- package/docs/declined.md +149 -0
- package/docs/limits.md +270 -0
- package/docs/opencode-plugin-setup.md +64 -0
- package/docs/token-footprint.md +68 -0
- package/docs/web/_groups.json +45 -0
- package/docs/web/about.md +6 -0
- package/docs/web/adapters-claude-code.md +8 -0
- package/docs/web/adapters-opencode-loads.md +8 -0
- package/docs/web/adapters-opencode-spec.md +8 -0
- package/docs/web/adapters-opencode.md +8 -0
- package/docs/web/agents.md +8 -0
- package/docs/web/authoring.md +11 -0
- package/docs/web/autostart.md +9 -0
- package/docs/web/claude-hooks.md +36 -0
- package/docs/web/cli.md +6 -0
- package/docs/web/collaboration.md +27 -0
- package/docs/web/configure-wiki.md +9 -0
- package/docs/web/design.md +7 -0
- package/docs/web/env-knobs.md +9 -0
- package/docs/web/exclusions.md +26 -0
- package/docs/web/first-session.md +28 -0
- package/docs/web/footprint.md +31 -0
- package/docs/web/foreman-mode.md +33 -0
- package/docs/web/glossary.md +6 -0
- package/docs/web/harnesses.md +28 -0
- package/docs/web/headless.md +9 -0
- package/docs/web/ingest-docs.md +9 -0
- package/docs/web/install-paths.md +9 -0
- package/docs/web/install-quick.md +35 -0
- package/docs/web/lsp-overview.md +37 -0
- package/docs/web/mcp-claude-code.md +10 -0
- package/docs/web/mcp-filesystem.md +9 -0
- package/docs/web/mcp-gitlab.md +9 -0
- package/docs/web/mcp-markitdown.md +9 -0
- package/docs/web/mcp-overview.md +34 -0
- package/docs/web/mcp-trouble.md +9 -0
- package/docs/web/mcp-verify.md +9 -0
- package/docs/web/memory.md +7 -0
- package/docs/web/migrate.md +39 -0
- package/docs/web/model.md +20 -0
- package/docs/web/notebook.md +7 -0
- package/docs/web/pipelines.md +24 -0
- package/docs/web/plugin-activity.md +23 -0
- package/docs/web/plugin-context.md +26 -0
- package/docs/web/plugin-guard.md +24 -0
- package/docs/web/plugin-learn.md +27 -0
- package/docs/web/plugin-notify.md +24 -0
- package/docs/web/plugin-ponytail.md +23 -0
- package/docs/web/plugin-setup.md +7 -0
- package/docs/web/plugin-workflow.md +24 -0
- package/docs/web/plugins.md +17 -0
- package/docs/web/project-context.md +9 -0
- package/docs/web/rules.md +8 -0
- package/docs/web/run-anywhere.md +9 -0
- package/docs/web/self-improve.md +8 -0
- package/docs/web/setup-choices.md +39 -0
- package/docs/web/skills.md +8 -0
- package/docs/web/themes-author.md +8 -0
- package/docs/web/themes.md +9 -0
- package/docs/web/token-footprint.md +7 -0
- package/docs/web/trouble.md +38 -0
- package/docs/web/upgrade.md +9 -0
- package/docs/web/verify.md +30 -0
- package/docs/web/wiki.md +7 -0
- package/docs/web/worktree.md +10 -0
- package/docs/web-ui.md +144 -0
- package/docs/wiki.md +43 -0
- package/geneseed +39 -0
- package/geneseed.cmd +34 -0
- package/harness.config.json +6 -0
- package/js/anim.mjs +326 -0
- package/js/catalog.mjs +112 -0
- package/js/checkout.mjs +88 -0
- package/js/cli-table.json +1034 -0
- package/js/cli.mjs +498 -0
- package/js/diff.mjs +384 -0
- package/js/doctor.mjs +1364 -0
- package/js/emit.mjs +1373 -0
- package/js/excludes.mjs +271 -0
- package/js/generate.mjs +379 -0
- package/js/hooks.mjs +1026 -0
- package/js/hosts.mjs +253 -0
- package/js/installs.mjs +378 -0
- package/js/inventory.mjs +175 -0
- package/js/lib/fs.mjs +913 -0
- package/js/lib/proc.mjs +28 -0
- package/js/lib/udiff.mjs +275 -0
- package/js/link.mjs +286 -0
- package/js/mcp.mjs +335 -0
- package/js/memory.mjs +108 -0
- package/js/menu.mjs +118 -0
- package/js/migrate.mjs +251 -0
- package/js/native.mjs +426 -0
- package/js/opencode.mjs +325 -0
- package/js/registry.mjs +110 -0
- package/js/render.mjs +282 -0
- package/js/settings.mjs +924 -0
- package/js/setup.mjs +492 -0
- package/js/status.mjs +281 -0
- package/js/themes.mjs +195 -0
- package/js/tui.mjs +444 -0
- package/js/uninstall.mjs +1137 -0
- package/js/update.mjs +910 -0
- package/js/web/actions.mjs +868 -0
- package/js/web/activity.mjs +284 -0
- package/js/web/api.mjs +873 -0
- package/js/web/docs.mjs +531 -0
- package/js/web/graph.mjs +0 -0
- package/js/web/jobs.mjs +480 -0
- package/js/web/server.mjs +1135 -0
- package/package.json +54 -0
- package/registry.json +474 -0
- package/src/AGENT.md.tmpl +390 -0
- package/src/agents/_template.md +76 -0
- package/src/agents/advocate.md +36 -0
- package/src/agents/architect.md +55 -0
- package/src/agents/developer.md +66 -0
- package/src/agents/docs.md +54 -0
- package/src/agents/empiricist.md +39 -0
- package/src/agents/explorer.md +54 -0
- package/src/agents/framer.md +36 -0
- package/src/agents/historian.md +51 -0
- package/src/agents/operator.md +37 -0
- package/src/agents/pragmatist.md +37 -0
- package/src/agents/reviewer.md +52 -0
- package/src/agents/security.md +57 -0
- package/src/agents/skeptic.md +48 -0
- package/src/agents/steward.md +36 -0
- package/src/agents/tester.md +54 -0
- package/src/agents/user-advocate.md +36 -0
- package/src/agents/visionary.md +36 -0
- package/src/laws/universal.md +387 -0
- package/src/memory/README.md +109 -0
- package/src/memory/gitignore +6 -0
- package/src/modes/README.md +26 -0
- package/src/modes/direct.md +2 -0
- package/src/modes/foreman.md +19 -0
- package/src/notebook/README.md +39 -0
- package/src/notebook/gitignore +7 -0
- package/src/postures/README.md +31 -0
- package/src/postures/artisan.md +14 -0
- package/src/postures/assistant.md +12 -0
- package/src/postures/expert.md +11 -0
- package/src/postures/mentor.md +11 -0
- package/src/postures/peer.md +12 -0
- package/src/skills/_template.md +47 -0
- package/src/skills/brainstorm.md +28 -0
- package/src/skills/clarify.md +28 -0
- package/src/skills/codebase-design.md +44 -0
- package/src/skills/commit.md +48 -0
- package/src/skills/council.md +34 -0
- package/src/skills/crash-course.md +28 -0
- package/src/skills/daydream/.claude-plugin/plugin.json +9 -0
- package/src/skills/daydream/SKILL.md +74 -0
- package/src/skills/daydream/VENDOR.md +25 -0
- package/src/skills/daydream/critic-prompt.md +38 -0
- package/src/skills/daydream/instructions.md +422 -0
- package/src/skills/daydream/synthesizer-prompt.md +41 -0
- package/src/skills/debug.md +37 -0
- package/src/skills/decode.md +28 -0
- package/src/skills/develop.md +29 -0
- package/src/skills/document-project.md +79 -0
- package/src/skills/domain-modeling.md +38 -0
- package/src/skills/drill.md +28 -0
- package/src/skills/feynman.md +26 -0
- package/src/skills/forge-mcp.md +48 -0
- package/src/skills/fresh-eyes.md +29 -0
- package/src/skills/frontend-design.md +58 -0
- package/src/skills/gap-detector.md +27 -0
- package/src/skills/geneseed-code-review.md +33 -0
- package/src/skills/geneseed.md +29 -0
- package/src/skills/git-archaeology.md +47 -0
- package/src/skills/git-rescue.md +68 -0
- package/src/skills/handoff.md +29 -0
- package/src/skills/herdr.md +164 -0
- package/src/skills/ingest.md +49 -0
- package/src/skills/learning-path.md +26 -0
- package/src/skills/migrate.md +45 -0
- package/src/skills/opencode-theme.md +52 -0
- package/src/skills/parallel-agents.md +28 -0
- package/src/skills/pipeline.md +52 -0
- package/src/skills/plan.md +45 -0
- package/src/skills/ponytail.md +29 -0
- package/src/skills/profile.md +58 -0
- package/src/skills/prose.md +42 -0
- package/src/skills/prototype.md +44 -0
- package/src/skills/react-view-transitions/AGENTS.md +955 -0
- package/src/skills/react-view-transitions/README.md +42 -0
- package/src/skills/react-view-transitions/SKILL.md +320 -0
- package/src/skills/react-view-transitions/VENDOR.md +16 -0
- package/src/skills/react-view-transitions/metadata.json +12 -0
- package/src/skills/react-view-transitions/references/css-recipes.md +242 -0
- package/src/skills/react-view-transitions/references/implementation.md +182 -0
- package/src/skills/react-view-transitions/references/nextjs.md +176 -0
- package/src/skills/react-view-transitions/references/patterns.md +262 -0
- package/src/skills/refactor.md +32 -0
- package/src/skills/release.md +43 -0
- package/src/skills/repo-map.md +36 -0
- package/src/skills/research.md +42 -0
- package/src/skills/review-response.md +36 -0
- package/src/skills/roast-me.md +28 -0
- package/src/skills/rule.md +100 -0
- package/src/skills/ship.md +49 -0
- package/src/skills/tdd.md +31 -0
- package/src/skills/tickets.md +44 -0
- package/src/skills/token-report/SKILL.md +76 -0
- package/src/skills/token-report/VENDOR.md +23 -0
- package/src/skills/token-report/scripts/token_report.mjs +1301 -0
- package/src/skills/wayfinder.md +49 -0
- package/src/skills/wiki.md +68 -0
- package/src/skills/workflow.md +38 -0
- package/themes/README.md +69 -0
- package/themes/_TEMPLATE.json +147 -0
- package/themes/biker.json +147 -0
- package/themes/commentator.json +147 -0
- package/themes/cyberpunk.json +147 -0
- package/themes/gamer.json +147 -0
- package/themes/imperial.json +147 -0
- package/themes/joker.json +147 -0
- package/themes/marvin.json +147 -0
- package/themes/mean.json +147 -0
- package/themes/military.json +147 -0
- package/themes/neutral.json +147 -0
- package/themes/opencode/README.md +58 -0
- package/themes/opencode/catppuccin.json +27 -0
- package/themes/opencode/dracula.json +27 -0
- package/themes/opencode/everforest.json +27 -0
- package/themes/opencode/gruvbox.json +27 -0
- package/themes/opencode/kanagawa.json +27 -0
- package/themes/opencode/neon.json +27 -0
- package/themes/opencode/nord.json +27 -0
- package/themes/opencode/rosepine.json +27 -0
- package/themes/opencode/tokyonight.json +27 -0
- package/themes/pirate.json +147 -0
- package/themes/sports.json +147 -0
- package/themes/verstappen.json +147 -0
- package/themes/wizard.json +147 -0
- package/web/dist/assets/About-DX1-VTBP.js +1 -0
- package/web/dist/assets/Activity-Dp9NaXOn.js +1 -0
- package/web/dist/assets/ActivityDetail-HpOBtrXS.js +1 -0
- package/web/dist/assets/Diff-Ct4G3Se8.js +5 -0
- package/web/dist/assets/Doctor-DGjkZFuA.js +1 -0
- package/web/dist/assets/Graph-b8o-S6kv.js +1 -0
- package/web/dist/assets/Harnesses-gr3-aHBF.js +1 -0
- package/web/dist/assets/Laws-DTwF5tTC.js +1 -0
- package/web/dist/assets/Library-BEZscyaQ.js +1 -0
- package/web/dist/assets/Markdown-k_LSLBGq.js +1 -0
- package/web/dist/assets/Profile-DydukTxP.js +1 -0
- package/web/dist/assets/Rules-CIRG4Ldt.js +1 -0
- package/web/dist/assets/Skills-Buoi1IBr.js +1 -0
- package/web/dist/assets/StatusBadge-CE_AJttl.js +1 -0
- package/web/dist/assets/Themes-CI4o3gJC.js +2 -0
- package/web/dist/assets/activity-BCW7e6Cy.js +1 -0
- package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-400-italic-Bserb8X5.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-400-normal-BG6hkEXj.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-500-italic-BGfwwhek.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-500-normal-DrDcrrxK.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-600-normal-CIXX6EOa.woff2 +0 -0
- package/web/dist/assets/hanken-grotesk-latin-700-normal-CeQ8H3UY.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
- package/web/dist/assets/index-B1PIx7RM.js +1 -0
- package/web/dist/assets/index-BnTQUSUz.js +12 -0
- package/web/dist/assets/index-Ck2iCdaC.js +2 -0
- package/web/dist/assets/index-DUCsDv7z.css +1 -0
- package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/web/dist/assets/jetbrains-mono-latin-600-normal-C8RAYTDA.woff2 +0 -0
- package/web/dist/assets/marked.esm-JKvyS0Pp.js +46 -0
- package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
- package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
- package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
- package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/web/dist/assets/react-DOpiTn5v.js +32 -0
- package/web/dist/assets/roman-DkYN0LX9.js +1 -0
- package/web/dist/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-600-normal-DjKNqYRj.woff2 +0 -0
- package/web/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-400-italic-YylcN9Ay.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-400-normal-Rg4St2Dn.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-700-italic-vNvENeTh.woff2 +0 -0
- package/web/dist/assets/space-mono-latin-700-normal-mWgeinG7.woff2 +0 -0
- package/web/dist/icon-maskable.svg +10 -0
- package/web/dist/icon.svg +8 -0
- package/web/dist/index.html +21 -0
- package/web/dist/manifest.webmanifest +14 -0
- package/web/dist/sw.js +40 -0
- package/web/src/pages/Laws.jsx +230 -0
package/js/diff.mjs
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harness diff` — `rituals/_harness_diff.py`. What a DEPLOYED install has that a fresh
|
|
3
|
+
* render of `src/` does not, so in-place edits can be reviewed and back-ported.
|
|
4
|
+
*
|
|
5
|
+
* WHAT THE CLOSURE WALK MISSED, AND IT IS A NEW KIND OF MISS. An import-aware `ast` walk puts
|
|
6
|
+
* this verb at 252 LOC over 10 functions, and every one of those is a few lines of reading a
|
|
7
|
+
* marker or a manifest. The verb's entire user-visible payload is `difflib.unified_diff`,
|
|
8
|
+
* which the walk counts as zero because `difflib` is not in `rituals/`. P5e learned that a
|
|
9
|
+
* walk cannot see through a `subprocess`; this is the same blind spot pointed the other way —
|
|
10
|
+
* it cannot see through the STANDARD LIBRARY either, and a stdlib call with no JS equivalent
|
|
11
|
+
* is a port, not a call. `js/lib/udiff.mjs` is where that went, and it is 273 lines against
|
|
12
|
+
* this file's 312 — the module the walk described as the whole verb is not half of it.
|
|
13
|
+
*
|
|
14
|
+
* THE SECOND thing the walk shows only as one name: `_diff_collect` runs a whole GLOBAL EMIT
|
|
15
|
+
* into a temp dir. `build.HOSTS[host]["emit_global"]` is a `build.*` reference like any other
|
|
16
|
+
* in the table, and behind it is the generator. That call is why this file imports
|
|
17
|
+
* `bin/geneseed.mjs` — the same in-process route `js/generate.mjs` established for `build`,
|
|
18
|
+
* under the same transitive `child_process` ban.
|
|
19
|
+
*
|
|
20
|
+
* WHAT NO CELL CAN VARY, stated rather than left implicit:
|
|
21
|
+
*
|
|
22
|
+
* * **The diff algorithm.** A cell can produce ONE edited file and see one hunk. It cannot
|
|
23
|
+
* produce the shapes where `difflib`'s choices differ from any other diff's — ties, the
|
|
24
|
+
* recursion's alignment, and above all `autojunk`, which only engages at 200 lines and
|
|
25
|
+
* changes the hunks on every real harness file. Gated as a corpus in
|
|
26
|
+
* `tests/test_pure_function_parity.py`, measured in both directions (28 of the 176 cases
|
|
27
|
+
* there disagree when `autojunk` is switched off, so the corpus is not describing a
|
|
28
|
+
* constant).
|
|
29
|
+
* * **The timestamp.** `_write_improvements` names the file `improvements-%Y%m%d-%H%M%S.md`
|
|
30
|
+
* and stamps the report with `datetime.now()`, so the two implementations cannot write the
|
|
31
|
+
* same bytes across a second boundary — and a cell runs them in sequence, so the boundary
|
|
32
|
+
* is a coin flip. `harness_golden._STAMPS` normalises both spellings out of the snapshot,
|
|
33
|
+
* which costs the FORMAT assertion and pays it back in
|
|
34
|
+
* `test_the_improvements_filename_is_stamped_the_same_way_by_both` — absolute, per
|
|
35
|
+
* implementation, because a comparison made tolerant of a value owes one somewhere else.
|
|
36
|
+
*/
|
|
37
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
38
|
+
import os from 'node:os';
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
|
|
41
|
+
import { emitGlobalInto } from '../bin/geneseed.mjs';
|
|
42
|
+
import { GLOBAL_MANIFEST, VERSION_MARKER, expanduser, opencodeConfigDir } from './hosts.mjs';
|
|
43
|
+
import {
|
|
44
|
+
EMIT_HOST_SCOPE, defaultTheme, footprintOfDir, modeOfDir, postureOfDir, readJsonMaybe,
|
|
45
|
+
readMaybe, themeOfDir,
|
|
46
|
+
} from './installs.mjs';
|
|
47
|
+
import { unifiedDiff, pySplitLines } from './lib/udiff.mjs';
|
|
48
|
+
import { pyPathStr, pyPrint, pyPrintErr, readText, writeText } from './lib/fs.mjs';
|
|
49
|
+
|
|
50
|
+
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `_harness_core._T0` — this process's start, in epoch seconds.
|
|
54
|
+
*
|
|
55
|
+
* Python samples it at `_harness_core` import time and `_flush_export_notes` is its only
|
|
56
|
+
* reader. Module load is the same moment here for the same reason: the entry imports this
|
|
57
|
+
* module before it parses an argument, so nothing an invocation does can have happened
|
|
58
|
+
* before it.
|
|
59
|
+
*/
|
|
60
|
+
const T0 = Date.now() / 1000;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* `_harness_diff._cmp_key` — how an owned file is keyed for drift.
|
|
64
|
+
*
|
|
65
|
+
* `.geneseed-version` carries a build-DATE stamp that is not a local edit, so it is keyed on
|
|
66
|
+
* the fingerprint token alone. `text.split()` is Python's whitespace split with no separator:
|
|
67
|
+
* it collapses runs AND strips leading whitespace, which `String.split(' ')` does neither of.
|
|
68
|
+
*/
|
|
69
|
+
export function cmpKey(rel, text) {
|
|
70
|
+
if (rel === VERSION_MARKER) {
|
|
71
|
+
const toks = text.split(/\s+/).filter((s) => s !== '');
|
|
72
|
+
return toks.length ? toks[0] : text;
|
|
73
|
+
}
|
|
74
|
+
return text;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** `_harness_diff._owned_set` — the files a global install claims, per its manifest. */
|
|
78
|
+
function ownedSet(d) {
|
|
79
|
+
const data = readJsonMaybe(path.join(d, GLOBAL_MANIFEST));
|
|
80
|
+
if (data === null || typeof data !== 'object') return new Set();
|
|
81
|
+
const owned = data.owned;
|
|
82
|
+
return new Set(Array.isArray(owned) ? owned : []);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* `_harness_diff._manifest_scope` — "project" or "global", defaulting to "global".
|
|
87
|
+
*
|
|
88
|
+
* Every manifest predating this key is a GLOBAL install, so a missing key, an unreadable file
|
|
89
|
+
* and a malformed one all read as "global". `data.get("scope") or "global"` — an EMPTY string
|
|
90
|
+
* falls back too, which `?? 'global'` alone would not do.
|
|
91
|
+
*/
|
|
92
|
+
export function manifestScope(d) {
|
|
93
|
+
const data = readJsonMaybe(path.join(d, GLOBAL_MANIFEST));
|
|
94
|
+
if (data === null || typeof data !== 'object') return 'global';
|
|
95
|
+
return data.scope || 'global';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `contextlib.redirect_stdout(io.StringIO())` around the emit.
|
|
100
|
+
*
|
|
101
|
+
* The emit prints its own summary line, which is not part of `diff`'s output; the Python
|
|
102
|
+
* swallows it and lets stderr through, so a WARN from the emit still reaches the user. Both
|
|
103
|
+
* halves matter and only the first needs code here — `pyPrint` and every driver funnel go
|
|
104
|
+
* through `process.stdout.write`, so replacing that one method is the whole redirect.
|
|
105
|
+
*
|
|
106
|
+
* `finally`, because the emit can throw: a swallow that leaked would silence every later line
|
|
107
|
+
* this process printed, including the refusal explaining why.
|
|
108
|
+
*/
|
|
109
|
+
export function withStdoutSwallowed(fn) {
|
|
110
|
+
const real = process.stdout.write.bind(process.stdout);
|
|
111
|
+
process.stdout.write = () => true;
|
|
112
|
+
try {
|
|
113
|
+
return fn();
|
|
114
|
+
} finally {
|
|
115
|
+
process.stdout.write = real;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* `_harness_diff._diff_collect` — returns `{target, theme, files}`, with `files === null` for
|
|
121
|
+
* "nothing to compare here".
|
|
122
|
+
*
|
|
123
|
+
* FIVE VALUES ARE READ OFF THE DEPLOYMENT rather than defaulted, and each has a scar behind
|
|
124
|
+
* it. The theme, so themed wording is not reported as a local edit. The EMIT, so a Claude
|
|
125
|
+
* install is not diffed against an OpenCode-dialect tree — which would flag every agent and
|
|
126
|
+
* AGENT.md as drift. The FOOTPRINT, because rendering 'expected' at the wrong one reports
|
|
127
|
+
* AGENT.md and laws/universal.md as edited on every lean install, so a freshly rebuilt
|
|
128
|
+
* harness looked drifted the moment it finished building and no rebuild could clear it.
|
|
129
|
+
*
|
|
130
|
+
* The POSTURE and the MODE joined them late, and the reference is where the hole came from:
|
|
131
|
+
* `_build_core`'s defaults are `peer`/`direct` and `rituals/harness.py` never overrode them,
|
|
132
|
+
* so every verb that rendered rendered at the defaults. An install that chose any other
|
|
133
|
+
* register reported its own `## Posture` and `## Mode` sections as a local edit for as long
|
|
134
|
+
* as it lived — and `apiRestore` would have written the default register back over the user's
|
|
135
|
+
* choice, which is the same defect with teeth. `rebuild-all` never had it: it reads
|
|
136
|
+
* `postureOfDir`/`modeOfDir` per install root, and this now asks the same two questions.
|
|
137
|
+
*/
|
|
138
|
+
export function diffCollect({ target = null, theme = null, emit = null, footprint = null } = {}) {
|
|
139
|
+
// `Path(target).expanduser()` — expanded and NOT resolved, which is the opposite of
|
|
140
|
+
// `cmd_version`'s `.expanduser().resolve()` two files over. It is observable: `target` is
|
|
141
|
+
// printed on the summary line, so `--target .` reports `.` here and an absolute path
|
|
142
|
+
// there. Reproduced rather than made consistent, because consistency would be a change to
|
|
143
|
+
// the reference's output.
|
|
144
|
+
const dir = target ? expanduser(target) : opencodeConfigDir();
|
|
145
|
+
if (!existsSync(path.join(dir, GLOBAL_MANIFEST))) return { target: dir, theme, files: null };
|
|
146
|
+
if (manifestScope(dir) === 'project') return { target: dir, theme, files: null };
|
|
147
|
+
|
|
148
|
+
let themeName = theme;
|
|
149
|
+
if (!themeName) themeName = themeOfDir(dir);
|
|
150
|
+
// The Python INLINES this read rather than calling `_default_theme`, and the two bodies
|
|
151
|
+
// are identical — so this calls the one owner instead of copying the body a third time.
|
|
152
|
+
if (!themeName) themeName = defaultTheme();
|
|
153
|
+
|
|
154
|
+
let emitName = emit;
|
|
155
|
+
if (!emitName) {
|
|
156
|
+
const marker = readMaybe(path.join(dir, '.geneseed-emit'));
|
|
157
|
+
emitName = marker === null ? '' : marker.trim();
|
|
158
|
+
}
|
|
159
|
+
const host = (EMIT_HOST_SCOPE.get(emitName) ?? ['opencode', 'global'])[0];
|
|
160
|
+
|
|
161
|
+
const fp = footprint || footprintOfDir(dir);
|
|
162
|
+
|
|
163
|
+
const files = [];
|
|
164
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), 'geneseed-diff-'));
|
|
165
|
+
try {
|
|
166
|
+
const expected = path.join(tmp, 'expected');
|
|
167
|
+
withStdoutSwallowed(() => emitGlobalInto(host, {
|
|
168
|
+
theme: themeName, out: path.join(tmp, 'bundle'), cfgDir: expected, footprint: fp,
|
|
169
|
+
posture: postureOfDir(dir), mode: modeOfDir(dir),
|
|
170
|
+
}));
|
|
171
|
+
const rels = [...new Set([...ownedSet(dir), ...ownedSet(expected)])].sort();
|
|
172
|
+
for (const rel of rels) {
|
|
173
|
+
const a = path.join(dir, rel);
|
|
174
|
+
const b = path.join(expected, rel);
|
|
175
|
+
if (isFile(a) && isFile(b)) {
|
|
176
|
+
// `read_text(errors="replace")` — Node's utf8 decoder substitutes U+FFFD for the
|
|
177
|
+
// same bytes, and `readText` folds the line endings the way `Path.read_text` does.
|
|
178
|
+
const ta = readText(a);
|
|
179
|
+
const tb = readText(b);
|
|
180
|
+
if (cmpKey(rel, ta) !== cmpKey(rel, tb)) {
|
|
181
|
+
const diff = unifiedDiff(pySplitLines(tb), pySplitLines(ta), {
|
|
182
|
+
fromfile: `source/${rel}`, tofile: `deployed/${rel}`, lineterm: '',
|
|
183
|
+
});
|
|
184
|
+
files.push({ rel, status: 'edited', diff });
|
|
185
|
+
}
|
|
186
|
+
} else if (isFile(a)) {
|
|
187
|
+
const body = pySplitLines(readText(a));
|
|
188
|
+
files.push({
|
|
189
|
+
rel,
|
|
190
|
+
status: 'added',
|
|
191
|
+
diff: ['(only in deployed — your addition)', '', ...body.map((ln) => `+${ln}`)],
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
files.push({
|
|
195
|
+
rel, status: 'missing', diff: ['(in source, not deployed — re-emit to add)'],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
// `tempfile.TemporaryDirectory()`'s cleanup, which runs on the exception path too.
|
|
201
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
// Already sorted by `rel` above; the Python sorts again after the loop and so does this,
|
|
204
|
+
// because the two sorts are over different things — that one is over the RECORDS, and a
|
|
205
|
+
// port that dropped it would agree only while the set-union sort happens to be stable.
|
|
206
|
+
files.sort((x, y) => (x.rel < y.rel ? -1 : x.rel > y.rel ? 1 : 0));
|
|
207
|
+
return { target: dir, theme: themeName, files };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** `_harness_diff._improvements_md` — pure; `when` is the caller's timestamp. */
|
|
211
|
+
export function improvementsMd(target, theme, files, when) {
|
|
212
|
+
const edited = files.filter((f) => f.status === 'edited');
|
|
213
|
+
const added = files.filter((f) => f.status === 'added');
|
|
214
|
+
const missing = files.filter((f) => f.status === 'missing');
|
|
215
|
+
const lines = [
|
|
216
|
+
'# Geneseed — deployed improvements to back-port',
|
|
217
|
+
'',
|
|
218
|
+
`- captured: ${when}`,
|
|
219
|
+
`- deployed: \`${target}\``,
|
|
220
|
+
`- theme: ${theme}`,
|
|
221
|
+
`- ${edited.length} edited · ${added.length} added in deployed · `
|
|
222
|
+
+ `${missing.length} missing from deployed`,
|
|
223
|
+
'',
|
|
224
|
+
'The deployed harness drifted from a fresh render of `src/` — typically the',
|
|
225
|
+
'self-improvement loops editing agent/skill files in place. Hand this file to',
|
|
226
|
+
'an agent in the Geneseed source repo and ask it to fold the changes below',
|
|
227
|
+
'back into `src/`. Diffs read source -> deployed; the expected copy was',
|
|
228
|
+
'rendered in the deployed theme, so only genuine local edits appear.',
|
|
229
|
+
'',
|
|
230
|
+
];
|
|
231
|
+
const label = {
|
|
232
|
+
edited: 'edited in deployed',
|
|
233
|
+
added: 'only in deployed — your addition',
|
|
234
|
+
missing: 'in source, not deployed',
|
|
235
|
+
};
|
|
236
|
+
for (const f of files) {
|
|
237
|
+
lines.push(`## \`${f.rel}\` (${label[f.status]})`, '', '```diff', ...f.diff, '```', '');
|
|
238
|
+
}
|
|
239
|
+
return `${lines.join('\n')}\n`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* `datetime.now().strftime(...)` for the two formats this file uses.
|
|
244
|
+
*
|
|
245
|
+
* Local time, not UTC — `datetime.datetime.now()` has no timezone argument here. Written out
|
|
246
|
+
* rather than reached for through `toISOString()` (which is UTC) or `toLocaleString()` (which
|
|
247
|
+
* is locale-dependent and would change the filename on a French machine).
|
|
248
|
+
*/
|
|
249
|
+
function stamp(d) {
|
|
250
|
+
const p2 = (n) => String(n).padStart(2, '0');
|
|
251
|
+
const date = `${d.getFullYear()}${p2(d.getMonth() + 1)}${p2(d.getDate())}`;
|
|
252
|
+
const time = `${p2(d.getHours())}${p2(d.getMinutes())}${p2(d.getSeconds())}`;
|
|
253
|
+
return {
|
|
254
|
+
file: `improvements-${date}-${time}.md`,
|
|
255
|
+
when: `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())} `
|
|
256
|
+
+ `${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* `_harness_diff._write_improvements`.
|
|
262
|
+
*
|
|
263
|
+
* The default destination is `<deployed>/improvements/<timestamp>.md` — inside the install it
|
|
264
|
+
* describes, and never in its manifest: rebuilds compare only owned files so the report is
|
|
265
|
+
* not itself reported as drift, re-emits do not clobber it, and uninstall leaves it (the same
|
|
266
|
+
* contract as the memory store).
|
|
267
|
+
*
|
|
268
|
+
* ONE `now` FOR BOTH USES, as the Python has: the filename and the `captured:` line come from
|
|
269
|
+
* the same instant, so a run that crosses a second boundary cannot name a file one second
|
|
270
|
+
* away from what it says inside.
|
|
271
|
+
*/
|
|
272
|
+
export function writeImprovements(target, theme, files, outPath = null) {
|
|
273
|
+
const now = stamp(new Date());
|
|
274
|
+
// `Path(out_path).expanduser()` — again NOT resolved, and again observable: the path is
|
|
275
|
+
// printed, so `--out p.md` reports `p.md`.
|
|
276
|
+
const p = outPath
|
|
277
|
+
? expanduser(outPath)
|
|
278
|
+
: path.join(target, 'improvements', now.file);
|
|
279
|
+
mkdirSync(path.dirname(p), { recursive: true });
|
|
280
|
+
writeText(p, improvementsMd(target, theme, files, now.when));
|
|
281
|
+
return p;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* `_harness_diff.export_improvements` — the drift report as a LIBRARY call, for the callers
|
|
286
|
+
* that want the artifact without the verb's printing.
|
|
287
|
+
*
|
|
288
|
+
* Returns `[path, files]`: `path` is null when nothing was written, and the two reasons are
|
|
289
|
+
* distinguishable in `files` — `null` is "no deployed global install to compare", `[]` is
|
|
290
|
+
* "an install, and no drift". `setup` is the caller and reads exactly that: it prints the
|
|
291
|
+
* path when there is one and says nothing when there is not.
|
|
292
|
+
*
|
|
293
|
+
* `flushExportNotes` below is its neighbour in the Python, and P5f left it out on the
|
|
294
|
+
* keep-vs-delete criterion this port uses: a function with no caller in this binary. P8b is
|
|
295
|
+
* the due date that docblock named — `cmdBootstrap` is the caller.
|
|
296
|
+
*/
|
|
297
|
+
export function exportImprovements(target = null, theme = null, outPath = null) {
|
|
298
|
+
const collected = diffCollect({ target, theme });
|
|
299
|
+
// Python's `if not files` — null AND the empty list both return, which is why the caller
|
|
300
|
+
// gets `files` back rather than a bare path and can tell the two apart itself.
|
|
301
|
+
if (!collected.files || collected.files.length === 0) return [null, collected.files];
|
|
302
|
+
return [writeImprovements(collected.target, collected.theme, collected.files, outPath),
|
|
303
|
+
collected.files];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* `_harness_diff._flush_export_notes` — re-print, on the RESTORED terminal, every improvements
|
|
308
|
+
* file exported since this process started.
|
|
309
|
+
*
|
|
310
|
+
* WHY IT SCANS RATHER THAN TRACKS. The export that matters most is the one an update STEP made
|
|
311
|
+
* — in the reference that is a child process, so a call counter in this one would never see it.
|
|
312
|
+
* It reads the GLOBAL install's `improvements/` and takes anything newer than `T0 - 1`, and the
|
|
313
|
+
* one-second slack is the reference's: a file written in the same second the process started
|
|
314
|
+
* can carry an mtime a hair below it.
|
|
315
|
+
*
|
|
316
|
+
* Its callers in the Python are `cmd_setup`'s curses arm, the TUI's teardown and
|
|
317
|
+
* `cmd_bootstrap`; here it is `cmdBootstrap` alone, because the first two are P7's.
|
|
318
|
+
*/
|
|
319
|
+
export function flushExportNotes() {
|
|
320
|
+
let fresh = [];
|
|
321
|
+
try {
|
|
322
|
+
const d = path.join(String(opencodeConfigDir()), 'improvements');
|
|
323
|
+
// `Path.glob` on a directory that is not there yields nothing rather than raising, which
|
|
324
|
+
// is why the reference needs no existence check and this needs the readdir in the try.
|
|
325
|
+
fresh = readdirSync(d)
|
|
326
|
+
.filter((n) => n.startsWith('improvements-') && n.endsWith('.md'))
|
|
327
|
+
.map((n) => path.join(d, n))
|
|
328
|
+
.filter((p) => statSync(p).mtimeMs / 1000 >= T0 - 1)
|
|
329
|
+
.sort();
|
|
330
|
+
} catch { return; }
|
|
331
|
+
if (fresh.length === 0) return;
|
|
332
|
+
for (const p of fresh) pyPrint(`[geneseed] improvements file saved: ${pyPathStr(p)}\n`);
|
|
333
|
+
pyPrint('[geneseed] the deployed harness carried local edits — hand the file to '
|
|
334
|
+
+ 'your agent to back-port them into src/.\n');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** `_harness_diff.cmd_diff`. */
|
|
338
|
+
export function cmdDiff(args) {
|
|
339
|
+
const { target, theme, files } = diffCollect({ target: args.target, theme: args.theme });
|
|
340
|
+
if (files === null) {
|
|
341
|
+
if (existsSync(path.join(target, GLOBAL_MANIFEST)) && manifestScope(target) === 'project') {
|
|
342
|
+
pyPrintErr(`[diff] ${target} is a PROJECT install — diff only compares GLOBAL `
|
|
343
|
+
+ 'installs against a fresh render (a project layer is diffed against '
|
|
344
|
+
+ 'its own repo, which this command does not do). Pass --target at a '
|
|
345
|
+
+ 'global config dir (opencode-global/claude-global/bob-global/'
|
|
346
|
+
+ 'copilot-global), or omit '
|
|
347
|
+
+ '--target to use the default.\n');
|
|
348
|
+
} else {
|
|
349
|
+
pyPrintErr(`[diff] no global Geneseed install at ${target} (no ${GLOBAL_MANIFEST}). `
|
|
350
|
+
+ 'Pass --target, or run `--emit opencode-global` first.\n');
|
|
351
|
+
}
|
|
352
|
+
return 1;
|
|
353
|
+
}
|
|
354
|
+
const edited = files.filter((f) => f.status === 'edited');
|
|
355
|
+
const added = files.filter((f) => f.status === 'added');
|
|
356
|
+
const missing = files.filter((f) => f.status === 'missing');
|
|
357
|
+
pyPrint(`[diff] deployed ${target} vs source (theme: ${theme})\n`);
|
|
358
|
+
pyPrint(`[diff] ${edited.length} edited, ${added.length} added-in-deployed, `
|
|
359
|
+
+ `${missing.length} missing-from-deployed\n`);
|
|
360
|
+
for (const f of edited) pyPrint(` ~ ${f.rel} (edited in deployed — review to back-port)\n`);
|
|
361
|
+
for (const f of added) pyPrint(` + ${f.rel} (only in deployed — your addition)\n`);
|
|
362
|
+
for (const f of missing) pyPrint(` - ${f.rel} (in source, not deployed — re-emit to add)\n`);
|
|
363
|
+
if (args.out) {
|
|
364
|
+
if (files.length) {
|
|
365
|
+
// Bare `--out` is argparse's `nargs="?"` const: True, meaning "the default timestamped
|
|
366
|
+
// path". `parse()` gives a flag `true` and an option a string, so the two arrive
|
|
367
|
+
// distinguishable here without a third kind of entry in the VERBS table.
|
|
368
|
+
const outPath = args.out === true ? null : args.out;
|
|
369
|
+
pyPrint(`[diff] improvements file written: ${
|
|
370
|
+
writeImprovements(target, theme, files, outPath)}\n`);
|
|
371
|
+
} else {
|
|
372
|
+
pyPrint('[diff] no differences — nothing written.\n');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (args.full) {
|
|
376
|
+
for (const f of edited) {
|
|
377
|
+
pyPrint(`\n--- ${f.rel} (source -> deployed) ---\n`);
|
|
378
|
+
pyPrint(`${f.diff.join('\n')}\n`);
|
|
379
|
+
}
|
|
380
|
+
} else if (edited.length && !args.out) {
|
|
381
|
+
pyPrint('\nRun with --full to see the line-level diffs, or --out FILE to export them.\n');
|
|
382
|
+
}
|
|
383
|
+
return 0;
|
|
384
|
+
}
|