geneseed 3.3.0 → 3.5.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 +323 -0
- package/DESIGN.md +11 -14
- package/QUICKSTART.md +3 -1
- package/README.md +53 -44
- package/SETUP.md +25 -27
- package/SHIPPED.md +26 -32
- package/adapters/bob/README.md +12 -9
- package/adapters/copilot/README.md +28 -26
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +1 -1
- package/adapters/opencode/README.md +8 -4
- package/adapters/opencode/plugins/geneseed-context.js +5 -3
- package/adapters/opencode/plugins/geneseed-workflow.js +30 -3
- package/adapters/opencode/workflows/_runtime.js +97 -7
- package/bin/build-driver.mjs +5 -1248
- package/bin/geneseed-cli.mjs +11 -6
- package/bin/geneseed-hook.mjs +4 -2
- package/docs/candidate-hosts.md +1 -1
- package/docs/design-history.md +37 -37
- package/docs/extending.md +49 -27
- package/docs/token-footprint.md +52 -26
- package/docs/web/agents.md +1 -1
- package/docs/web/claude-hooks.md +3 -2
- package/docs/web/collaboration.md +2 -2
- package/docs/web/first-session.md +2 -2
- package/docs/web/footprint.md +2 -2
- package/docs/web/harnesses.md +1 -1
- package/docs/web/model.md +1 -1
- package/docs/web/plugin-workflow.md +1 -0
- package/docs/web/rules.md +3 -3
- package/docs/web/setup-choices.md +2 -2
- package/docs/web/skills.md +1 -1
- package/docs/web/themes-author.md +1 -1
- package/docs/web-ui.md +18 -10
- package/harness.config.json +1 -1
- package/js/README.md +6 -5
- package/js/build/driver.mjs +1191 -0
- package/js/build/emit-claude.mjs +17 -10
- package/js/build/emit-common.mjs +4 -4
- package/js/build/generate.mjs +1 -1
- package/js/build/render.mjs +48 -44
- package/js/build/stubs.mjs +1 -1
- package/js/build/themes.mjs +4 -4
- package/js/build/version.mjs +12 -0
- package/js/cli-table.json +21 -6
- package/js/hosts/hooks.mjs +89 -74
- package/js/hosts/hosts.mjs +19 -2
- package/js/hosts/mcp.mjs +14 -20
- package/js/hosts/native.mjs +85 -25
- package/js/hosts/opencode.mjs +14 -0
- package/js/hosts/settings.mjs +52 -104
- package/js/inspect/checks-authoring.mjs +117 -3
- package/js/inspect/diff.mjs +1 -1
- package/js/inspect/doctor.mjs +1 -1
- package/js/inspect/excludes.mjs +1 -1
- package/js/inspect/inventory.mjs +12 -7
- package/js/inspect/scan.mjs +3 -14
- package/js/inspect/status.mjs +2 -2
- package/js/inspect/validate.mjs +1 -1
- package/js/lib/fs.mjs +23 -8
- package/js/lib/json.mjs +7 -1
- package/js/lib/proc.mjs +6 -4
- package/js/maintain/migrate.mjs +1 -1
- package/js/maintain/setup.mjs +1 -1
- package/js/maintain/uninstall.mjs +17 -15
- package/js/ui/cli.mjs +19 -0
- package/js/ui/menu.mjs +1 -1
- package/js/web/actions.mjs +4 -4
- package/js/web/activity.mjs +4 -4
- package/js/web/api.mjs +14 -11
- package/js/web/daemon.mjs +1 -1
- package/js/web/docs.mjs +12 -12
- package/js/web/handler.mjs +12 -5
- package/js/web/jobs.mjs +1 -1
- package/package.json +1 -1
- package/registry.json +86 -79
- package/src/AGENT.md.tmpl +83 -48
- package/src/agents/architect.md +4 -1
- package/src/agents/developer.md +2 -2
- package/src/agents/explorer.md +2 -0
- package/src/agents/operator.md +13 -0
- package/src/agents/pragmatist.md +12 -0
- package/src/agents/researcher.md +72 -0
- package/src/agents/reviewer.md +5 -2
- package/src/agents/security.md +7 -1
- package/src/agents/skeptic.md +2 -1
- package/src/agents/tester.md +5 -1
- package/src/agents/user-advocate.md +13 -0
- package/src/doctrines/README.md +26 -3
- package/src/doctrines/craft.md +78 -5
- package/src/doctrines/ops.md +62 -0
- package/src/doctrines/process.md +91 -4
- package/src/doctrines/rigor.md +41 -1
- package/src/laws/universal.md +27 -21
- package/src/memory/README.md +4 -2
- package/src/modes/foreman.md +5 -5
- package/src/notebook/README.md +2 -1
- package/src/ontology/universal.md +71 -67
- package/src/postures/peer.md +3 -0
- package/src/skills/_self-improvement.md +19 -0
- package/src/skills/_template.md +1 -12
- package/src/skills/brainstorm.md +37 -19
- package/src/skills/bruno-collection-generator.md +2 -13
- package/src/skills/bruno-test-writer.md +3 -14
- package/src/skills/ci-fix.md +49 -0
- package/src/skills/codebase-design.md +1 -12
- package/src/skills/commit.md +1 -12
- package/src/skills/consolidate-memory.md +50 -0
- package/src/skills/council.md +1 -12
- package/src/skills/daydream/LICENSE +21 -0
- package/src/skills/daydream/VENDOR.md +3 -1
- package/src/skills/debug.md +1 -12
- package/src/skills/deps-audit.md +46 -0
- package/src/skills/develop.md +38 -20
- package/src/skills/document-project.md +1 -12
- package/src/skills/domain-modeling.md +1 -12
- package/src/skills/forge-mcp.md +1 -12
- package/src/skills/fresh-eyes.md +1 -12
- package/src/skills/frontend-design.md +1 -12
- package/src/skills/geneseed-code-review.md +7 -14
- package/src/skills/geneseed.md +1 -12
- package/src/skills/git-archaeology.md +1 -12
- package/src/skills/git-rescue.md +1 -12
- package/src/skills/handoff.md +1 -12
- package/src/skills/herdr.md +1 -12
- package/src/skills/ingest.md +1 -12
- package/src/skills/migrate.md +1 -12
- package/src/skills/opencode-theme.md +1 -12
- package/src/skills/parallel-agents.md +4 -15
- package/src/skills/pipeline.md +6 -17
- package/src/skills/plan.md +1 -12
- package/src/skills/ponytail.md +1 -12
- package/src/skills/profile.md +1 -12
- package/src/skills/prose.md +1 -12
- package/src/skills/prototype.md +1 -12
- package/src/skills/quiz.md +43 -0
- package/src/skills/react-view-transitions/VENDOR.md +7 -1
- package/src/skills/refactor.md +1 -12
- package/src/skills/release.md +1 -12
- package/src/skills/repo-map.md +1 -12
- package/src/skills/research.md +5 -13
- package/src/skills/review-response.md +1 -12
- package/src/skills/roast-me.md +1 -12
- package/src/skills/rule.md +1 -12
- package/src/skills/security-audit.md +51 -0
- package/src/skills/ship.md +1 -12
- package/src/skills/skill-forge.md +59 -0
- package/src/skills/teach.md +50 -0
- package/src/skills/tickets.md +1 -12
- package/src/skills/wayfinder.md +1 -12
- package/src/skills/wiki.md +1 -12
- package/src/skills/workflow.md +82 -26
- package/src/skills/worktree.md +52 -0
- package/themes/_TEMPLATE.json +15 -12
- package/themes/biker.json +14 -11
- package/themes/commentator.json +14 -11
- package/themes/cyberpunk.json +14 -11
- package/themes/gamer.json +14 -11
- package/themes/imperial.json +14 -11
- package/themes/joker.json +14 -11
- package/themes/marvin.json +14 -11
- package/themes/mean.json +14 -11
- package/themes/military.json +14 -11
- package/themes/neutral.json +15 -12
- package/themes/opencode/README.md +2 -2
- package/themes/pirate.json +14 -11
- package/themes/sports.json +14 -11
- package/themes/verstappen.json +14 -11
- package/themes/wizard.json +14 -11
- package/web/dist/assets/Activity-Flgf4kHy.js +1 -0
- package/web/dist/assets/ActivityDetail-DIpr85nh.js +1 -0
- package/web/dist/assets/CatalogRow-CjyLjaLb.js +1 -0
- package/web/dist/assets/Diff-Deisdha8.js +2 -0
- package/web/dist/assets/Docs-DGUdY4l4.js +2 -0
- package/web/dist/assets/Doctor-F74lJ69P.js +1 -0
- package/web/dist/assets/FilterInput-BakJLvaV.js +1 -0
- package/web/dist/assets/GreenhouseView-D072lBiD.js +2 -0
- package/web/dist/assets/Harness-DNRP2Va7.js +1 -0
- package/web/dist/assets/JournalView-DWFqZ0Iw.js +1 -0
- package/web/dist/assets/Laws-BN6g3HU7.js +3 -0
- package/web/dist/assets/Library-CDOv4emM.js +1 -0
- package/web/dist/assets/Markdown-BQAvN8xx.js +38 -0
- package/web/dist/assets/OperatorHudView-CwNIUVX8.js +2 -0
- package/web/dist/assets/Profile-CpP9BTne.js +1 -0
- package/web/dist/assets/Rules-DFXsUCB7.js +1 -0
- package/web/dist/assets/Settings-BqZVUtJ5.js +1 -0
- package/web/dist/assets/Skills-Cu6kBgfF.js +1 -0
- package/web/dist/assets/StatusBadge-Cg6j-NJL.js +1 -0
- package/web/dist/assets/activity-iWaWVtsR.js +1 -0
- package/web/dist/assets/drawable-BgfrEBer.js +1 -0
- package/web/dist/assets/index-CAqrWhKl.js +2 -0
- package/web/dist/assets/index-Ruh93A3A.css +1 -0
- package/web/dist/assets/jobBuckets-Dbq8ZlWW.js +1 -0
- package/web/dist/assets/lawCats-BLgxl151.js +1 -0
- package/web/dist/assets/react-DbkvxWG5.js +8 -0
- package/web/dist/assets/rolldown-runtime-CbXtAM7H.js +1 -0
- package/web/dist/index.html +4 -3
- package/web/src/pages/Laws.jsx +22 -8
- package/src/skills/clarify.md +0 -28
- package/src/skills/crash-course.md +0 -29
- package/src/skills/decode.md +0 -29
- package/src/skills/drill.md +0 -29
- package/src/skills/feynman.md +0 -27
- package/src/skills/gap-detector.md +0 -27
- package/src/skills/learning-path.md +0 -27
- package/src/skills/react-view-transitions/README.md +0 -42
- package/src/skills/tdd.md +0 -31
- package/web/dist/assets/Activity-BfVcLrda.js +0 -1
- package/web/dist/assets/ActivityDetail-CQXccGPl.js +0 -1
- package/web/dist/assets/CatalogRow-DALEQImZ.js +0 -1
- package/web/dist/assets/Diff-BIjxXx46.js +0 -5
- package/web/dist/assets/Doctor-CJLbgsrw.js +0 -1
- package/web/dist/assets/FilterInput-CkUCk_lY.js +0 -1
- package/web/dist/assets/GreenhouseView-DfC5IILJ.js +0 -2
- package/web/dist/assets/Harness-Dm5CCU-v.js +0 -1
- package/web/dist/assets/JournalView-Cpu2eI4s.js +0 -1
- package/web/dist/assets/Laws-DDDVFuQz.js +0 -3
- package/web/dist/assets/Library-Dao3T9na.js +0 -1
- package/web/dist/assets/Markdown-C__nuWX5.js +0 -46
- package/web/dist/assets/OperatorHudView-DEpCtAjP.js +0 -2
- package/web/dist/assets/Profile-BlXtUlh2.js +0 -1
- package/web/dist/assets/Rules-DIre6gxR.js +0 -1
- package/web/dist/assets/Skills-rY13ZPhM.js +0 -1
- package/web/dist/assets/StatusBadge-wW_RS_my.js +0 -1
- package/web/dist/assets/activity-DpYPKNrA.js +0 -1
- package/web/dist/assets/index-BEZvPvpF.js +0 -1
- package/web/dist/assets/index-DNQE5ydy.js +0 -10
- package/web/dist/assets/index-DzdQjvM1.css +0 -1
- package/web/dist/assets/index-PmQGRS1X.js +0 -2
- package/web/dist/assets/jobBuckets-DfTUJUxR.js +0 -1
- package/web/dist/assets/lawCats-DUS9UTH4.js +0 -1
- package/web/dist/assets/react-DOpiTn5v.js +0 -32
package/js/hosts/settings.mjs
CHANGED
|
@@ -90,18 +90,22 @@ const consentRuleOn = (d, excluded = []) =>
|
|
|
90
90
|
* doctrine section — and a boundary that keeps asking for a rule the install did not adopt is
|
|
91
91
|
* the one disagreement the tiering is not allowed to produce. (It does NOT follow that the
|
|
92
92
|
* string is gone from the bundle; see `claudeHookGroups` for what D5 ships anyway and why.)
|
|
93
|
-
* `rm -rf *`,
|
|
94
|
-
*
|
|
95
|
-
*
|
|
93
|
+
* `rm -rf *`, the force pushes and the history-discarding git verbs (`reset --hard`,
|
|
94
|
+
* `clean -f`, `branch -D`, `checkout --`) stay in EVERY build: they are Law IV's territory —
|
|
95
|
+
* an always-on invariant — not the process pack's. They mirror `DESTRUCTIVE_GIT_RE` in
|
|
96
|
+
* js/hosts/hooks.mjs, so Claude's git-gate and OpenCode's permission ask about the same acts;
|
|
97
|
+
* before, OpenCode only logged the last four.
|
|
96
98
|
*/
|
|
99
|
+
const LAW_IV_BASH = ['git push --force*', 'git push -f*', 'git reset --hard*', 'git clean -f*',
|
|
100
|
+
'git branch -D*', 'git checkout -- *'];
|
|
101
|
+
|
|
97
102
|
function defaultPermission(doctrines = null, excluded = []) {
|
|
98
103
|
const bash = { 'rm -rf *': 'ask' };
|
|
99
104
|
if (consentRuleOn(doctrines, excluded)) {
|
|
100
105
|
bash['git commit*'] = 'ask';
|
|
101
106
|
bash['git push*'] = 'ask';
|
|
102
107
|
}
|
|
103
|
-
bash[
|
|
104
|
-
bash['git push -f*'] = 'ask';
|
|
108
|
+
for (const k of LAW_IV_BASH) bash[k] = 'ask';
|
|
105
109
|
return { bash };
|
|
106
110
|
}
|
|
107
111
|
|
|
@@ -112,7 +116,7 @@ function defaultPermission(doctrines = null, excluded = []) {
|
|
|
112
116
|
* has to be able to recognise a key that is Geneseed's business at all — to add it back when
|
|
113
117
|
* the pack returns, and to name it when this build no longer wants it but will not remove it.
|
|
114
118
|
*/
|
|
115
|
-
const OWNED_BASH = ['rm -rf *', 'git commit*', 'git push*',
|
|
119
|
+
const OWNED_BASH = ['rm -rf *', 'git commit*', 'git push*', ...LAW_IV_BASH];
|
|
116
120
|
|
|
117
121
|
/**
|
|
118
122
|
* Bring an ALREADY-WRITTEN `permission` block back into line with the pack selection, and
|
|
@@ -639,15 +643,16 @@ export function hookPrefix({ runner, entry, platform = process.platform } = {})
|
|
|
639
643
|
/**
|
|
640
644
|
* `_build_settings._claude_hook_groups` — Geneseed's Claude hooks, keyed by event.
|
|
641
645
|
*
|
|
642
|
-
* ⚠ `doctrines` IS WHERE PROMPT AND BOUNDARY ARE KEPT IN AGREEMENT. The `git-gate` group
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
646
|
+
* ⚠ `doctrines` IS WHERE PROMPT AND BOUNDARY ARE KEPT IN AGREEMENT. The `git-gate` group
|
|
647
|
+
* carries TWO rules: Law IV (destructive git — universal, every build) and doctrine process 5
|
|
648
|
+
* (consent before commit/push). With the process pack off, or process 5 excluded, that second
|
|
649
|
+
* rule is no longer BINDING — so a gate that stopped every commit to ask for consent to a rule
|
|
650
|
+
* the install did not adopt is the disagreement the three-tier split forbids. The group stays
|
|
651
|
+
* and gains `--no-consent`, which skips only the process-5 branch. It once DROPPED the whole
|
|
652
|
+
* group instead, and Law IV went with it — on Claude only, since Bob and Copilot carry it
|
|
653
|
+
* through `tool-gate`. The command changing is also what re-wires an existing install:
|
|
654
|
+
* `mergeClaudeSettings` prunes every previously-managed group that is not in this return
|
|
655
|
+
* value.
|
|
651
656
|
*
|
|
652
657
|
* ⚠ WHAT THIS DOES **NOT** CLAIM, because it was written here once and it was false: that the
|
|
653
658
|
* string `process 5` is absent from a pack-off bundle. It is not — 18 emitted files still cite
|
|
@@ -695,14 +700,29 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [],
|
|
|
695
700
|
Stop: [{ hooks: [{ type: 'command', command: `${run} learn ${mem} || exit 0` }] }],
|
|
696
701
|
};
|
|
697
702
|
}
|
|
703
|
+
if (host === 'copilot') {
|
|
704
|
+
// COPILOT'S OWN SCHEMA (docs.github.com/en/copilot/reference/hooks-configuration): camelCase
|
|
705
|
+
// events, each an ARRAY of `{type: "command", command}` entries — no matcher groups, so the
|
|
706
|
+
// array elements ARE the groups this merge appends and prunes. `preToolUse` answers with
|
|
707
|
+
// a top-level `permissionDecision` (`--host copilot`), and `agentStop`/`preCompact` carry
|
|
708
|
+
// `transcriptPath`, so `learn` runs here as on Claude. No matcher, so `tool-gate` reads
|
|
709
|
+
// the payload's shape. Unverified live: no Copilot CLI on the authoring machine.
|
|
710
|
+
const c = ' --host copilot';
|
|
711
|
+
const entry = (command) => [{ type: 'command', command }];
|
|
712
|
+
return {
|
|
713
|
+
sessionStart: entry(`${run} context --root "${cfg}"${c} || exit 0`),
|
|
714
|
+
preToolUse: entry(`${run} tool-gate --root "${cfg}"${c}`),
|
|
715
|
+
agentStop: entry(`${run} learn ${mem} || exit 0`),
|
|
716
|
+
preCompact: entry(`${run} learn ${mem} || exit 0`),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
698
719
|
const context = `${run} context --root "${cfg}" || exit 0`;
|
|
699
|
-
const gate = `${run} git-gate --root "${cfg}"`;
|
|
720
|
+
const gate = `${run} git-gate --root "${cfg}"${consentRuleOn(doctrines, excluded) ? '' : ' --no-consent'}`;
|
|
700
721
|
const ruleGate = `${run} rule-gate --root "${cfg}"`;
|
|
701
722
|
const learn = `${run} learn ${mem} || exit 0`;
|
|
702
723
|
return {
|
|
703
724
|
PreToolUse: [
|
|
704
|
-
|
|
705
|
-
? [{ matcher: 'Bash', hooks: [{ type: 'command', command: gate }] }] : []),
|
|
725
|
+
{ matcher: 'Bash', hooks: [{ type: 'command', command: gate }] },
|
|
706
726
|
{
|
|
707
727
|
matcher: 'Write|Edit|MultiEdit|NotebookEdit',
|
|
708
728
|
hooks: [{ type: 'command', command: ruleGate }],
|
|
@@ -731,90 +751,11 @@ export function claudeHookGroups(cfg, hookOpts, doctrines = null, excluded = [],
|
|
|
731
751
|
}
|
|
732
752
|
|
|
733
753
|
/**
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
*
|
|
738
|
-
* nothing in `claudeHookGroups`/`mergeClaudeSettings` can be reused for it: there is no
|
|
739
|
-
* array to append to, and a second Geneseed group per event is not expressible. Two hooks:
|
|
740
|
-
*
|
|
741
|
-
* * `sessionStart` → `context --host copilot`: the same project-context injection Claude
|
|
742
|
-
* gets, in Copilot's `{"additionalContext"}` envelope.
|
|
743
|
-
* * `toolCall` → `tool-gate --host copilot`: both gates behind the one command the event
|
|
744
|
-
* allows, block-only for Laws I/IV, warn for the rest (js/hosts/hooks.mjs's header).
|
|
745
|
-
*
|
|
746
|
-
* No `sessionEnd`/`agentStop` → `learn`: Copilot's payloads for those carry no
|
|
747
|
-
* `transcript_path`, so `learn` would have nothing to distil. The memory write-back stays on
|
|
748
|
-
* the preamble for this host, as before.
|
|
749
|
-
*
|
|
750
|
-
* `|| exit 0` on the non-gate, as in `claudeHookGroups` and for the same reason.
|
|
751
|
-
*/
|
|
752
|
-
export function copilotHooks(cfg, hookOpts) {
|
|
753
|
-
const run = hookPrefix(hookOpts);
|
|
754
|
-
return {
|
|
755
|
-
sessionStart: { command: `${run} context --root "${cfg}" --host copilot || exit 0` },
|
|
756
|
-
toolCall: { command: `${run} tool-gate --root "${cfg}" --host copilot` },
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* Wire Copilot's hooks into `p` — returns the complete current claim set, `[{event, hook}]`.
|
|
762
|
-
*
|
|
763
|
-
* ONE SLOT PER EVENT MEANS A FOREIGN HOOK CANNOT BE JOINED, ONLY DISPLACED — and displacing
|
|
764
|
-
* a hook the user wrote is not Geneseed's to do. A slot is taken when it holds a hook that
|
|
765
|
-
* is neither canonical nor a claim recorded by a previous emit; that event is skipped with a
|
|
766
|
-
* warning and never claimed. A recorded stale claim (a moved shim) is replaced, as the Claude
|
|
767
|
-
* merge prunes stale groups. Comments in the file mean it is not rewritten, as everywhere.
|
|
754
|
+
* LEGACY: the single-slot Copilot shape (`hooks.<event>` = ONE object, events `sessionStart` /
|
|
755
|
+
* `toolCall`) an emit wrote before Copilot's hooks became event → array. Copilot has no
|
|
756
|
+
* `toolCall` event any more, so those gates never ran. Nothing writes this shape now; these
|
|
757
|
+
* two only take an older install's recorded `copilot_hooks` claims back out.
|
|
768
758
|
*/
|
|
769
|
-
export function mergeCopilotSettings(p, priorHooks = null, hookOpts = {}) {
|
|
770
|
-
const prior = (priorHooks || []).filter(isDict);
|
|
771
|
-
let config = {};
|
|
772
|
-
let hadComments = false;
|
|
773
|
-
if (existsSync(p)) {
|
|
774
|
-
try {
|
|
775
|
-
const [loaded, hc] = readJsonc(readText(p));
|
|
776
|
-
hadComments = hc;
|
|
777
|
-
if (isDict(loaded)) config = loaded;
|
|
778
|
-
} catch (e) {
|
|
779
|
-
if (!isOsError(e)) throw e;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
let hooks = get(config, 'hooks');
|
|
783
|
-
if (!isDict(hooks)) hooks = {};
|
|
784
|
-
const canonical = copilotHooks(path.dirname(p), hookOpts);
|
|
785
|
-
const claims = [];
|
|
786
|
-
let changed = false;
|
|
787
|
-
for (const [event, hook] of Object.entries(canonical)) {
|
|
788
|
-
const cur = get(hooks, event);
|
|
789
|
-
const recorded = prior.find((r) => get(r, 'event') === event);
|
|
790
|
-
if (isDict(cur) && deepEquals(cur, hook)) { claims.push({ event, hook }); continue; }
|
|
791
|
-
if (isDict(cur) && !(recorded && deepEquals(recorded.hook, cur))) {
|
|
792
|
-
process.stderr.write(`[geneseed] ${path.basename(p)}: hooks.${event} is already set and `
|
|
793
|
-
+ 'is not Geneseed\'s — left alone, so this event is not wired.\n');
|
|
794
|
-
continue;
|
|
795
|
-
}
|
|
796
|
-
hooks[event] = hook;
|
|
797
|
-
claims.push({ event, hook });
|
|
798
|
-
changed = true;
|
|
799
|
-
}
|
|
800
|
-
// A recorded claim whose event is no longer canonical: unwire it, or it fires forever.
|
|
801
|
-
for (const rec of prior) {
|
|
802
|
-
const event = get(rec, 'event');
|
|
803
|
-
if (event in canonical) continue;
|
|
804
|
-
if (deepEquals(get(hooks, event), get(rec, 'hook'))) { delete hooks[event]; changed = true; }
|
|
805
|
-
}
|
|
806
|
-
if (!changed) return [p, claims];
|
|
807
|
-
if (hadComments) {
|
|
808
|
-
process.stderr.write(`[geneseed] ${path.basename(p)} has comments — not rewriting it `
|
|
809
|
-
+ "(your edits are kept). Add Geneseed's hooks by hand from adapters/copilot/README.md.\n");
|
|
810
|
-
return [p, prior];
|
|
811
|
-
}
|
|
812
|
-
if (Object.keys(hooks).length) config.hooks = hooks;
|
|
813
|
-
else delete config.hooks;
|
|
814
|
-
atomicWriteJson(p, config);
|
|
815
|
-
return [p, claims];
|
|
816
|
-
}
|
|
817
|
-
|
|
818
759
|
/** Remove exactly the recorded Copilot hooks from `p`. True when the file was rewritten. */
|
|
819
760
|
export function unwireCopilotSettings(p, recorded) {
|
|
820
761
|
if (!existsSync(p) || !recorded || !recorded.length) return false;
|
|
@@ -876,9 +817,15 @@ export function copilotIntegrityCheck(p, recorded, expect = 'present') {
|
|
|
876
817
|
* `managed` is the complete current claim set, so unwire removes exactly those.
|
|
877
818
|
*
|
|
878
819
|
* `scope` is accepted and unused, exactly as in the Python.
|
|
820
|
+
*
|
|
821
|
+
* `cfgDir` is the install's own dir — what the hooks carry as `--root` and under which
|
|
822
|
+
* `memory/` lives. It is NOT always `dirname(p)`: Bob's global settings file is nested
|
|
823
|
+
* (`~/.bob/settings/settings.json`), and deriving the root from it pointed every global Bob
|
|
824
|
+
* hook at `~/.bob/settings` — memory learned where nothing reads it, excludes missed, and a
|
|
825
|
+
* global that never stood down for a project install.
|
|
879
826
|
*/
|
|
880
827
|
export function mergeClaudeSettings(p, _scope = 'global', priorHooks = null, hookOpts = {},
|
|
881
|
-
doctrines = null, excluded = [], host = 'claude') {
|
|
828
|
+
doctrines = null, excluded = [], host = 'claude', cfgDir = path.dirname(p)) {
|
|
882
829
|
const prior = (priorHooks || []).filter(isDict);
|
|
883
830
|
let config = {};
|
|
884
831
|
let hadComments = false;
|
|
@@ -898,7 +845,7 @@ export function mergeClaudeSettings(p, _scope = 'global', priorHooks = null, hoo
|
|
|
898
845
|
}
|
|
899
846
|
let hooks = get(config, 'hooks');
|
|
900
847
|
if (!isDict(hooks)) hooks = {};
|
|
901
|
-
const canonical = claudeHookGroups(
|
|
848
|
+
const canonical = claudeHookGroups(cfgDir, hookOpts, doctrines, excluded, host);
|
|
902
849
|
const canonFlat = [];
|
|
903
850
|
for (const [event, groups] of Object.entries(canonical)) {
|
|
904
851
|
for (const g of groups) canonFlat.push({ event, group: g });
|
|
@@ -1231,7 +1178,8 @@ export function settingsIntegrityCheck(p, managed, expect = 'present') {
|
|
|
1231
1178
|
for (const [event, group] of presentGroups) {
|
|
1232
1179
|
const key = `${formatRepr(event)}\u0000${jsonDumpsCompact(group, { sortKeys: true })}`;
|
|
1233
1180
|
if (recordedSet.has(key)) continue;
|
|
1234
|
-
|
|
1181
|
+
// A Claude group nests its commands under `hooks`; a Copilot entry IS the command.
|
|
1182
|
+
const cmds = [...(get(group, 'hooks') || []).filter(isDict), group]
|
|
1235
1183
|
.map((h) => (has(h, 'command') ? h.command : ''));
|
|
1236
1184
|
if (cmds.some((c) => typeof c === 'string'
|
|
1237
1185
|
&& GENESEED_HOOK_SNIFF.some((mk) => c.includes(mk)))) {
|
|
@@ -279,6 +279,91 @@ const DOCTRINE_CITE_RE = /\{\{DOCTRINE\}\}\s+([a-z]+)\s+(\d+)/g;
|
|
|
279
279
|
const DOCTRINE_TOKEN_RE = /\{\{DOCTRINE\}\}/;
|
|
280
280
|
const DOC_TOKEN_RE = /\{\{(DOC_[A-Z0-9_]+)\}\}/g;
|
|
281
281
|
|
|
282
|
+
/**
|
|
283
|
+
* The LEAN block grammar, and the heading split, both duplicated from `js/build/render.mjs`.
|
|
284
|
+
*
|
|
285
|
+
* Deliberately duplicated, the way `DOCTRINE_HEADING_RE` above is: both are module-private
|
|
286
|
+
* there, and exporting them so one gate could borrow them would widen the renderer's surface
|
|
287
|
+
* for a reader that only needs to know the shape. Two literals, one paragraph apart from the
|
|
288
|
+
* rule they encode, is cheaper than a seam.
|
|
289
|
+
*/
|
|
290
|
+
const LEAN_BLOCK_RE =
|
|
291
|
+
/[ \t]*<!-- LEAN:begin -->\n([\s\S]*?)[ \t]*<!-- LEAN:else -->\n([\s\S]*?)[ \t]*<!-- LEAN:end -->\n/g;
|
|
292
|
+
|
|
293
|
+
/** `### ` at line start — the same cut `splitAtLawHeadings` makes, and for the same addresses. */
|
|
294
|
+
function leanRuleBlocks(text) {
|
|
295
|
+
const out = [];
|
|
296
|
+
let start = 0;
|
|
297
|
+
for (let i = 0; i < text.length; i++) {
|
|
298
|
+
if ((i === 0 || text[i - 1] === '\n') && text.startsWith('### ', i)) {
|
|
299
|
+
out.push(text.slice(start, i));
|
|
300
|
+
start = i;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
out.push(text.slice(start));
|
|
304
|
+
return out.slice(1); // drop the lead chunk: the file's preamble
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Every authored rule carries exactly one LEAN block, and both halves say something.
|
|
309
|
+
*
|
|
310
|
+
* ⚠ `resolveLean` IS A REPLACE OVER A MARKER PAIR, WHICH MAKES IT A NO-OP ON MARKERLESS TEXT.
|
|
311
|
+
* A rule whose block was never written, or lost to an edit, therefore ships its FULL body at
|
|
312
|
+
* the default footprint — silently, in every install, with every other gate in the tree green.
|
|
313
|
+
* That is the failure this arm exists for, and before it there was no LEAN gate anywhere in
|
|
314
|
+
* `js/inspect/`: not for the doctrine packs, and not for the invariants either.
|
|
315
|
+
*
|
|
316
|
+
* The other two shapes are cheaper to state than to debug. An empty lean half renders a
|
|
317
|
+
* heading over nothing at the footprint most installs use. Two blocks in one rule are
|
|
318
|
+
* ambiguous by construction — `LEAN_BLOCK_RE` is global, so both would resolve, and which
|
|
319
|
+
* text the reader gets would be a fact about the order they were written in.
|
|
320
|
+
*
|
|
321
|
+
* The ontology is exempt on purpose: it carries ONE file-wide block whose halves each repeat
|
|
322
|
+
* the four `####` section headings, which is a different shape with its own gate in
|
|
323
|
+
* `tests/unit/emit_smoke.test.mjs` (four distinct names, eight occurrences).
|
|
324
|
+
*/
|
|
325
|
+
export function leanBlockProblems() {
|
|
326
|
+
const problems = [];
|
|
327
|
+
const files = [['laws', path.join(SRC, 'laws', 'universal.md')]];
|
|
328
|
+
for (const pack of PACK_ORDER) files.push(['doctrines', path.join(SRC, 'doctrines', `${pack}.md`)]);
|
|
329
|
+
|
|
330
|
+
for (const [dir, file] of files) {
|
|
331
|
+
const rel = `${dir}/${path.basename(file)}`;
|
|
332
|
+
let text;
|
|
333
|
+
try { text = readText(file); } catch (e) {
|
|
334
|
+
problems.push(`[authoring] ${rel} unreadable: ${e.message} — every rule in it would ship `
|
|
335
|
+
+ 'its full body at the lean footprint, unannounced');
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
for (const block of leanRuleBlocks(text)) {
|
|
339
|
+
// The address as the SOURCE spells it — `{{LAW}} II`, `{{DOCTRINE}} craft 1` — because
|
|
340
|
+
// that is the string an author greps for, and the rendered spelling is fourteen strings.
|
|
341
|
+
const heading = block.slice(0, block.indexOf('\n') === -1 ? undefined : block.indexOf('\n'));
|
|
342
|
+
const addr = heading.replace(/^###\s+/, '').split(/\s+[—-]\s+/)[0].trim();
|
|
343
|
+
const found = [...block.matchAll(LEAN_BLOCK_RE)];
|
|
344
|
+
if (found.length === 0) {
|
|
345
|
+
problems.push(`[authoring] ${rel}: ${addr} has no LEAN block — resolveLean is a no-op on `
|
|
346
|
+
+ 'markerless text, so the rule would inline its FULL body at the default footprint');
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (found.length > 1) {
|
|
350
|
+
problems.push(`[authoring] ${rel}: ${addr} has ${found.length} LEAN blocks — both resolve, `
|
|
351
|
+
+ 'so which text a reader is handed is a fact about the order they were written in');
|
|
352
|
+
}
|
|
353
|
+
const [, full, lean] = found[0];
|
|
354
|
+
if (!full.trim()) {
|
|
355
|
+
problems.push(`[authoring] ${rel}: ${addr} has an empty full half — the rule would vanish `
|
|
356
|
+
+ 'from a full build and from its own on-disk file');
|
|
357
|
+
}
|
|
358
|
+
if (!lean.trim()) {
|
|
359
|
+
problems.push(`[authoring] ${rel}: ${addr} has an empty lean half — the rule would render `
|
|
360
|
+
+ 'as a heading over nothing at the footprint most installs use');
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return problems;
|
|
365
|
+
}
|
|
366
|
+
|
|
282
367
|
/**
|
|
283
368
|
* The three-tier constitution's own authoring gates — the doctrine twin of the `LAW_CLASS` /
|
|
284
369
|
* `LAW_META` family, plus the two purity rules the tiering rests on.
|
|
@@ -292,7 +377,7 @@ const DOC_TOKEN_RE = /\{\{(DOC_[A-Z0-9_]+)\}\}/g;
|
|
|
292
377
|
* ⚠ THE VOCABULARY GATES ARE HERE BECAUSE `themeParityProblems` CANNOT SEE THEM. That check is
|
|
293
378
|
* presence-only and SYMMETRIC over the union of every theme's keys: a key missing from all
|
|
294
379
|
* fourteen is in parity, and a key whose VALUE breaks a parser is not its business at all. So
|
|
295
|
-
* `DOC_*` coverage, the `LEX_I..
|
|
380
|
+
* `DOC_*` coverage, the `LEX_I..LEX_XI` equality and the single-word tier noun each need a gate
|
|
296
381
|
* that reads the SOURCE and asks what the source requires.
|
|
297
382
|
*/
|
|
298
383
|
export function constitutionProblems() {
|
|
@@ -378,6 +463,18 @@ export function constitutionProblems() {
|
|
|
378
463
|
// The whole tree, not the template: a skill footer citing `process 9` is as broken as a
|
|
379
464
|
// template one, and it is the shape 339 rewired citations could have left behind.
|
|
380
465
|
const cited = new Map(); // 'process.5' -> [rel, …]
|
|
466
|
+
// ⚠ A RETIRED LAW KEEPS ITS NUMBER, SO A CITATION OF IT STILL RESOLVES — to "Retired". Laws
|
|
467
|
+
// are appended, never renumbered, which is what keeps ~150 cross-references stable; the price
|
|
468
|
+
// is that nothing else notices `{{LAW}} IX` in a skill that means "ask first" (it is rigor 5
|
|
469
|
+
// now) or `{{LAW}} XI` that means "verify" (folded into III). Read off the canon itself, so a
|
|
470
|
+
// law retired tomorrow is covered the same day.
|
|
471
|
+
const retired = new Set();
|
|
472
|
+
try {
|
|
473
|
+
for (const [, num] of readText(path.join(SRC, 'laws', 'universal.md'))
|
|
474
|
+
.matchAll(/^### \{\{LAW\}\} ([IVXLCDM]+) —[^\n]*\n<!-- LEAN:begin -->\nRetired\b/gm)) {
|
|
475
|
+
retired.add(num);
|
|
476
|
+
}
|
|
477
|
+
} catch { /* an unreadable canon is reported elsewhere */ }
|
|
381
478
|
for (const p of rglob(SRC)) {
|
|
382
479
|
if (!isFile(p) || !(p.endsWith('.md') || p.endsWith('.tmpl'))) continue;
|
|
383
480
|
let text;
|
|
@@ -392,6 +489,14 @@ export function constitutionProblems() {
|
|
|
392
489
|
+ 'reference a toggleable one, or a pack-off build ships a rule pointing at text '
|
|
393
490
|
+ 'that is not there. State the point directly instead');
|
|
394
491
|
}
|
|
492
|
+
if (rel !== 'laws/universal.md') {
|
|
493
|
+
for (const [, num] of text.matchAll(/\{\{LAW\}\}\s+([IVXLCDM]+)\b/g)) {
|
|
494
|
+
if (retired.has(num)) {
|
|
495
|
+
problems.push(`[authoring] ${rel} cites {{LAW}} ${num}, which is retired — cite the `
|
|
496
|
+
+ 'law or rule its text now lives in');
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
395
500
|
for (const [, pack, n] of text.matchAll(DOCTRINE_CITE_RE)) {
|
|
396
501
|
const key = `${pack}.${Number(n)}`;
|
|
397
502
|
if (!cited.has(key)) cited.set(key, []);
|
|
@@ -583,9 +688,17 @@ export function countTableProblems() {
|
|
|
583
688
|
}
|
|
584
689
|
problems.push(...lawMetaProblems(lawNums, LAW_CLASS, LAW_CLASSES));
|
|
585
690
|
|
|
691
|
+
// WHAT SHIPS, NOT WHAT IS FLAT. A folder skill (`src/skills/<name>/SKILL.md`, vendored with
|
|
692
|
+
// its scripts or references) ships exactly like a flat one, so the badge, the README list and
|
|
693
|
+
// the SHIPPED.md triple count it. It is added HERE rather than in `srcStems`, whose other
|
|
694
|
+
// readers above — the AGENT.md table links and SKILL_CLASS — are about flat specs only.
|
|
695
|
+
const skillsDir = path.join(SRC, 'skills');
|
|
696
|
+
const shippedSkills = new Set([...skillFiles, ...(isDir(skillsDir) ? readdirSync(skillsDir) : [])
|
|
697
|
+
.filter((d) => !d.startsWith('_') && isFile(path.join(skillsDir, d, 'SKILL.md')))]);
|
|
698
|
+
|
|
586
699
|
const counts = {
|
|
587
700
|
agents: srcStems('agents').size,
|
|
588
|
-
skills:
|
|
701
|
+
skills: shippedSkills.size,
|
|
589
702
|
laws: lawNums.length,
|
|
590
703
|
themes: themeFiles().length,
|
|
591
704
|
// Last on purpose: the badge loop below walks this object in insertion order and the
|
|
@@ -656,7 +769,7 @@ export function countTableProblems() {
|
|
|
656
769
|
problems.push('[authoring] SHIPPED.md carries no \'N laws, N agents, N skills\' line — '
|
|
657
770
|
+ 'deleting it is how that gate goes green while the count drifts');
|
|
658
771
|
}
|
|
659
|
-
problems.push(...proseMirrorProblems(readme, web, counts,
|
|
772
|
+
problems.push(...proseMirrorProblems(readme, web, counts, shippedSkills, shipped));
|
|
660
773
|
return problems;
|
|
661
774
|
}
|
|
662
775
|
|
|
@@ -756,6 +869,7 @@ export function authoringProblems() {
|
|
|
756
869
|
problems.push(...secretProblems());
|
|
757
870
|
problems.push(...vendorPinProblems());
|
|
758
871
|
problems.push(...constitutionProblems());
|
|
872
|
+
problems.push(...leanBlockProblems());
|
|
759
873
|
problems.push(...countTableProblems());
|
|
760
874
|
return problems;
|
|
761
875
|
}
|
package/js/inspect/diff.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync } fro
|
|
|
40
40
|
import os from 'node:os';
|
|
41
41
|
import path from 'node:path';
|
|
42
42
|
|
|
43
|
-
import { emitGlobalInto } from '
|
|
43
|
+
import { emitGlobalInto } from '../build/driver.mjs';
|
|
44
44
|
import { GLOBAL_MANIFEST, VERSION_MARKER, expanduser, opencodeConfigDir } from '../hosts/hosts.mjs';
|
|
45
45
|
import {
|
|
46
46
|
EMIT_HOST_SCOPE, defaultTheme, doctrinesForBuild, footprintOfDir, modeOfDir, postureOfDir,
|
package/js/inspect/doctor.mjs
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* keep the Doctor page's per-check cards a parameter rather than a rewrite.
|
|
31
31
|
*/
|
|
32
32
|
import path from 'node:path';
|
|
33
|
-
import { emitGlobalInto, emitProjectInto, main as driverMain } from '
|
|
33
|
+
import { emitGlobalInto, emitProjectInto, main as driverMain } from '../build/driver.mjs';
|
|
34
34
|
import { ROOT } from '../build/source.mjs';
|
|
35
35
|
import { resolvePath } from '../hosts/hosts.mjs';
|
|
36
36
|
import { installedDefaults, themeFiles } from '../hosts/installs.mjs';
|
package/js/inspect/excludes.mjs
CHANGED
|
@@ -244,7 +244,7 @@ export function cmdExclude(args) {
|
|
|
244
244
|
for (const rec of snap.excludes) {
|
|
245
245
|
const missing = [...allHosts].filter((h) => !rec.hosts.includes(h)).sort();
|
|
246
246
|
const flag = missing.length
|
|
247
|
-
? ` (MISSING from: ${missing.join(', ')} — re-run \`
|
|
247
|
+
? ` (MISSING from: ${missing.join(', ')} — re-run \`geneseed exclude add\`)` : '';
|
|
248
248
|
printOut(` ${rec.path} [${rec.hosts.slice().sort().join(', ')}]${flag}\n`);
|
|
249
249
|
}
|
|
250
250
|
return 0;
|
package/js/inspect/inventory.mjs
CHANGED
|
@@ -90,21 +90,26 @@ export const LAW_CLASSES = ['security', 'process', 'verify', 'craft', 'context',
|
|
|
90
90
|
|
|
91
91
|
/** `_harness_tui.SKILL_CLASS` — each skill's category, keyed by file stem. */
|
|
92
92
|
export const SKILL_CLASS = {
|
|
93
|
-
brainstorm: 'design',
|
|
93
|
+
brainstorm: 'design', plan: 'design', council: 'design',
|
|
94
94
|
workflow: 'design', 'parallel-agents': 'design', pipeline: 'design',
|
|
95
95
|
'codebase-design': 'design', 'domain-modeling': 'design', wayfinder: 'design',
|
|
96
96
|
tickets: 'design',
|
|
97
|
-
|
|
97
|
+
develop: 'build', refactor: 'build', debug: 'build', migrate: 'build',
|
|
98
98
|
'frontend-design': 'build', 'opencode-theme': 'build', prototype: 'build',
|
|
99
99
|
'forge-mcp': 'build', 'bruno-collection-generator': 'build', 'bruno-test-writer': 'build',
|
|
100
|
-
|
|
100
|
+
worktree: 'build', 'ci-fix': 'build', 'deps-audit': 'build',
|
|
101
|
+
'geneseed-code-review': 'review', 'fresh-eyes': 'review', 'security-audit': 'review',
|
|
101
102
|
'roast-me': 'review', 'review-response': 'review', ponytail: 'review',
|
|
102
103
|
commit: 'ship', ship: 'ship', release: 'ship', handoff: 'ship', 'git-rescue': 'ship',
|
|
103
|
-
'repo-map': 'understand', 'git-archaeology': 'understand',
|
|
104
|
+
'repo-map': 'understand', 'git-archaeology': 'understand',
|
|
104
105
|
research: 'understand', ingest: 'understand', 'document-project': 'understand',
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
prose: 'understand',
|
|
107
|
+
// `harness` — skills about the agent's own apparatus (its harness, memory, rules,
|
|
108
|
+
// knowledge base, terminal) rather than about the user's code. They sat under
|
|
109
|
+
// `understand` before, which made that class a grab-bag.
|
|
110
|
+
wiki: 'harness', geneseed: 'harness', rule: 'harness', profile: 'harness',
|
|
111
|
+
herdr: 'harness', 'skill-forge': 'harness', 'consolidate-memory': 'harness',
|
|
112
|
+
teach: 'learn', quiz: 'learn',
|
|
108
113
|
};
|
|
109
114
|
|
|
110
115
|
/** `k in dict` — own keys only, where `k in obj` also finds `Object.prototype` members. */
|
package/js/inspect/scan.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { SRC } from '../build/source.mjs';
|
|
14
|
-
import { comparePaths,
|
|
14
|
+
import { comparePaths, within } from '../lib/paths.mjs';
|
|
15
15
|
import { isDir, isFile } from '../lib/fs.mjs';
|
|
16
16
|
import { has } from '../lib/json.mjs';
|
|
17
17
|
import { mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
|
@@ -41,19 +41,8 @@ export function stripCode(text) {
|
|
|
41
41
|
return text.replace(FENCE_RE, '').replace(COMMENT_RE, '').replace(INLINE_CODE_RE, '');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
* Segment-wise and through `normcase`, because that is what `PurePath` comparison does: a
|
|
48
|
-
* bundle at `C:\Temp\X` contains `c:\temp\x\a.md` on Windows and does not on Linux. A
|
|
49
|
-
* `startsWith` on the raw strings would additionally call `/tmp/bundle2` a child of
|
|
50
|
-
* `/tmp/bundle`, which is the classic version of this bug.
|
|
51
|
-
*/
|
|
52
|
-
export function within(child, parent) {
|
|
53
|
-
const c = normcase(child).split(/[\\/]/);
|
|
54
|
-
const p = normcase(parent).split(/[\\/]/);
|
|
55
|
-
return p.length <= c.length && p.every((seg, i) => c[i] === seg);
|
|
56
|
-
}
|
|
44
|
+
// `within` — owned by js/lib/paths.mjs; re-exported for this module's importers.
|
|
45
|
+
export { within };
|
|
57
46
|
|
|
58
47
|
/** `sorted(d.glob(pat))`, or `[]` for a directory that is not there. */
|
|
59
48
|
export function globSorted(dir, filter) {
|
package/js/inspect/status.mjs
CHANGED
|
@@ -62,7 +62,7 @@ import { tuiInventory } from './inventory.mjs';
|
|
|
62
62
|
import { readVersion, sourceFingerprint } from '../build/version.mjs';
|
|
63
63
|
import {
|
|
64
64
|
claudeConfigDir, bobConfigDir, copilotConfigDir, opencodeConfigDir, resolvePath,
|
|
65
|
-
resolveMemoryDir, sovereignBypass,
|
|
65
|
+
resolveMemoryDir, sovereignBypass, GATE_LEDGER,
|
|
66
66
|
} from '../hosts/hosts.mjs';
|
|
67
67
|
// P5f moved the install DETECTORS out of this file — `diff` renders its expected copy in the
|
|
68
68
|
// deployed theme and footprint, and `rebuild-all` re-emits in the deployed everything, so
|
|
@@ -194,7 +194,7 @@ export function gateSummary(cfgDirs) {
|
|
|
194
194
|
if (sovereignBypass(cfg)) standingDown.push(String(cfg));
|
|
195
195
|
let raw;
|
|
196
196
|
try {
|
|
197
|
-
raw = readFileSync(path.join(cfg,
|
|
197
|
+
raw = readFileSync(path.join(cfg, GATE_LEDGER), 'utf8');
|
|
198
198
|
} catch {
|
|
199
199
|
continue;
|
|
200
200
|
}
|
package/js/inspect/validate.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* reached — see its row in `tests/ported.json`.
|
|
8
8
|
*/
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import { buildInto, emitGlobalInto, emitProjectInto } from '
|
|
10
|
+
import { buildInto, emitGlobalInto, emitProjectInto } from '../build/driver.mjs';
|
|
11
11
|
import { resolvePath } from '../hosts/hosts.mjs';
|
|
12
12
|
import { validateIsVendored } from '../hosts/native.mjs';
|
|
13
13
|
import { printOut, printErr, readText } from '../lib/fs.mjs';
|
package/js/lib/fs.mjs
CHANGED
|
@@ -202,19 +202,34 @@ export function withPlatformNewlines(fn) {
|
|
|
202
202
|
* it for its own existing callers rather than owning a copy itself. `js/hosts/hooks.mjs` is
|
|
203
203
|
* the deliberate exception — the hot path keeps its own inline copy rather than pay an import.
|
|
204
204
|
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
205
|
+
* WHICH ERRORS MEAN "NO", matching `pathlib`'s `_ignore_error`: `ENOENT` (nothing there),
|
|
206
|
+
* `ENOTDIR` (a path THROUGH a file — `skills/brainstorm.md/SKILL.md`), `ELOOP`, `EBADF`. Each is a
|
|
207
|
+
* definite answer that nothing of that kind is at the path. Anything else — a permission
|
|
208
|
+
* failure above all — still throws: read as "not there" it would be the wrong answer, silently.
|
|
209
|
+
* Every copy this replaced used a bare `try/catch` that swallowed everything alike.
|
|
210
|
+
*
|
|
211
|
+
* ⚠ `ENOTDIR` WAS ONCE LEFT TO THROW here, on the reading that it resembled an access error.
|
|
212
|
+
* It is not one, and Windows reports the same probe as `ENOENT` — so the difference only shows
|
|
213
|
+
* on macOS/Linux: the folder-skill count asked `isFile(<skills>/<flat>.md/SKILL.md)` and the
|
|
214
|
+
* web console's overview died with `ENOTDIR` there while every Windows run stayed green.
|
|
211
215
|
*/
|
|
216
|
+
const NOT_THERE = new Set(['ENOENT', 'ENOTDIR', 'ELOOP', 'EBADF']);
|
|
217
|
+
|
|
218
|
+
function statOrNull(p) {
|
|
219
|
+
try {
|
|
220
|
+
return statSync(p);
|
|
221
|
+
} catch (e) {
|
|
222
|
+
if (e && NOT_THERE.has(e.code)) return null;
|
|
223
|
+
throw e;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
212
227
|
export function isFile(p) {
|
|
213
|
-
return
|
|
228
|
+
return statOrNull(p)?.isFile() ?? false;
|
|
214
229
|
}
|
|
215
230
|
|
|
216
231
|
export function isDir(p) {
|
|
217
|
-
return
|
|
232
|
+
return statOrNull(p)?.isDirectory() ?? false;
|
|
218
233
|
}
|
|
219
234
|
|
|
220
235
|
/**
|
package/js/lib/json.mjs
CHANGED
|
@@ -248,8 +248,14 @@ export function has(obj, key) {
|
|
|
248
248
|
return Object.hasOwn(obj, key);
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/**
|
|
252
|
+
* `isinstance(x, dict)` for a value that came out of `parseJson`. Numbers come back WRAPPED
|
|
253
|
+
* (`JsonNumber`), so `typeof v === 'object'` alone says yes to every number — and a settings
|
|
254
|
+
* file with `"permission": 5` then read as a dict the merge could write into, silently
|
|
255
|
+
* skipping the git gate. The one exclusion is the wrapper; everything else is unchanged.
|
|
256
|
+
*/
|
|
251
257
|
export function isDict(v) {
|
|
252
|
-
return Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
258
|
+
return Boolean(v) && typeof v === 'object' && !Array.isArray(v) && !(v instanceof JsonNumber);
|
|
253
259
|
}
|
|
254
260
|
|
|
255
261
|
/**
|
package/js/lib/proc.mjs
CHANGED
|
@@ -20,9 +20,11 @@
|
|
|
20
20
|
* CAPTURING SPAWNS ONLY, and the asymmetry is deliberate. The rule was measured on Windows:
|
|
21
21
|
* a process that sets `STARTF_USESTDHANDLES` only when a stream is redirected gives an
|
|
22
22
|
* INHERITING child spawned with `CREATE_NO_WINDOW` a fresh hidden console and discards
|
|
23
|
-
* everything it prints — `geneseed build > log.txt` wrote an empty file.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* everything it prints — `geneseed build > log.txt` wrote an empty file. That was Python's
|
|
24
|
+
* `CREATE_NO_WINDOW` creation flag. Node's `windowsHide` is a different mechanism, and libuv
|
|
25
|
+
* always passes the handles explicitly, so on an INHERITING spawn it is harmless: the child
|
|
26
|
+
* writes to the parent's handles. A few inheriting sites set it (`js/hosts/link.mjs`,
|
|
27
|
+
* `js/maintain/update.mjs`) and are correct — `tests/unit/spawn_hygiene.test.mjs` explains why
|
|
28
|
+
* it asserts no inverse rule.
|
|
27
29
|
*/
|
|
28
30
|
export const NO_WINDOW = { windowsHide: process.platform === 'win32' };
|
package/js/maintain/migrate.mjs
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
35
35
|
import path from 'node:path';
|
|
36
36
|
|
|
37
|
-
import { main as driverMain } from '
|
|
37
|
+
import { main as driverMain } from '../build/driver.mjs';
|
|
38
38
|
import { ROOT } from '../build/source.mjs';
|
|
39
39
|
import {
|
|
40
40
|
EMIT_HOST_SCOPE, defaultMode, defaultPosture, defaultTheme, doctrinesForBuild,
|
package/js/maintain/setup.mjs
CHANGED
|
@@ -44,7 +44,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
44
44
|
import { existsSync, readSync } from 'node:fs';
|
|
45
45
|
import path from 'node:path';
|
|
46
46
|
|
|
47
|
-
import { main as driverMain, resolveOut } from '
|
|
47
|
+
import { main as driverMain, resolveOut } from '../build/driver.mjs';
|
|
48
48
|
import { playLine } from '../ui/anim.mjs';
|
|
49
49
|
import { discoverNames, PACK_ORDER } from '../build/source.mjs';
|
|
50
50
|
import { exportImprovements } from '../inspect/diff.mjs';
|