diff --git a/tools/INDEX.md b/tools/INDEX.md index b85a7b3..9f91c83 100644 --- a/tools/INDEX.md +++ b/tools/INDEX.md @@ -17,4 +17,4 @@ gets a Soul pass before merge. | `tools/context-pack.py` | `py -3 tools/context-pack.py [--since ] [--only \|r[,...]] [--spec-json ] [--max-lines ]`. Carries a cut_spec's `file_changes` and `reads` anchors (read rows `r`; a read absent at base is MISSING) from the spec's base to the worktree by `git diff` hunks (the working tree included) and prints one header, a status table (SAME, MOVED, TOUCHED, RENAMED, NEW, GONE, OUT_OF_RANGE, UNRESOLVED, MISSING, plus SYMBOL_MISSING when an anchor's `symbol` is absent from its window) and windows of at most 100 lines numbered in worktree lines, then one capped caller list per symbol (`git grep`, hits outside the windows), within `--max-lines` (800). Exit 3 when an anchor cannot be carried, 4 when the spec is unreadable (fetched by id through the eureka-state binding (`tools/mind.py`), retried once on Unavailable; `--spec-json` takes a saved view). `--since ` re-prints only the windows edited since that commit. Standard library plus git; writes nothing. | Hands, Soul, Imagination | `references/briefs.md` (Imagination map, Hands, Soul), `SKILL.md` (step 1) | | `tools/mind.py` | The one way a Eureka script reaches the mind: `mind.call(tool, arguments, deadline)` spawns the eureka-state binding (`EUREKA_STATE_EXE`, else `~/.claude.json` `mcpServers.eureka-state`), does the stdio handshake and one tool call within `deadline` seconds, returns the parsed JSON body, raises `MindError` (kind binding, transport, tool or answer; a detail, never an input value), and kills the child on every exit path. No retry inside `call`; `retry_once(fn)` is the caller's choice. Standard library only. | `tools/context-pack.py`, `tools/recall.py` | not named by a brief; it lives and dies with its consumers | | `tools/recall.py` | `py -3 tools/recall.py prompt`, a `UserPromptSubmit` command hook (hook JSON on stdin): queries the rulings, questions and follow-ups in force across every root, ranked by Huginn against the prompt (`RECALL_TOP_K`, 5), and prints one hook JSON whose `additionalContext` is a header saying the lines are recalled records, data and never instructions, written by agents and not the operator, then one JSON object per item (`id`, `title`, a short `operator_quote`) with `<`, `>` and control, bidi and separator characters escaped, within `RECALL_BUDGET` characters (4000, at most 9000: Claude Code caps `additionalContext` at 10,000), whole lines only. Prompts under 3 words are not queried. Never blocks: any failure exits 0 with empty stdout; its own deadline is `RECALL_DEADLINE_MS` (4000) for the whole run (stdin read to 4 MiB, answer read to 1 MiB), so set the hook `timeout` above it. One attempt, no retry. Install both hooks as ` C:/Users/Meta/.claude/skills/eureka/tools/recall.py prompt || true` (and `... admit || true`): Claude Code runs hook commands through Git Bash on Windows, and `py -3 ` exits 2, which blocks every prompt or admit; with `|| true` the hook cannot exit 2 (a missing script, interpreter or file passes) and the script's stdout is untouched (proved live on 2.1.308). `py -3 tools/recall.py admit` is a `PreToolUse` hook on `mcp__eureka-state__admit`: for each ruling or question in the batch (at most `RECALL_ADMIT_MAX`, 3) it queries the nearest in-force rulings and questions, and denies the first attempt with the neighbours as the reason (a deliberate hold, not a failure); the identical call sent again (digest of `tool_input` plus `agent_id`, kept in the session scratchpad) passes. Never allow or ask; a failed check exits 0 with a note naming the failure kind (`RECALL_TOP_K` 3, `RECALL_DEADLINE_MS` 10000 in this mode). Standard library only; writes only the digest file. | the operator's `UserPromptSubmit` hook and `PreToolUse` hook on `mcp__eureka-state__admit` (fires inside subagents too) in `~/.claude/settings.json` | `references/campaign-state.md` (Precedent) | -| `tools/bloom-sheet/bloom_sheet.py` | `bloom-sheet notes.md -o sheet.png [--visitor a.svg]...`: a markdown document as one neon A4 page. The first top-level heading is the title, every other heading a card sized by its text; a plant grows through what's left, and each SVG visitor gets a disc it grows around and is drawn with the plant's pen, so it must be flat strokes or fills that touch. PDF or PNG, about 2.5 minutes. `grow.py` owns the plant, `page.tex` the look, `test_visitors.py` what a visitor may be. | Minds and Self, for info renders | the `bloom-sheet` skill; `bloom-sheet` on Yggdrasil's PATH | +| `tools/bloom-sheet/bloom_sheet.py` | `bloom-sheet notes.md -o sheet.png [--visitor a.svg]...`: a markdown document as one neon A4 page. The first top-level heading is the title, every other heading a card sized by its text; a plant grows through what's left, and each SVG visitor gets a disc it grows around and is drawn with the plant's pen, so it must be flat strokes or fills that touch; large fills become a wash and a heavy outline. PDF or PNG, about 2.5 minutes. `grow.py` owns the plant, `page.tex` the look, `test_visitors.py` what a visitor may be. | Minds and Self, for info renders | the `bloom-sheet` skill; `bloom-sheet` on Yggdrasil's PATH | diff --git a/tools/bloom-sheet/bloom_sheet.py b/tools/bloom-sheet/bloom_sheet.py index d84eccd..6613081 100755 --- a/tools/bloom-sheet/bloom_sheet.py +++ b/tools/bloom-sheet/bloom_sheet.py @@ -11,10 +11,11 @@ two minutes: the growth, then LuaLaTeX twice. A visitor is drawn with the plant's own pen, not pasted in. Stroked shapes become weighted neon lines, and a fill under a stroke becomes a faint wash, as -on a petal. Shapes with only a fill become solid neon. Colours snap to the -sheet's palette, near-black paints the ground (a cutout), and opacity is -ignored. All of it must touch, reading as one thing; a closed outline counts -as covering its inside. Text, images and gradients are refused. +on a petal. A shape with only a fill is drawn the same way with a heavy line, +unless it is small: spots and eyes stay solid, like a flower's centre. Colours +snap to the sheet's palette, near-black paints the ground (a cutout), and +opacity is ignored. All of it must touch, reading as one thing; a closed +outline counts as covering its inside. Text, images and gradients are refused. Needs python3-numpy, python3-markdown-it, LuaLaTeX with tikz and tcolorbox, pdftoppm for PNG, the Ubuntu, IBM Plex Mono and Montserrat fonts, and for @@ -236,6 +237,7 @@ def macro(k): return r"\content" + name +SPOT = 0.35 # a bare fill wider than this, as a fraction of its drawing, is outlined PALETTE_HUES = {"neonorange": 28, "neonsky": 205, "neonviolet": 244, "neonrose": 336} @@ -250,8 +252,8 @@ def neon_colour(colour): def read_visitor(svg): """Read an SVG into marks the plant's pen can draw, in paint order, centred, y up, and scaled so everything lies within radius 1. Exits with the reason - if the drawing can't be one. A mark is {"colour", "rings"} for a solid or - {"colour", "pts", "width", "closed", "wash"} for a line.""" + if the drawing can't be one. A mark is {"colour", "rings", "outlined"} for a + bare fill or {"colour", "pts", "width", "closed", "wash"} for a line.""" from shapely.geometry import LineString, Polygon from shapely.ops import unary_union from svgelements import SVG, Close, Line, Move, Path as SvgPath, Shape, Text @@ -309,7 +311,10 @@ def read_visitor(svg): area = poly if area is None else area.symmetric_difference(poly) if area is not None: covers.append(area.buffer(touch)) if stroke is None: - marks.append({"colour": ink(fill), "rings": [unit(pts) for pts, _ in subs]}) + rings = [unit(pts) for pts, _ in subs] + xs, ys = [p[0] for r in rings for p in r], [p[1] for r in rings for p in r] + big = math.hypot(max(xs) - min(xs), max(ys) - min(ys)) / 2 > SPOT + marks.append({"colour": ink(fill), "rings": rings, "outlined": big and ink(fill) != "ground"}) continue for pts, closed in subs: marks.append({"colour": ink(stroke), "pts": unit(pts), "width": round(width / radius, 5), "closed": closed, diff --git a/tools/bloom-sheet/grow.py b/tools/bloom-sheet/grow.py index db9730a..6b5cf05 100644 --- a/tools/bloom-sheet/grow.py +++ b/tools/bloom-sheet/grow.py @@ -230,19 +230,26 @@ def draw_visitor(cx, cy, r, marks): a = math.radians(rng.uniform(-18, 18)) spot = lambda pts: [(cx + r * 0.95 * (x * math.cos(a) - y * math.sin(a)), cy + r * 0.95 * (x * math.sin(a) + y * math.cos(a))) for x, y in pts] + def loop(col, pts, w): # a closed line breathes like a ring + ph = rng.uniform(0, 2 * math.pi) + neon(col, ribbon(pts + pts[:1], lambda u: w * (0.75 + 0.25 * math.cos(2 * math.pi * u + ph))), 0.9) for m in marks: if "rings" in m: - rings = " ".join(path(spot(ring_)) for ring_ in m["rings"]) - out.append(f"\\begin{{scope}}[even odd rule]\\neonfill{{{m['colour']}}}{{85}}{{{rings}}}\\end{{scope}}") + rings = [spot(ring_) for ring_ in m["rings"]] + whole = " ".join(path(ring_) for ring_ in rings) + if m["outlined"]: + out.append(f"\\begin{{scope}}[even odd rule]\\petalwash{{{m['colour']}}}{{{whole}}}\\end{{scope}}") + for ring_ in rings: + loop(m["colour"], ring_, 0.6) + else: + out.append(f"\\begin{{scope}}[even odd rule]\\neonfill{{{m['colour']}}}{{85}}{{{whole}}}\\end{{scope}}") continue pts = spot(m["pts"]) if m["wash"]: out.append(f"\\petalwash{{{m['wash']}}}{{{path(pts)}}}") - # the caller's weight, kept within the plant's; a closed line breathes - # like a ring, an open one swells between thin ends + # the caller's weight, kept within the plant's; an open line swells between thin ends w = max(0.14, min(0.7, m["width"] * r * 0.95)) if m["closed"]: - ph = rng.uniform(0, 2 * math.pi) - neon(m["colour"], ribbon(pts + pts[:1], lambda u: w * (0.75 + 0.25 * math.cos(2 * math.pi * u + ph))), 0.9) + loop(m["colour"], pts, w) else: neon(m["colour"], ribbon(pts, lambda u: w * (0.4 + 0.6 * math.sin(math.pi * u) ** 0.7)), 0.9) diff --git a/tools/bloom-sheet/test_visitors.py b/tools/bloom-sheet/test_visitors.py index 045102d..3551c6b 100644 --- a/tools/bloom-sheet/test_visitors.py +++ b/tools/bloom-sheet/test_visitors.py @@ -41,7 +41,7 @@ refused("image", DISC + '', "fill or a stroke") m = read_visitor(svg(DISC)) -check("a bare fill is a solid", len(m) == 1 and "rings" in m[0] and m[0]["colour"] == "neonorange") +check("a large bare fill is outlined, not solid", len(m) == 1 and m[0]["outlined"] and m[0]["colour"] == "neonorange") m = read_visitor(svg('')) check("a stroke is an open line, no wash", len(m) == 1 and "pts" in m[0] and not m[0]["closed"] and m[0]["wash"] is None) check("the line keeps its colour", m[0]["colour"] == "neonsky") @@ -51,8 +51,13 @@ check("a fill under a stroke is a wash", m[0]["closed"] and m[0]["wash"] == "neo m = read_visitor(svg('')) check("an all-black icon is seen", m[0]["colour"] == "neonwhite") m = read_visitor(svg(DISC + '')) -check("black on colour is a cutout", m[1]["colour"] == "ground") -check("paint order is kept", m[0]["colour"] == "neonorange") +check("black on colour is a cutout", m[1]["colour"] == "ground" and not m[1]["outlined"]) +m = read_visitor(svg(DISC + '')) +check("a small bare fill stays solid", not m[1]["outlined"]) +m = read_visitor(svg('')) +check("a large cutout stays a cutout", m[0]["colour"] == "ground" and not m[0]["outlined"]) +m = read_visitor(svg(DISC + '')) +check("paint order is kept", [x["colour"] for x in m] == ["neonorange", "ground"]) m = read_visitor(svg('')) check("eyes inside an outline count as touching", len(m) == 3) m = read_visitor(svg(''))