fix(init): make --hooks idempotently merge into existing repos#1
Merged
Conversation
beagle init --hooks was a no-op for already-initialized projects: it wrote .claude/settings.json only when absent (so an existing settings.json never got the PostToolUse repair hook) and dropped stray starter files (src/main, CLAUDE.md, flake.nix, .envrc) into populated repos. Now it detects an existing project and, in that case, ONLY wires the hook — merging the PostToolUse entry into any existing settings.json (preserving other keys, idempotent on re-run) and skipping all starter-file scaffolding. A fresh/ empty dir still gets the full scaffold + hooks. Makes 'beagle init --hooks' the one-command sync it was meant to be. Matcher widened to Edit|Write|MultiEdit.
tompassarelli
added a commit
that referenced
this pull request
Jun 20, 2026
* docs: codegen/check findings from gjoa dogfooding (8 items, minimal repros) A companion ledger to text-as-source-latent-bugs.md, capturing codegen/emit/lint findings surfaced by authoring gjoa with beagle. #2 (unary minus) is already fixed; #1 (statement-position lowering) is flagged as an invasive, deliberate change — NOT a dogfooding tangent. The rest are filed with minimal repros while fresh. * docs(findings): add #9 — keyword map-key underscore doubled vs property read A keyword map-literal key with '_' lowers to a doubled underscore ({:a_b 1} -> {a__b: 1}) while the matching property read (.-a_b) stays single (m.a_b), so writes and reads disagree and the round-trip silently returns undefined. Surfaced building the gjoa audit ledger; worked around with camelCase keys. * docs(findings): add #10 — emitted import specifier vs snake_cased filename mismatch A hyphenated module (drag-overlay.bjs -> drag_overlay.js) is imported by a sibling with the original hyphen (from './drag-overlay.js'), which doesn't match the emitted filename — broken under plain ESM. Same root family as #9 (divergent name normalization across positions). Surfaced resolving the gjoa audit-ledger dependency graph.
tompassarelli
added a commit
that referenced
this pull request
Jun 21, 2026
…scape-across-fn) Closes soundness hole-class #3 (escape across the fn boundary) from beagle-2's adversarial boundary map: a fn RETURNING a compound-keyed map (or a `(set var)` over a compound-typed param) was read NATIVE by the caller because rep-selection was intraprocedural + per-node-table only (bare var-refs and call results carry no rep). Two additions to classify-rep: - TYPE-DIRECTED dispatch: an expression whose node-type is a concrete compound-keyed Map / compound-elem Set classifies hmap/hset directly. This catches annotated fn returns (`build-index :- (Map (Vec Int) Keyword)`) and any typed expr — the caller's get/contains/count now route to HAMT ops. - BINDING-TYPE ENV (current-type-env): a var-ref resolves to its declared type (param `:-` annotation / let value type) via arg-type, so `(set paths)` with paths :- (Vec (Vec Int)) sees the compound element and routes to hamtSet. Seeded at defn/fn/arity params (with-param-envs) + composes with rep-env. - rep-env now also seeded from param types (a param :- (Map compound ..) reads as hmap inside the body). Real-program evidence: bjs-bench/programs/structdiff.bjs (pure-value diff/merge = Eddy skip-if-unchanged at lib scale) goes 5/7 -> 7/7 — distinct-paths (value dedup over a compound param) and cross-fn compound-map lookup both flip GREEN. No regression: active 1474/1474, gated JS (emit-js 147, fixtures, jst, exec-oracle) + behavioral 78 all green. Still provably-compound predicate; the Any/records/unions flip (#1,#2,#5,#6) is the next commit.
tompassarelli
added a commit
that referenced
this pull request
Jun 21, 2026
…phic reads Closes soundness hole-classes #1 (records-as-keys), #2 (union keys), #5 (heterogeneous-key literals), #6 (Any-typed compound keys) from beagle-2's adversarial boundary map. The classifier predicate flips from "provably-COMPOUND → HAMT" to "provably-SCALAR → native, else → HAMT": key-class(t): scalar-eq-safe -> 'native ; Map/Set/Vec/List ctor OR a record type -> 'hamt (object-emitting, ; no scalar subtype: a value of this type is ALWAYS a HAMT) ; Any | type-var | union | Float | Nil | unknown -> 'poly PRODUCTION: a map/set whose key/elem is not provably-scalar produces a HAMT (incl records, unions, Any, heterogeneous K=Any). Map LITERALS classify by their actual KEY DATA per-pair (robust to an uncaptured node-type — e.g. a map built inside a `.map` arrow); scalar literal keys (incl quoted `':k`) resolve to their scalar type via arg-type, so they stay native (no over-promotion). READS — 3-way, because Any is a BIDIRECTIONAL wildcard (a native scalar map <: (Map Any V)), so a native value can flow into an Any/union-typed read: - provably-compound/record coll -> hamtMapGet/Has/Count (monomorphic, O(log n)) - scalar coll -> native - poly (Any/union) coll -> $$bc.get/contains/count/keys/vals (polymorphic over native + HAMT; beagle-2's @26df650). count/keys/vals/get gain a 'poly branch. Adversarial-verified (vs the Clojure shape): record keys (distinct lookup + absent-miss), Any-keyed assoc-chain (put2 -> "x" not "y"), heterogeneous-key count (3 not 2) all flip RED->GREEN. rep-soundness gate +4 flip cases (records / Any-key / heterogeneous / Any-typed poly read) = 18/18. Active 1478/1478, gated JS (emit-js 147, exec-oracle 7, ...) + behavioral 78 all green. DEFERRED (rarer, documented): poly-PRODUCTION ops (assoc/conj/dissoc on an Any-typed COLL) need a $$bc COW producer; the Any->concrete-typed-param coercion boundary (gradual-typing blame point).
tompassarelli
added a commit
that referenced
this pull request
Jun 21, 2026
…HAMT Closes the last soundness hole-class (#4 runtime dedup) from beagle-2's boundary map. The runtime collection builders no longer emit native ARRAYS for sets (a pre-existing set-builder bug) and route to value-dedup HAMT when the element type is compound: coll-kind helper: 'set | 'vec | 'map | 'unknown, seeing through conj/into/disj to the underlying collection (so a native Set/conj-result isn't mistaken for an array). conj : value-set -> hamtSetAdd fold ; native set -> new Set([...c, x]) (a Set, not [...c,x]) ; vec -> [...c,x]. into : value-set -> xs.reduce(hamtSetAdd, target) ; native set -> new Set([... target, ...xs]) ; vec -> spread. count : native branch now dispatches on coll-kind -> set:.size / map:Object.keys ().length / else:.length, so (count (conj #{..} x)) is .size not .length (was undefined). frequencies : compound elements -> value-keyed hamtMap (was native object -> "[object Object]" collisions). group-by left native (its key type comes from f's return, not reliably inferable — documented residual). Verified vs Clojure shape: conj/into on scalar+compound sets (3/1/4/2), frequencies compound/scalar (2/2). rep-soundness +4 = 24/24; active 1484/1484, gated JS (emit-js 147, exec-oracle 7, ...) + behavioral 78 green. ALL 6 hole-classes now closed: #1 records, #2 unions, #3 escape-across-fn, #4 runtime dedup, #5 heterogeneous, #6 cross-rep equality.
tompassarelli
added a commit
that referenced
this pull request
Jun 22, 2026
… build) P0 follow-up (fram-2 #18). parse.rkt has its OWN beagle-readtable — it drives read-beagle-syntax (check --agent, build, the repair loop + PostToolUse hook) — SEPARATE from reader-impl.rkt's readtable (#lang module loading). bc53027 added the `^` metadata reader macro only to reader-impl's, so read-beagle-syntax read `^:dynamic` as a bare symbol → "malformed def" on the first dynamic var, and the agent path STOPPED there (real type-errors after it skipped) — the repair loop + hook went blind on every dynamic-var file (resolve.clj + the daemon are dynamic-var-heavy). One missing macro caused both reported issues: #1 check --agent / build / repair-loop blind on dynamic-var files #2 "typed dynamic def rejected" — never a grammar gap; the read path just never produced #%meta, so (def ^:dynamic NAME :- TYPE VAL) misparsed too. Fix: mirror the `^` macro into parse.rkt's readtable (meta-reader-local), with a comment flagging the two-readtable duplication so future reader features stay in sync. Regression test drives read-beagle-syntax directly with a typed ^:dynamic def. Active 1500/1500 + gated 437/437, all targets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
beagle init --hooks— the intended one-command "sync this repo to the repair-compiler hook" — silently no-ops on already-initialized projects:.claude/settings.jsononly when absent, so any repo that already had asettings.jsonnever got thePostToolUserepair hook wired in.src/main.*,CLAUDE.md,flake.nix,.envrc) into populated repos.Net effect: on an existing repo you'd run the command, get a stray
src/main.bclj, and still not have the hook — forcing manual JSON editing, which is exactly what the toolchain is supposed to avoid.Fix
CLAUDE.md/.claude/settings.json/.git/package.json/flake.nix/existing*.b*sources). In that case, only wire the hook — skip all starter-file scaffolding.PostToolUseentry into any existingsettings.json(preserving other keys), and no-op cleanly if already wired.Edit|Write|MultiEdit.Verified
--hooks→ full scaffold + hook.settings.json(other keys) +--hooks→ hook merged in,permissionspreserved, no stray files.already wired (no change)), entry count stays 1..bjs.