From 0237ee4310b20f444357031ce6a879c847a3749d Mon Sep 17 00:00:00 2001 From: Kaden McKeen Date: Wed, 20 May 2026 22:37:09 -0400 Subject: [PATCH 01/10] =?UTF-8?q?refactor(debrand):=20stage=20A=20?= =?UTF-8?q?=E2=80=94=20de-brand=20the=20vendored=20tree,=20drop=20the=20st?= =?UTF-8?q?ale=20skill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit De-brand the vendored research-harness tree so it reads as plain aexp functionality rather than a named "Limina" centerpiece: - AGENTS.md / CLAUDE.md headers -> "Agentic Experiments — ...". - kb/DASHBOARD.md -> "Research Dashboard". - The 4 research-methodology skills: drop "Limina" from descriptions and prose; the skill logic itself is unchanged. - VENDORED_FROM.txt slimmed to a concise provenance note (keeps the KadenMc/limina credit; drops the dead reference/limina/ pointer). Delete the stale top-level "limina" skill (vendor/limina/skill/): its body still told the agent to `git clone github.com/theam/limina` and `pip install -r requirements.txt`, neither of which is how aexp works — `aexp install` already scaffolds a project. Remove the singular-skill copy path and `_SKILL_TOPLEVEL_NAME` from install.py; update the two test_install.py skill tests (4 research skills, no top-level skill). The directory src/aexp/vendor/limina/ keeps its name — vendor// is honest provenance for a vendored snapshot. Part of the limina de-brand (one PR, four staged commits A-D). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/aexp/install.py | 38 +---- src/aexp/vendor/limina/AGENTS.md | 6 +- src/aexp/vendor/limina/CLAUDE.md | 2 +- src/aexp/vendor/limina/VENDORED_FROM.txt | 36 ++--- src/aexp/vendor/limina/kb/DASHBOARD.md | 2 +- src/aexp/vendor/limina/skill/SKILL.md | 138 ------------------ .../build-maintainable-software/SKILL.md | 6 +- .../limina/skills/experiment-rigor/SKILL.md | 4 +- .../experiment-rigor/agents/openai.yaml | 4 +- .../evals/trigger-prompts.csv | 2 +- .../references/metrics-storage.md | 4 +- .../skills/exploratory-sota-research/SKILL.md | 14 +- .../references/output-template.md | 4 +- .../worked-example-information-retrieval.md | 2 +- .../skills/research-devil-advocate/SKILL.md | 10 +- .../agents/openai.yaml | 4 +- .../evals/trigger-prompts.csv | 2 +- tests/test_install.py | 12 +- 18 files changed, 57 insertions(+), 233 deletions(-) delete mode 100644 src/aexp/vendor/limina/skill/SKILL.md diff --git a/src/aexp/install.py b/src/aexp/install.py index 8218ed0..aa3502a 100644 --- a/src/aexp/install.py +++ b/src/aexp/install.py @@ -107,16 +107,6 @@ def _find_aexp_source_tree(start: Path) -> Path | None: _BEGIN_MARKER = "" _END_MARKER = "" -# Limina's Claude Code skills get copied into /.claude/skills// so -# they travel with the repo. AGENTS.md references them as $; without -# this step those references would be broken. -# -# The vendored ``skill/`` (singular) is the top-level "limina" skill; the -# vendored ``skills/*`` (plural) are the four research-methodology skills -# (experiment-rigor, exploratory-sota-research, research-devil-advocate, -# build-maintainable-software). -_SKILL_TOPLEVEL_NAME = "limina" - ActionKind = Literal[ "copied", @@ -686,34 +676,16 @@ def _merge_or_copy_markdown(src: Path, dst: Path, *, dry_run: bool = False) -> I def _install_skills(root: Path, *, force: bool, dry_run: bool = False) -> list[InstallAction]: - """Copy vendored Limina skills into ``/.claude/skills/``. + """Copy the vendored research-methodology skills into ``/.claude/skills/``. - - ``vendor/limina/skill/`` (singular, the top-level "limina" skill) → - ``/.claude/skills/limina/`` - - ``vendor/limina/skills//`` (each research skill) → - ``/.claude/skills//`` - - Each installed skill emits one ``installed_skill`` action. File-level - conflicts (existing skill files differing from vendor) are handled per - the same rules as ``_copy_tree``. + Each ``vendor/limina/skills//`` directory is copied to + ``/.claude/skills//`` and emits one ``installed_skill`` + action. File-level conflicts (existing skill files differing from + vendor) are handled per the same rules as ``_copy_tree``. """ actions: list[InstallAction] = [] dst_skills = root / ".claude" / "skills" - top_src = VENDOR_LIMINA / "skill" - if top_src.is_dir(): - dst = dst_skills / _SKILL_TOPLEVEL_NAME - tree_actions = _copy_tree(top_src, dst, force=force, dry_run=dry_run) - actions.extend(tree_actions) - if any(a.kind == "copied" for a in tree_actions): - actions.append( - InstallAction( - "installed_skill", - _display_relpath(dst), - f"copied vendor/limina/skill -> {_display_relpath(dst)}", - ) - ) - skills_src = VENDOR_LIMINA / "skills" if skills_src.is_dir(): for skill_dir in sorted(p for p in skills_src.iterdir() if p.is_dir()): diff --git a/src/aexp/vendor/limina/AGENTS.md b/src/aexp/vendor/limina/AGENTS.md index 4bbb9ba..0df2536 100644 --- a/src/aexp/vendor/limina/AGENTS.md +++ b/src/aexp/vendor/limina/AGENTS.md @@ -1,6 +1,6 @@ -# Limina — Shared Runtime Contract +# Agentic Experiments — Shared Runtime Contract -Limina is a research-first contract for autonomous technical investigation. +This is a research-first contract for autonomous technical investigation. This file is the shared machine-facing instruction surface. Keep it short, specific, and stable. Runtime-specific details belong in adapters, skills, or scoped rules. @@ -209,4 +209,4 @@ narrate. `/aexp-finding-from-batch`, or `/aexp-finding-placeholder`. The H/E/F chain closes when the finding cites real tracked runs. -Implementation work can happen, but it is not a parallel core artifact graph in Limina. Research drives the contract; delivery details belong to the local project, not the shared template. +Implementation work can happen, but it is not a parallel core artifact graph here. Research drives the contract; delivery details belong to the local project, not the shared template. diff --git a/src/aexp/vendor/limina/CLAUDE.md b/src/aexp/vendor/limina/CLAUDE.md index cdc8b45..90f642f 100644 --- a/src/aexp/vendor/limina/CLAUDE.md +++ b/src/aexp/vendor/limina/CLAUDE.md @@ -1,4 +1,4 @@ -# Limina — Claude Adapter +# Agentic Experiments — Claude Adapter Follow what `AGENTS.md` says. diff --git a/src/aexp/vendor/limina/VENDORED_FROM.txt b/src/aexp/vendor/limina/VENDORED_FROM.txt index 1b9e38a..79aed24 100644 --- a/src/aexp/vendor/limina/VENDORED_FROM.txt +++ b/src/aexp/vendor/limina/VENDORED_FROM.txt @@ -1,24 +1,16 @@ -Limina source snapshot vendored into agentic-experiments. +Vendored snapshot of "limina" (https://github.com/KadenMc/limina), v0.1.0, +forked into agentic-experiments on 2026-04-20. One-time fork; not resynced. -Upstream version: 0.1.0 (see ./VERSION) -Vendored on: 2026-04-20 -Vendored from archive at: C:\Vaults\SecondBrain\repos\agentic-experiments\reference\limina\ -Vendor policy: One-time fork. Not resynced from upstream. +The H->E->F artifact model, the kb/ layout, the templates/, and the +methodology skills under this directory originate from that project and +have since been adapted as agentic-experiments' own research scaffold. -Files under this directory are the forked copy; the pristine snapshot is -preserved at ``reference/limina/`` at the repo root for diff provenance. - -Changes applied during vendoring: -- Replaced the 4 shell hooks with cross-platform Python ports: - scripts/hooks/session_start.sh -> session_start.py - scripts/hooks/enforce_hef_chain.sh -> enforce_hef_chain.py - scripts/hooks/kb_write_guard.sh -> kb_write_guard.py - scripts/hooks/stop_validate.sh -> stop_validate.py -- Refactored scripts/hooks/_parse_hook_input.py into an importable module - (preserved CLI fallback for backward compatibility). -- Translated .claude/settings.json into ./claude_settings.json with the - hook commands rewritten from `bash ...sh` to `python ...py`. -- Dropped upstream setup.sh (its clone-and-reinit flow is replaced by - aexp.install.install_limina). -- Dropped upstream requirements.txt (the sole entry, python-frontmatter, - is now a first-class dependency in agentic-experiments' pyproject.toml). +Adaptations applied during vendoring: +- The 4 shell hooks were replaced with cross-platform Python ports; they + now live in the aexp package (aexp.hooks.*) and are wired up by + `aexp install`. +- The upstream .claude/settings.json was folded into the settings merge + that `aexp install` performs. +- The upstream setup.sh and requirements.txt were dropped: project setup + is `aexp install`, and dependencies live in agentic-experiments' + pyproject.toml. diff --git a/src/aexp/vendor/limina/kb/DASHBOARD.md b/src/aexp/vendor/limina/kb/DASHBOARD.md index 207ea84..120caa3 100644 --- a/src/aexp/vendor/limina/kb/DASHBOARD.md +++ b/src/aexp/vendor/limina/kb/DASHBOARD.md @@ -3,7 +3,7 @@ aliases: ["DASHBOARD"] type: dashboard --- -# Limina Dashboard +# Research Dashboard ## Entry Points diff --git a/src/aexp/vendor/limina/skill/SKILL.md b/src/aexp/vendor/limina/skill/SKILL.md deleted file mode 100644 index eda6ecf..0000000 --- a/src/aexp/vendor/limina/skill/SKILL.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -name: limina -description: "Set up and launch an autonomous research project with Limina. Use when the user wants a persistent research agent workflow with hypotheses, experiments, findings, and review artifacts." ---- - -# Limina - -Limina is a small research-first agent template. It keeps durable evidence in `kb/` without dragging a large operational ledger into every session. - -## Workflow - -### Step 1: Introduce Limina - -Before asking setup questions, explain: - -> **What you're setting up:** an autonomous research project with a persistent knowledge base, a narrow active-state file, and a required `H -> E -> F` evidence flow. -> -> The agent can work across long sessions, but the always-on context stays small: mission brief, active state, and only the relevant artifacts for the current step. - -### Step 2: Ask for a project name - -Default: `limina-research`. - -### Step 3: Clone the template - -Clone `https://github.com/theam/limina.git` into `.//`, then remove `.git` and initialize a fresh repo. - -### Step 4: Check prerequisites - -Verify that `python3`, `git`, and either Claude Code or Codex are available. - -Install Python dependencies: - -```bash -pip install -r requirements.txt -``` - -### Step 5: Define the mission - -Ask for: - -1. objective -2. context or baseline -3. success criteria - -Use concise examples and keep the prompt focused on the research problem, not on project administration. - -### Step 6: Write the mission brief - -Create `kb/mission/CHALLENGE.md` with: - -```markdown ---- -aliases: ["CHALLENGE"] -type: mission ---- - -# Research Mission - -## Objective - - - -## Context - - - -## Success Criteria - - - -## Constraints - -- Ask when blocked on access, trust in the evaluation, or strategic decisions. -- Persist durable evidence in `kb/`. -- Keep active state in `kb/ACTIVE.md`. - -## Links - -- Active State: [[ACTIVE]] -- Dashboard: [[DASHBOARD]] -``` - -Create `kb/ACTIVE.md` with: - -```markdown ---- -aliases: ["ACTIVE"] -type: active-state ---- - -# Active State - -## Current Objective - -Initialize the research loop for the mission. - -## Next Step - -Read the mission and form the first concrete research question. - -## Blocker - -None. - -## Links - -- Mission: [[CHALLENGE]] -``` - -### Step 7: Initial commit - -```bash -git add -A -git commit -m "Initialize Limina research project" -``` - -### Step 8: Tell the user how to start - -Tell the user: - -> Your research project is ready at `.//`. -> -> Open Claude Code in the project directory: -> -> ```bash -> cd && claude -> ``` -> -> Or open the folder in Codex. -> -> The runtime loads the mission brief and active state at startup. Hooks enforce `H -> E -> F`, validate kb writes, and run a final kb validation before stop. -> -> To install the bundled repo-local companion skills (`experiment-rigor`, `exploratory-sota-research`, `research-devil-advocate`, `build-maintainable-software`) into Claude Code and/or Codex from inside the project repo, run: -> -> ```bash -> bash scripts/install_skills.sh -> ``` diff --git a/src/aexp/vendor/limina/skills/build-maintainable-software/SKILL.md b/src/aexp/vendor/limina/skills/build-maintainable-software/SKILL.md index ec95e64..bb8a2df 100644 --- a/src/aexp/vendor/limina/skills/build-maintainable-software/SKILL.md +++ b/src/aexp/vendor/limina/skills/build-maintainable-software/SKILL.md @@ -62,8 +62,8 @@ When trade-offs are ambiguous, prefer: - Open [references/design-principles.md](references/design-principles.md) for new features, major refactors, architecture changes, module boundaries, API design, naming, state management, error handling, or dependency direction. - Open [references/review-checklist.md](references/review-checklist.md) for code review, cleanup, simplification, test strategy, risk checks, or the final maintainability pass. - If the repository has local guidance (`AGENTS.md`, `CLAUDE.md`, `CONTRIBUTING.md`, architecture docs, or test/lint scripts), follow those specifics first and use this skill as the cross-project default. -- In Limina projects, use this skill for implementation, refactor, and review work that supports the mission. Do not use it to replace the research loop or to justify direction changes without evidence. -- In Limina projects, pair this skill with: +- In a research project, use this skill for implementation, refactor, and review work that supports the mission. Do not use it to replace the research loop or to justify direction changes without evidence. +- In a research project, pair this skill with: - `$experiment-rigor` when the code change is part of experiment design, execution, or interpretation - `$exploratory-sota-research` when the missing piece is external research rather than code quality or design @@ -79,5 +79,5 @@ When trade-offs are ambiguous, prefer: - When touching legacy code, leave the surrounding area clearer than you found it, but keep the behavioral delta tight. - When reviewing, distinguish correctness risks, maintainability risks, and optional polish. Prioritize the first two. - When comments are needed, explain why, invariants, or non-obvious trade-offs. Do not narrate obvious code. -- If the work changes a mission-critical behavior, evaluation surface, or system assumption in Limina, persist the relevant rationale or follow-up evidence in the repository's `kb/` flow. +- If the work changes a mission-critical behavior, evaluation surface, or system assumption in the project, persist the relevant rationale or follow-up evidence in the repository's `kb/` flow. - If a requested design conflicts with these rules, explain the trade-off briefly and follow the user's explicit intent. diff --git a/src/aexp/vendor/limina/skills/experiment-rigor/SKILL.md b/src/aexp/vendor/limina/skills/experiment-rigor/SKILL.md index 662ca09..277e49e 100644 --- a/src/aexp/vendor/limina/skills/experiment-rigor/SKILL.md +++ b/src/aexp/vendor/limina/skills/experiment-rigor/SKILL.md @@ -1,11 +1,11 @@ --- name: experiment-rigor -description: Design, review, and conclude rigorous research experiments for Limina's H→E→F workflow. Use when creating or revising hypotheses, experiments, or findings; defining baselines, datasets, metrics, or stopping rules; comparing candidate methods; deciding whether a result is conclusive; or when a negative result may be caused by an invalid setup rather than a true method failure. +description: Design, review, and conclude rigorous research experiments for the H→E→F workflow. Use when creating or revising hypotheses, experiments, or findings; defining baselines, datasets, metrics, or stopping rules; comparing candidate methods; deciding whether a result is conclusive; or when a negative result may be caused by an invalid setup rather than a true method failure. --- # Experiment Rigor -Use this skill to turn Limina research into decision-grade evidence. +Use this skill to turn research into decision-grade evidence. ## When to use it diff --git a/src/aexp/vendor/limina/skills/experiment-rigor/agents/openai.yaml b/src/aexp/vendor/limina/skills/experiment-rigor/agents/openai.yaml index 1ba0875..3661ecf 100644 --- a/src/aexp/vendor/limina/skills/experiment-rigor/agents/openai.yaml +++ b/src/aexp/vendor/limina/skills/experiment-rigor/agents/openai.yaml @@ -1,6 +1,6 @@ interface: display_name: "Experiment Rigor" - short_description: "Design rigorous Limina experiments" - default_prompt: "Use $experiment-rigor to design or review a Limina hypothesis or experiment before it runs." + short_description: "Design rigorous research experiments" + default_prompt: "Use $experiment-rigor to design or review a hypothesis or experiment before it runs." policy: allow_implicit_invocation: false diff --git a/src/aexp/vendor/limina/skills/experiment-rigor/evals/trigger-prompts.csv b/src/aexp/vendor/limina/skills/experiment-rigor/evals/trigger-prompts.csv index 90b85df..d71c1e9 100644 --- a/src/aexp/vendor/limina/skills/experiment-rigor/evals/trigger-prompts.csv +++ b/src/aexp/vendor/limina/skills/experiment-rigor/evals/trigger-prompts.csv @@ -2,7 +2,7 @@ id,should_trigger,prompt rigor-01,true,"Use $experiment-rigor to review whether H003 is specific enough before we run E007." rigor-02,true,"Design a fair experiment to compare a new reranker against the incumbent with clear guardrails." rigor-03,true,"Decide whether this negative result is a true method failure or just an invalid setup." -rigor-04,true,"Help me choose baselines, slices, and stopping rules for this Limina experiment." +rigor-04,true,"Help me choose baselines, slices, and stopping rules for this experiment." rigor-05,true,"Review this finding and tell me whether it is conclusive or still inconclusive." rigor-06,false,"Map the state of the art for retrieval under domain shift." rigor-07,false,"Summarize this paper for me." diff --git a/src/aexp/vendor/limina/skills/experiment-rigor/references/metrics-storage.md b/src/aexp/vendor/limina/skills/experiment-rigor/references/metrics-storage.md index 8dd5f55..a078b4f 100644 --- a/src/aexp/vendor/limina/skills/experiment-rigor/references/metrics-storage.md +++ b/src/aexp/vendor/limina/skills/experiment-rigor/references/metrics-storage.md @@ -2,7 +2,7 @@ ## Principle -Keep Limina's narrative and decisions in `kb/`. +Keep the project's narrative and decisions in `kb/`. Keep raw metrics in machine-readable files under `kb/research/data/`. If you also use an external tracker, treat `kb/` as the canonical cross-session memory and store the external run IDs there. @@ -197,7 +197,7 @@ Record at minimum: - URL - model or artifact ID if available -Mirror the decisive summary back into `summary.json` and the experiment file so Limina can reason about it in later sessions without depending on the external UI. +Mirror the decisive summary back into `summary.json` and the experiment file so the agent can reason about it in later sessions without depending on the external UI. ## What belongs in `E` vs raw files diff --git a/src/aexp/vendor/limina/skills/exploratory-sota-research/SKILL.md b/src/aexp/vendor/limina/skills/exploratory-sota-research/SKILL.md index e5c8d21..d7b78eb 100644 --- a/src/aexp/vendor/limina/skills/exploratory-sota-research/SKILL.md +++ b/src/aexp/vendor/limina/skills/exploratory-sota-research/SKILL.md @@ -5,7 +5,7 @@ description: Map the AI/ML state of the art for a concrete technical problem. Us # Exploratory SOTA Research -Use this skill to map the external mechanism landscape for a hard AI/ML research problem and turn that map into concrete Limina artifacts. +Use this skill to map the external mechanism landscape for a hard AI/ML research problem and turn that map into concrete research artifacts. ## When to use it @@ -25,9 +25,9 @@ Do not use this skill for: - implementation-only requests - questions where the user clearly wants a direct answer rather than an exploratory research process -## Limina adapter +## kb/ adapter -When you use this skill inside a Limina repo: +When you use this skill inside a research project: 1. Read `kb/mission/CHALLENGE.md`. 2. Read `kb/ACTIVE.md`. @@ -191,7 +191,7 @@ For every serious candidate or source cluster, record at minimum: - limitations - recommendation -Inside Limina: +Inside a research project: - create or update `L` notes for serious sources, not just ad-hoc notes in chat - update `ACTIVE.md` when the working set changes - if the search materially changes the research direction, create or update `H`, `CR`, or `SR` @@ -244,7 +244,7 @@ Unless the user asked for a different format, return: 8. Generalizable insights 9. Open questions 10. Suggested next searches or experiments -11. Persistent updates in Limina +11. Persistent updates in kb/ ## Output quality bar @@ -262,7 +262,7 @@ A poor answer: - treats citations or venue prestige as enough - ignores cost and reproducibility - recommends mechanisms without saying where they fail -- leaves decisive conclusions only in chat when Limina artifacts should change +- leaves decisive conclusions only in chat when kb/ artifacts should change ## Additional resources @@ -274,4 +274,4 @@ Load these only when helpful: ## Final instruction -Be an exploratory researcher, not a keyword search engine. Inside Limina, leave behind durable notes and next moves, not just a polished survey. +Be an exploratory researcher, not a keyword search engine. Inside a research project, leave behind durable notes and next moves, not just a polished survey. diff --git a/src/aexp/vendor/limina/skills/exploratory-sota-research/references/output-template.md b/src/aexp/vendor/limina/skills/exploratory-sota-research/references/output-template.md index a9114fc..d4cd5b3 100644 --- a/src/aexp/vendor/limina/skills/exploratory-sota-research/references/output-template.md +++ b/src/aexp/vendor/limina/skills/exploratory-sota-research/references/output-template.md @@ -74,7 +74,7 @@ List the unknowns that prevent a stronger recommendation. ## 10. Suggested next searches or experiments Give the next 3-7 concrete research or validation moves. -## 11. Persistent updates in Limina +## 11. Persistent updates in kb/ - Which `L` notes were created or updated? - Did the search open or revise any `H`, `CR`, or `SR` artifacts? - How should `kb/ACTIVE.md` change? @@ -107,7 +107,7 @@ Use this card for each serious paper, benchmark, artifact, or review. - Main assumptions: - Main limitations / risks: - Recommendation: -- Limina note: +- kb/ note: --- diff --git a/src/aexp/vendor/limina/skills/exploratory-sota-research/references/worked-example-information-retrieval.md b/src/aexp/vendor/limina/skills/exploratory-sota-research/references/worked-example-information-retrieval.md index 275a648..e4e61f9 100644 --- a/src/aexp/vendor/limina/skills/exploratory-sota-research/references/worked-example-information-retrieval.md +++ b/src/aexp/vendor/limina/skills/exploratory-sota-research/references/worked-example-information-retrieval.md @@ -149,7 +149,7 @@ It should say something like: - Narrow heuristic patches that only fix one query subtype unless they are being used as a short-term stopgap. - Tiny leaderboard gains with large serving cost and no sign of transfer. -## How to persist this in Limina +## How to persist this in kb/ After the landscape pass: - write the serious external sources as `L` notes diff --git a/src/aexp/vendor/limina/skills/research-devil-advocate/SKILL.md b/src/aexp/vendor/limina/skills/research-devil-advocate/SKILL.md index c0a057d..b37e1c5 100644 --- a/src/aexp/vendor/limina/skills/research-devil-advocate/SKILL.md +++ b/src/aexp/vendor/limina/skills/research-devil-advocate/SKILL.md @@ -1,11 +1,11 @@ --- name: research-devil-advocate -description: Challenge the current Limina research direction and next-step plan. Use for step-back reviews, pre-commitment checkpoints, plateau reviews, contradictory findings, or pivot/continue/stop recommendations when evidence or framing may be weak. +description: Challenge the current research direction and next-step plan. Use for step-back reviews, pre-commitment checkpoints, plateau reviews, contradictory findings, or pivot/continue/stop recommendations when evidence or framing may be weak. --- # Research Devil's Advocate -You are the adversarial research reviewer for Limina. Your job is **not** to help the current plan look reasonable. Your job is to decide whether the current direction deserves more time. +You are the adversarial research reviewer. Your job is **not** to help the current plan look reasonable. Your job is to decide whether the current direction deserves more time. For difficult reviews, ultrathink before concluding. @@ -56,7 +56,7 @@ If not specified, classify the review as one of: ### 2) Re-ground in evidence -Inside a Limina repository, read at minimum: +Inside a research project, read at minimum: - `kb/mission/CHALLENGE.md` - `kb/ACTIVE.md` - the directly linked `H`, `E`, `F`, `L`, `CR`, or `SR` artifacts relevant to the current direction @@ -130,7 +130,7 @@ If the review changes strategic framing, trust in the setup, or the mission path - update `kb/ACTIVE.md` - keep parent/child `## Links` consistent -If a local project keeps a decision log outside the Limina core, update it only if it already exists. Do not assume `DECISIONS.md` is part of the required base template. +If a local project keeps a decision log outside the kb/ core, update it only if it already exists. Do not assume `DECISIONS.md` is part of the required base template. Run: ```bash @@ -148,7 +148,7 @@ End with a concise executive summary: - what to do next - what to stop doing immediately -## Limina integration notes +## Integration notes - Use this skill when adversarial review is the main need. - Pair it with `$experiment-rigor` when the outcome is "continue, but redesign the hypothesis or experiment." diff --git a/src/aexp/vendor/limina/skills/research-devil-advocate/agents/openai.yaml b/src/aexp/vendor/limina/skills/research-devil-advocate/agents/openai.yaml index 1a36461..9d9780c 100644 --- a/src/aexp/vendor/limina/skills/research-devil-advocate/agents/openai.yaml +++ b/src/aexp/vendor/limina/skills/research-devil-advocate/agents/openai.yaml @@ -1,6 +1,6 @@ interface: display_name: "Research Devil's Advocate" - short_description: "Adversarial step-back review for Limina research direction" - default_prompt: "Use $research-devil-advocate to challenge the current Limina research direction and tell me whether we should continue, continue with fixes, pivot, stop, or escalate." + short_description: "Adversarial step-back review for research direction" + default_prompt: "Use $research-devil-advocate to challenge the current research direction and tell me whether we should continue, continue with fixes, pivot, stop, or escalate." policy: allow_implicit_invocation: false diff --git a/src/aexp/vendor/limina/skills/research-devil-advocate/evals/trigger-prompts.csv b/src/aexp/vendor/limina/skills/research-devil-advocate/evals/trigger-prompts.csv index 51e7889..be84bfc 100644 --- a/src/aexp/vendor/limina/skills/research-devil-advocate/evals/trigger-prompts.csv +++ b/src/aexp/vendor/limina/skills/research-devil-advocate/evals/trigger-prompts.csv @@ -1,6 +1,6 @@ id,should_trigger,prompt devil-01,true,"Use $research-devil-advocate to challenge whether this retrieval direction still deserves more time after two noisy experiments." -devil-02,true,"Give me a step-back review on this Limina research plan and tell me whether we should continue, pivot, stop, or escalate." +devil-02,true,"Give me a step-back review on this research plan and tell me whether we should continue, pivot, stop, or escalate." devil-03,true,"These findings conflict with our earlier conclusion. Use $research-devil-advocate to audit the direction." devil-04,true,"Before we spend a week on this hypothesis, run a devil's-advocate checkpoint on the current evidence." devil-05,true,"We may be stuck in a local maximum. Challenge the current research direction and propose the smallest next move." diff --git a/tests/test_install.py b/tests/test_install.py index 2e73fe6..02ec16f 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -435,8 +435,8 @@ def test_install_action_kinds_are_expected(fresh_git_repo: Path) -> None: assert a.path, a -def test_install_copies_limina_skills_to_claude_skills(fresh_git_repo: Path) -> None: - """All vendored Limina skills must land under /.claude/skills/. +def test_install_copies_skills_to_claude_skills(fresh_git_repo: Path) -> None: + """All vendored research skills must land under /.claude/skills/. Without these, the AGENTS.md references like $experiment-rigor are broken on every consumer repo. @@ -444,8 +444,6 @@ def test_install_copies_limina_skills_to_claude_skills(fresh_git_repo: Path) -> install_limina(fresh_git_repo) skills_root = fresh_git_repo / ".claude" / "skills" assert skills_root.is_dir() - # Top-level "limina" skill (from vendor/limina/skill/) - assert (skills_root / "limina" / "SKILL.md").is_file() # Research-methodology skills (from vendor/limina/skills/) expected = { "experiment-rigor", @@ -462,10 +460,10 @@ def test_install_copies_limina_skills_to_claude_skills(fresh_git_repo: Path) -> def test_install_skills_emits_installed_skill_actions(fresh_git_repo: Path) -> None: actions = install_limina(fresh_git_repo) skill_actions = [a for a in actions if a.kind == "installed_skill"] - # 4 research skills + 1 top-level "limina" skill = 5 installed_skill entries. - assert len(skill_actions) == 5, [a.path for a in skill_actions] + # 4 research-methodology skills = 4 installed_skill entries. + assert len(skill_actions) == 4, [a.path for a in skill_actions] paths = {Path(a.path).name for a in skill_actions} - assert "limina" in paths + assert "experiment-rigor" in paths assert "experiment-rigor" in paths From cba517b8efd4db0111e57bb509e0956a1b05a9b7 Mon Sep 17 00:00:00 2001 From: Kaden McKeen Date: Wed, 20 May 2026 22:55:43 -0400 Subject: [PATCH 02/10] =?UTF-8?q?refactor(debrand):=20stage=20B=20?= =?UTF-8?q?=E2=80=94=20rename=20the=20persisted=20"limina"=20keys=20(dual-?= =?UTF-8?q?read)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the persisted storage keys that carried the "limina" name, with a read-side fallback so existing signac projects and install markers self-heal -- no migration script: - Run-link key: job.doc["limina"] -> job.doc["aexp"]. New helpers schema.read_run_link() (reads "aexp", falls back to legacy "limina") and schema.write_run_link() (writes "aexp", clears the legacy key so a re-stamped job self-heals). All read sites + both write sites (runs.py, linking.py, queue.py, trackers/base.py, validate.py) routed through the helpers. - Install marker: .aexp/installed.json "limina_vendor_sha" -> "vendor_sha"; the install short-circuit dual-reads both keys. - W&B run config: config["limina"] -> config["aexp"]. Past W&B runs keep config.limina; new runs get config.aexp. - Validator error codes: limina.validation_failed / limina.validator_unavailable -> aexp.* (not persisted). Tests updated to the new keys/codes. Part of the limina de-brand (one PR, four staged commits A-D). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/aexp/cli.py | 2 +- src/aexp/install.py | 10 ++++-- src/aexp/linking.py | 20 ++++++----- src/aexp/mcp_server.py | 2 +- src/aexp/queue.py | 12 +++---- src/aexp/runs.py | 34 +++++++++--------- src/aexp/schema.py | 44 +++++++++++++++++++++--- src/aexp/slash_commands/aexp-validate.md | 4 +-- src/aexp/trackers/base.py | 32 ++++++++--------- src/aexp/utils/paths.py | 10 +++--- src/aexp/validate.py | 18 +++++----- tests/test_install.py | 2 +- tests/test_linking.py | 6 ++-- tests/test_queue.py | 4 +-- tests/test_runs.py | 4 +-- tests/test_trackers_wandb.py | 4 +-- tests/test_utils.py | 14 ++++---- tests/test_validate.py | 14 ++++---- 18 files changed, 139 insertions(+), 97 deletions(-) diff --git a/src/aexp/cli.py b/src/aexp/cli.py index f0524e5..f10c600 100644 --- a/src/aexp/cli.py +++ b/src/aexp/cli.py @@ -772,7 +772,7 @@ def link( hypothesis: str | None = typer.Option(None, "--hypothesis"), sub_hypothesis: str | None = typer.Option(None, "--sub-hypothesis"), ) -> None: - """Retroactively stamp ``job.doc['limina']`` onto an existing run.""" + """Retroactively stamp ``job.doc['aexp']`` onto an existing run.""" link_to_experiment( job_id, experiment_id=experiment, diff --git a/src/aexp/install.py b/src/aexp/install.py index aa3502a..4c8cb36 100644 --- a/src/aexp/install.py +++ b/src/aexp/install.py @@ -893,7 +893,13 @@ def install_limina( (existing_marker or {}).get("jupyter_enabled", False) ) if existing_marker and not force: - if existing_marker.get("limina_vendor_sha") == vendor_sha: + # Dual-read: markers written before the de-brand carry the legacy + # `limina_vendor_sha` key. Fall back to it so an old marker still + # short-circuits cleanly instead of forcing a spurious re-install. + marker_sha = existing_marker.get("vendor_sha") or existing_marker.get( + "limina_vendor_sha" + ) + if marker_sha == vendor_sha: actions.append( InstallAction( "already_installed", @@ -1010,7 +1016,7 @@ def install_limina( root, version=__version__, run_store_path=run_store, - limina_vendor_sha=vendor_sha, + vendor_sha=vendor_sha, jupyter_enabled=with_jupyter, ) actions.append(InstallAction("wrote_marker", _display_relpath(marker_path))) diff --git a/src/aexp/linking.py b/src/aexp/linking.py index 808454f..f1136bb 100644 --- a/src/aexp/linking.py +++ b/src/aexp/linking.py @@ -9,7 +9,7 @@ 1. :func:`runs_for_experiment` — convenience wrapper over ``find_runs``. 2. :func:`list_batches` / :func:`show_batch` — distinct ``(experiment, condition)`` slices rolled up to :class:`BatchSummary`. -3. :func:`link_to_experiment` — retroactively stamp ``job.doc["limina"]`` +3. :func:`link_to_experiment` — retroactively stamp ``job.doc["aexp"]`` onto a job that was created without a link (or to repoint it). """ from __future__ import annotations @@ -27,6 +27,8 @@ RunStatus, RunSummary, batch_slug, + read_run_link, + write_run_link, ) # --------------------------------------------------------------------------- @@ -39,16 +41,16 @@ def runs_for_experiment( *, repo_root: str | Path | None = None, ) -> list[signac.job.Job]: - """Return every job linked to a given Limina ``E###``.""" + """Return every job linked to a given ``E###``.""" return find_runs(experiment_id=experiment_id, repo_root=repo_root) def summarize_run(job: signac.job.Job) -> RunSummary: """Flatten a signac job into a :class:`RunSummary` row.""" - limina = dict(job.doc.get("limina", {})) + link = read_run_link(job.doc) tracker = dict(job.doc.get("tracker", {})) - hyp = job.sp.get("hypothesis_id") or limina.get("hypothesis_id") - exp = job.sp.get("experiment_id") or limina.get("experiment_id") + hyp = job.sp.get("hypothesis_id") or link.get("hypothesis_id") + exp = job.sp.get("experiment_id") or link.get("experiment_id") condition = job.sp.get("condition") slug = batch_slug( hypothesis_id=hyp, @@ -106,7 +108,7 @@ def list_batches( # Group by (experiment_id, *selector_values) groups: dict[tuple, list[signac.job.Job]] = defaultdict(list) for job in jobs: - exp = job.sp.get("experiment_id") or job.doc.get("limina", {}).get("experiment_id") + exp = job.sp.get("experiment_id") or read_run_link(job.doc).get("experiment_id") if exp is None: continue key = (exp,) + _selector_key(job, selector_keys) @@ -117,7 +119,7 @@ def list_batches( exp = key[0] sel = dict(zip(selector_keys, key[1:], strict=True)) first = batch_jobs[0] - hyp = first.sp.get("hypothesis_id") or first.doc.get("limina", {}).get("hypothesis_id") + hyp = first.sp.get("hypothesis_id") or read_run_link(first.doc).get("hypothesis_id") cond = sel.get("condition") slug = batch_slug( hypothesis_id=hyp, @@ -180,7 +182,7 @@ def link_to_experiment( experiment_path: str | None = None, repo_root: str | Path | None = None, ) -> signac.job.Job: - """Stamp (or overwrite) ``job.doc["limina"]`` on an existing job. + """Stamp (or overwrite) ``job.doc["aexp"]`` on an existing job. Used by the ``aex link`` command to retroactively link jobs that were created outside ``create_run`` (e.g. by direct signac calls from notebooks). @@ -192,7 +194,7 @@ def link_to_experiment( hypothesis_id=hypothesis_id, sub_hypothesis_id=sub_hypothesis_id, ) - job.doc["limina"] = link.model_dump() + write_run_link(job.doc, link.model_dump()) return job diff --git a/src/aexp/mcp_server.py b/src/aexp/mcp_server.py index e1e5b74..957f5a7 100644 --- a/src/aexp/mcp_server.py +++ b/src/aexp/mcp_server.py @@ -525,7 +525,7 @@ def link_run( sub_hypothesis_id: str | None = None, experiment_path: str | None = None, ) -> dict[str, Any]: - """Retroactively stamp ``doc['limina']`` onto an existing signac job.""" + """Retroactively stamp ``doc['aexp']`` onto an existing signac job.""" job = _link_to_experiment( job_id, experiment_id=experiment_id, diff --git a/src/aexp/queue.py b/src/aexp/queue.py index 2a1d187..fc2a972 100644 --- a/src/aexp/queue.py +++ b/src/aexp/queue.py @@ -50,7 +50,7 @@ open_run, run_lifecycle, ) -from aexp.schema import MaterializeResult, QueueEntry, iso_utc_now +from aexp.schema import MaterializeResult, QueueEntry, iso_utc_now, read_run_link from aexp.utils.atomic import atomic_write, doc_op_with_retry from aexp.utils.git import get_dirty_diff_summary from aexp.utils.paths import find_repo_root @@ -554,11 +554,11 @@ def add_many_to_queue( def _job_to_queue_entry(job: signac.job.Job) -> QueueEntry: q = dict(job.doc.get("queue") or {}) - limina = dict(job.doc.get("limina") or {}) + link = read_run_link(job.doc) return QueueEntry( job_id=job.id, - experiment_id=limina.get("experiment_id") or job.sp.get("experiment_id"), - hypothesis_id=limina.get("hypothesis_id") or job.sp.get("hypothesis_id"), + experiment_id=link.get("experiment_id") or job.sp.get("experiment_id"), + hypothesis_id=link.get("hypothesis_id") or job.sp.get("hypothesis_id"), status=job.doc.get("status"), tag=q.get("tag"), queued_at=q.get("queued_at"), @@ -825,8 +825,8 @@ def _resolve_runner_template( override = q.get("runner_command_override") if override: return str(override) - limina = dict(job.doc.get("limina") or {}) - exp_id = limina.get("experiment_id") or job.sp.get("experiment_id") + link = read_run_link(job.doc) + exp_id = link.get("experiment_id") or job.sp.get("experiment_id") if not exp_id: raise RunnerCommandMissing( f"job {job.id} has no experiment_id; cannot resolve runner_command" diff --git a/src/aexp/runs.py b/src/aexp/runs.py index 8e44c02..19ecc57 100644 --- a/src/aexp/runs.py +++ b/src/aexp/runs.py @@ -1,4 +1,4 @@ -"""signac-backed run store + the Limina-aware ``create_run`` / ``find_runs`` API. +"""signac-backed run store + the research-aware ``create_run`` / ``find_runs`` API. Plan §6: thin wrappers that expose signac's ``Project`` / ``Job`` types directly — we do *not* hide them — while owning two conventions: @@ -6,7 +6,7 @@ 1. State point auto-population: ``experiment_id`` is always injected into ``job.sp``; ``code_commit`` / ``code_dirty`` are injected by default (switchable via ``include_commit=False``). -2. Job-document layout: ``job.doc["limina"]`` carries the ``RunLink`` dict; +2. Job-document layout: ``job.doc["aexp"]`` carries the ``RunLink`` dict; ``job.doc["status"]`` tracks the lifecycle; ``job.doc["tracker"]`` is populated later by tracker adapters. """ @@ -22,7 +22,7 @@ import signac -from aexp.schema import RunLink, RunStatus, iso_utc_now +from aexp.schema import RunLink, RunStatus, iso_utc_now, read_run_link, write_run_link from aexp.utils.atomic import doc_op_with_retry from aexp.utils.git import get_git_provenance from aexp.utils.paths import ( @@ -142,13 +142,13 @@ def create_run( include_commit: bool = True, resolve_conditions: bool = True, ) -> signac.job.Job: - """Open (or create) a signac job linked to a Limina experiment. + """Open (or create) a signac job linked to an experiment. Parameters ---------- experiment_id : str - Limina ``E###`` id. Always mirrored into ``job.sp["experiment_id"]`` - and ``job.doc["limina"]["experiment_id"]``. + The ``E###`` id. Always mirrored into ``job.sp["experiment_id"]`` + and ``job.doc["aexp"]["experiment_id"]``. statepoint : dict | None Caller-supplied identity params. Merged on top of the auto-populated defaults (``experiment_id``, optional ``code_commit`` / ``code_dirty``). @@ -158,10 +158,10 @@ def create_run( before signac creates the job — so the resolved config is frozen to ``signac_statepoint.json``. hypothesis_id, sub_hypothesis_id : str | None - If provided, mirrored into both ``sp`` and ``doc["limina"]``. + If provided, mirrored into both ``sp`` and ``doc["aexp"]``. experiment_path : str | None Repo-relative POSIX path of the experiment artifact. Stored on - ``doc["limina"]`` for quick navigation; not required for function. + ``doc["aexp"]`` for quick navigation; not required for function. repo_root : str | Path | None Consumer repo root. Defaults to ``find_repo_root()``. init_doc : dict | None @@ -182,7 +182,7 @@ def create_run( ------- signac.job.Job The initialized job. Its workspace dir has been materialized and - ``job.doc`` carries the Limina link + initial ``status='created'``. + ``job.doc`` carries the run link + initial ``status='created'``. """ root = Path(repo_root).resolve() if repo_root else find_repo_root() project = get_run_store(root) @@ -207,7 +207,7 @@ def create_run( job = project.open_job(sp) job.init() - # Stamp Limina link + lifecycle metadata. Re-stamp is safe; values + # Stamp the run link + lifecycle metadata. Re-stamp is safe; values # are deterministic for a given job id. link = RunLink( experiment_id=experiment_id, @@ -215,7 +215,7 @@ def create_run( hypothesis_id=hypothesis_id, sub_hypothesis_id=sub_hypothesis_id, ) - job.doc["limina"] = link.model_dump() + write_run_link(job.doc, link.model_dump()) job.doc.setdefault("status", "created") job.doc.setdefault("created_at", iso_utc_now()) @@ -258,10 +258,10 @@ def find_runs( repo_root: str | Path | None = None, **sp_filters: Any, ) -> list[signac.job.Job]: - """Return signac jobs filtered by Limina-link metadata and / or ``sp`` keys. + """Return signac jobs filtered by run-link metadata and / or ``sp`` keys. ``experiment_id`` and ``hypothesis_id`` match either a top-level ``sp`` - key (the canonical path) or the ``doc["limina"]`` fields (fallback for + key (the canonical path) or the ``doc["aexp"]`` fields (fallback for runs created before the sp mirror convention). Status is matched against ``doc["status"]``. @@ -289,12 +289,12 @@ def find_runs( continue # Back-compat fallback when sp doesn't carry the link but doc does. if experiment_id is not None and job.sp.get("experiment_id") != experiment_id: - limina = job.doc.get("limina", {}) - if limina.get("experiment_id") != experiment_id: + link = read_run_link(job.doc) + if link.get("experiment_id") != experiment_id: continue if hypothesis_id is not None and job.sp.get("hypothesis_id") != hypothesis_id: - limina = job.doc.get("limina", {}) - if limina.get("hypothesis_id") != hypothesis_id: + link = read_run_link(job.doc) + if link.get("hypothesis_id") != hypothesis_id: continue results.append(job) return results diff --git a/src/aexp/schema.py b/src/aexp/schema.py index 767c8f4..3959c32 100644 --- a/src/aexp/schema.py +++ b/src/aexp/schema.py @@ -1,12 +1,12 @@ """Pydantic + dataclass models for the fusion layer. -The types here are the *lingua franca* between signac jobs, Limina artifacts, +The types here are the *lingua franca* between signac jobs, research artifacts, tracker adapters, and the CLI. They deliberately stay small and frozen-ish; business logic lives in the modules that produce / consume them. Conventions ----------- -- ``RunLink``: the canonical shape of ``job.doc["limina"]``. +- ``RunLink``: the canonical shape of ``job.doc["aexp"]``. - ``SupportingRun`` / ``BatchSelector``: entries in a Finding's ``supporting_runs:`` frontmatter list (see plan §2, §8). - ``LiminaArtifactRef``: typed handle returned by ``limina_io`` readers. @@ -15,6 +15,7 @@ """ from __future__ import annotations +from collections.abc import Mapping, MutableMapping from dataclasses import dataclass from datetime import UTC, date, datetime from typing import Any, Literal @@ -26,7 +27,7 @@ # --------------------------------------------------------------------------- ArtifactKind = Literal["H", "E", "F", "L", "CR", "SR", "T"] -"""The seven Limina artifact kinds validated by vendored ``kb_validate.py``. +"""The seven research artifact kinds validated by vendored ``kb_validate.py``. ``H``=Hypothesis, ``E``=Experiment, ``F``=Finding, ``L``=Literature, ``CR``=Challenge Review, ``SR``=Strategic Review, @@ -71,12 +72,19 @@ # --------------------------------------------------------------------------- -# Limina <-> signac link +# Run-link <-> signac # --------------------------------------------------------------------------- +RUN_LINK_KEY = "aexp" +"""``job.doc`` key under which the :class:`RunLink` dict is stored.""" + +_LEGACY_RUN_LINK_KEY = "limina" +"""Pre-rename run-link key. Read-only fallback so signac projects created +before the de-brand keep resolving; never written.""" + class RunLink(BaseModel): - """Canonical shape of ``job.doc["limina"]``. + """Canonical shape of ``job.doc["aexp"]``. Mirrors the invariants in plan §2: every tracked run must reference at least an experiment; hypothesis is optional when a run links to an @@ -95,6 +103,29 @@ class RunLink(BaseModel): sub_hypothesis_id: str | None = Field(default=None, pattern=r"^H\d{3}$") +def read_run_link(doc: Mapping[str, Any]) -> dict[str, Any]: + """Return the run-link dict stored on a signac ``job.doc``. + + Reads ``doc[RUN_LINK_KEY]``, falling back to the legacy ``limina`` key + for runs stamped before the key was renamed. Always returns a plain + dict -- empty when neither key is present. + """ + link = doc.get(RUN_LINK_KEY) + if link is None: + link = doc.get(_LEGACY_RUN_LINK_KEY) + return dict(link or {}) + + +def write_run_link(doc: MutableMapping[str, Any], link: Mapping[str, Any]) -> None: + """Stamp the run-link dict onto a signac ``job.doc``. + + Writes ``doc[RUN_LINK_KEY]`` and clears any legacy ``limina`` key, so a + job re-stamped after the de-brand self-heals to the current key. + """ + doc[RUN_LINK_KEY] = dict(link) + doc.pop(_LEGACY_RUN_LINK_KEY, None) + + class TrackerBinding(BaseModel): """Shape of ``job.doc["tracker"]`` after ``bind_tracker`` (plan §7).""" @@ -287,6 +318,7 @@ def iso_utc_now() -> str: "IssueSeverity", "LiminaArtifactRef", "MaterializeResult", + "RUN_LINK_KEY", "QueueEntry", "RunLink", "RunStatus", @@ -297,4 +329,6 @@ def iso_utc_now() -> str: "batch_slug", "date", "iso_utc_now", + "read_run_link", + "write_run_link", ] diff --git a/src/aexp/slash_commands/aexp-validate.md b/src/aexp/slash_commands/aexp-validate.md index b8f2793..bce1086 100644 --- a/src/aexp/slash_commands/aexp-validate.md +++ b/src/aexp/slash_commands/aexp-validate.md @@ -27,10 +27,10 @@ Flow: update ``supporting_runs:`` or re-run the job. - ``finding.empty_batch`` — the batch selector matches zero runs; add runs or change the selector. - - ``run.orphan`` — the signac job has no ``doc["limina"]`` link; + - ``run.orphan`` — the signac job has no ``doc["aexp"]`` link; stamp one via `python -m aexp link --experiment E###`. - ``run.broken_experiment_link`` — the experiment id in - ``doc["limina"]`` does not exist in ``kb/``; either create the + ``doc["aexp"]`` does not exist in ``kb/``; either create the experiment or re-link the run. - KB issues (``metadata``, ``links``, ``backlink``, ``reference``, ``aliases``) — fix the offending artifact; most common is a missing diff --git a/src/aexp/trackers/base.py b/src/aexp/trackers/base.py index 35c45c2..ce6ce6e 100644 --- a/src/aexp/trackers/base.py +++ b/src/aexp/trackers/base.py @@ -1,6 +1,6 @@ """TrackerAdapter ABC + ``bind_tracker`` helper. -An adapter translates a signac job + its linked Limina artifact into a +An adapter translates a signac job + its linked research artifacts into a tracker-backend run, without owning any of the canonical state. After ``init_run``, the caller's ``bind_tracker`` writes the returned handle's identity into ``job.doc["tracker"]`` so the link is persistent. @@ -21,7 +21,7 @@ load_experiment, load_hypothesis, ) -from aexp.schema import TrackerBinding, batch_slug +from aexp.schema import TrackerBinding, batch_slug, read_run_link # --------------------------------------------------------------------------- # Handle / Record @@ -148,9 +148,9 @@ def _derive_tracker_payload( legacy adapter path and the new BYO-init path compute identical metadata. Pure function; does not touch ``job.doc``. """ - limina = dict(job.doc.get("limina") or {}) - exp_id = limina.get("experiment_id") or job.sp.get("experiment_id") - hyp_id = limina.get("hypothesis_id") or job.sp.get("hypothesis_id") + link = read_run_link(job.doc) + exp_id = link.get("experiment_id") or job.sp.get("experiment_id") + hyp_id = link.get("hypothesis_id") or job.sp.get("hypothesis_id") cond = condition if condition is not None else job.sp.get("condition") group = batch_slug( @@ -165,7 +165,7 @@ def _derive_tracker_payload( tags.append(hyp_id) if exp_id: tags.append(exp_id) - sub = limina.get("sub_hypothesis_id") + sub = link.get("sub_hypothesis_id") if sub: tags.append(sub) if cond: @@ -251,7 +251,7 @@ def prepare_tracker( ) -> TrackerContext: """Compute the tracker payload for a signac job without starting a run. - Derives the deterministic group slug, auto-tags, curated Limina frame, + Derives the deterministic group slug, auto-tags, curated research frame, notes, and the flattened state point, then packages them as wandb-shaped ``init_kwargs``. The caller owns ``wandb.init`` and run lifecycle; invoke :meth:`TrackerContext.bind` after ``wandb.init`` to write the signac @@ -270,7 +270,7 @@ def prepare_tracker( offline If ``True``, ``init_kwargs`` will include ``mode="offline"``. repo_root - Consumer repo root for loading the Limina frame. Auto-detected + Consumer repo root for loading the research frame. Auto-detected if omitted. entity Optional W&B entity (team / user). Threaded through ``init_kwargs`` @@ -397,14 +397,14 @@ def tracked_run( def _curated_frame(job: signac.job.Job, repo_root: Path) -> dict[str, Any]: - """Pull hypothesis statement + local hypothesis + success criteria from Limina. + """Pull hypothesis statement + local hypothesis + success criteria from the kb/ artifacts. Never uploads raw markdown — just the curated text fields a tracker run page benefits from showing as context. """ frame: dict[str, Any] = {} - limina = job.doc.get("limina") or {} - exp_id = limina.get("experiment_id") + link = read_run_link(job.doc) + exp_id = link.get("experiment_id") if not exp_id: return frame @@ -426,7 +426,7 @@ def _curated_frame(job: signac.job.Job, repo_root: Path) -> dict[str, Any]: if success: frame["success_criteria"] = success - hyp_id = limina.get("hypothesis_id") + hyp_id = link.get("hypothesis_id") if hyp_id: try: hyp = load_hypothesis(hyp_id, kb_root=kb) @@ -462,14 +462,14 @@ def build_tracker_config( ) -> dict[str, Any]: """Assemble the ``config`` payload passed to ``adapter.init_run``. - Includes the full Limina chain (hypothesis, sub-hypothesis, experiment, + Includes the full run-link chain (hypothesis, sub-hypothesis, experiment, experiment path) + the flattened state point + curated frame fields. """ - limina = dict(job.doc.get("limina") or {}) + link = read_run_link(job.doc) config: dict[str, Any] = { **_flatten_sp(job.sp), "job_id": job.id, - "limina": limina, + "aexp": link, } frame = _curated_frame(job, repo_root) if frame: @@ -529,7 +529,7 @@ def bind_tracker( offline : bool Passed through to ``adapter.init_run``. repo_root : str | Path | None - Consumer repo root for loading the Limina frame. Auto-detected if + Consumer repo root for loading the research frame. Auto-detected if omitted. """ from aexp.utils.paths import find_repo_root # lazy to avoid circular diff --git a/src/aexp/utils/paths.py b/src/aexp/utils/paths.py index 93e2e63..c7d32b8 100644 --- a/src/aexp/utils/paths.py +++ b/src/aexp/utils/paths.py @@ -25,7 +25,7 @@ class InstalledMarker(TypedDict, total=False): version: str installed_at: str run_store_path: str - limina_vendor_sha: str + vendor_sha: str python_exe: str # absolute path to the Python that ran install_limina conda_env_name: str # CONDA_DEFAULT_ENV at install time, or "" for venv/system Python jupyter_enabled: bool # True if any prior install used --with-jupyter; sticky once set @@ -92,7 +92,7 @@ def write_installed_marker( *, version: str, run_store_path: str, - limina_vendor_sha: str, + vendor_sha: str, installed_at: str | None = None, python_exe: str | None = None, conda_env_name: str | None = None, @@ -108,8 +108,8 @@ def write_installed_marker( agentic-experiments package version. run_store_path : str Path (relative to ``repo_root``) of the signac project. - limina_vendor_sha : str - Fingerprint of the vendored Limina snapshot used at install time. + vendor_sha : str + Fingerprint of the vendored research-harness snapshot used at install time. installed_at : str or None ISO-8601 UTC timestamp. Defaults to ``now`` in UTC. python_exe : str or None @@ -147,7 +147,7 @@ def write_installed_marker( "version": version, "installed_at": installed_at, "run_store_path": run_store_path, - "limina_vendor_sha": limina_vendor_sha, + "vendor_sha": vendor_sha, "python_exe": python_exe, "conda_env_name": conda_env_name, } diff --git a/src/aexp/validate.py b/src/aexp/validate.py index 4a6885f..9a19d7b 100644 --- a/src/aexp/validate.py +++ b/src/aexp/validate.py @@ -3,9 +3,9 @@ Plan §8 lays out the full set of checks: 1. Call :func:`aexp.kb_validate.validate_kb` in-process and surface its - errors as ``Issue`` rows (code ``limina.validation_failed``). + errors as ``Issue`` rows (code ``aexp.validation_failed``). 2. Walk ``.runs/workspace/*`` and for each job: - - ``run.orphan`` if ``doc["limina"]`` is missing. + - ``run.orphan`` if ``doc["aexp"]`` is missing. - ``run.broken_experiment_link`` if the referenced E### file doesn't exist. - ``run.hypothesis_mismatch`` if the run's hypothesis_id contradicts the experiment's Hypothesis frontmatter. @@ -31,7 +31,7 @@ ) from aexp.linking import list_batches from aexp.runs import get_run_store -from aexp.schema import Issue, RunStatus +from aexp.schema import Issue, RunStatus, read_run_link from aexp.utils.paths import find_repo_root VALID_STATUSES: set[RunStatus] = { @@ -104,7 +104,7 @@ def _run_kb_validate(repo_root: Path) -> list[Issue]: except Exception as exc: return [ Issue( - code="limina.validator_unavailable", + code="aexp.validator_unavailable", message=f"kb_validate could not run: {exc}", severity="error", ) @@ -116,7 +116,7 @@ def _run_kb_validate(repo_root: Path) -> list[Issue]: message = _kb_format_text(result) return [ Issue( - code="limina.validation_failed", + code="aexp.validation_failed", message=message, severity="error", ) @@ -129,7 +129,7 @@ def _run_kb_validate(repo_root: Path) -> list[Issue]: def _check_run_links(repo_root: Path) -> list[Issue]: - """Validate ``job.doc["limina"]`` + ``doc["status"]`` for every run.""" + """Validate ``job.doc["aexp"]`` + ``doc["status"]`` for every run.""" issues: list[Issue] = [] try: project = get_run_store(repo_root) @@ -153,13 +153,13 @@ def _check_run_links(repo_root: Path) -> list[Issue]: ) ) - link = job.doc.get("limina") + link = read_run_link(job.doc) if not link: issues.append( Issue( code="run.orphan", message=( - f"run {job.id[:8]} has no doc['limina'] — " + f"run {job.id[:8]} has no doc['aexp'] — " "link it with `aex link --experiment E###`" ), path=rel, @@ -172,7 +172,7 @@ def _check_run_links(repo_root: Path) -> list[Issue]: issues.append( Issue( code="run.broken_experiment_link", - message=f"run {job.id[:8]} has doc['limina'] without experiment_id", + message=f"run {job.id[:8]} has doc['aexp'] without experiment_id", path=rel, ) ) diff --git a/tests/test_install.py b/tests/test_install.py index 02ec16f..e5d26c8 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -222,7 +222,7 @@ def test_install_writes_valid_marker(fresh_git_repo: Path) -> None: assert marker is not None assert marker["version"] assert marker["run_store_path"] == ".runs" - assert len(marker["limina_vendor_sha"]) == 64 + assert len(marker["vendor_sha"]) == 64 # Cross-platform invocation fields written by default. assert "python_exe" in marker assert Path(marker["python_exe"]).exists() diff --git a/tests/test_linking.py b/tests/test_linking.py index c014baf..9b12f09 100644 --- a/tests/test_linking.py +++ b/tests/test_linking.py @@ -185,9 +185,9 @@ def test_link_to_experiment_overwrites_doc(installed_repo: Path) -> None: ) from aexp.runs import open_run reopened = open_run(job.id, repo_root=installed_repo) - assert reopened.doc["limina"]["experiment_id"] == "E099" - assert reopened.doc["limina"]["hypothesis_id"] == "H099" - assert reopened.doc["limina"]["experiment_path"].endswith("E099-repointed.md") + assert reopened.doc["aexp"]["experiment_id"] == "E099" + assert reopened.doc["aexp"]["hypothesis_id"] == "H099" + assert reopened.doc["aexp"]["experiment_path"].endswith("E099-repointed.md") def test_link_to_experiment_rejects_bad_id(installed_repo: Path) -> None: diff --git a/tests/test_queue.py b/tests/test_queue.py index f77e6f0..dfadad4 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -186,8 +186,8 @@ def test_add_to_queue_inherits_create_run_behavior( assert "code_commit" in job.sp assert "code_dirty" in job.sp assert job.sp["hypothesis_id"] == "H001" - # Limina link stamped: - assert job.doc["limina"]["experiment_id"] == "E001" + # Run link stamped: + assert job.doc["aexp"]["experiment_id"] == "E001" # --------------------------------------------------------------------------- diff --git a/tests/test_runs.py b/tests/test_runs.py index 99bdfe6..70894d3 100644 --- a/tests/test_runs.py +++ b/tests/test_runs.py @@ -153,7 +153,7 @@ def test_create_run_new_commit_yields_new_job_id(installed_repo: Path) -> None: # --------------------------------------------------------------------------- -def test_create_run_stamps_limina_link(installed_repo: Path) -> None: +def test_create_run_stamps_run_link(installed_repo: Path) -> None: job = create_run( experiment_id="E018", hypothesis_id="H012", @@ -162,7 +162,7 @@ def test_create_run_stamps_limina_link(installed_repo: Path) -> None: statepoint={"c": "full"}, repo_root=installed_repo, ) - link = job.doc["limina"] + link = job.doc["aexp"] assert link["experiment_id"] == "E018" assert link["hypothesis_id"] == "H012" assert link["sub_hypothesis_id"] == "H013" diff --git a/tests/test_trackers_wandb.py b/tests/test_trackers_wandb.py index c35562b..2461a8d 100644 --- a/tests/test_trackers_wandb.py +++ b/tests/test_trackers_wandb.py @@ -154,8 +154,8 @@ def test_wandb_adapter_init_passes_core_kwargs(fake_wandb, installed_repo: Path) assert kw["project"] == "proj-x" assert kw["group"] == "H012/E018/full" assert "E018" in kw["tags"] and "H012" in kw["tags"] - # Config carries the full Limina chain + sp - assert kw["config"]["limina"]["experiment_id"] == "E018" + # Config carries the full run-link chain + sp + assert kw["config"]["aexp"]["experiment_id"] == "E018" assert kw["config"]["condition"] == "full" diff --git a/tests/test_utils.py b/tests/test_utils.py index 5ca9890..97ed4a6 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -143,13 +143,13 @@ def test_write_then_read_installed_marker(tmp_path: Path) -> None: tmp_path, version="0.1.0", run_store_path=".runs", - limina_vendor_sha="deadbeef", + vendor_sha="deadbeef", ) marker = read_installed_marker(tmp_path) assert marker is not None assert marker["version"] == "0.1.0" assert marker["run_store_path"] == ".runs" - assert marker["limina_vendor_sha"] == "deadbeef" + assert marker["vendor_sha"] == "deadbeef" assert "installed_at" in marker # Cross-platform invocation fields captured by default. assert "python_exe" in marker @@ -162,7 +162,7 @@ def test_write_installed_marker_honors_explicit_python_exe(tmp_path: Path) -> No tmp_path, version="0.1.0", run_store_path=".runs", - limina_vendor_sha="x", + vendor_sha="x", python_exe="/custom/python", conda_env_name="custom-env", ) @@ -177,7 +177,7 @@ def test_resolve_invocation_uses_conda_run_when_env_name_present(tmp_path: Path) tmp_path, version="0.1.0", run_store_path=".runs", - limina_vendor_sha="x", + vendor_sha="x", python_exe="/opt/miniforge3/envs/agentic-exp/bin/python", conda_env_name="agentic-exp", ) @@ -191,7 +191,7 @@ def test_resolve_invocation_falls_back_to_python_exe_when_venv(tmp_path: Path) - tmp_path, version="0.1.0", run_store_path=".runs", - limina_vendor_sha="x", + vendor_sha="x", python_exe="/home/u/.venv/bin/python", conda_env_name="", # venv, not conda ) @@ -259,7 +259,7 @@ def test_resolve_run_store_path_uses_marker(tmp_path: Path) -> None: tmp_path, version="0.1.0", run_store_path="custom/runs", - limina_vendor_sha="x", + vendor_sha="x", ) assert resolve_run_store_path(tmp_path) == (tmp_path / "custom" / "runs").resolve() @@ -271,7 +271,7 @@ def test_resolve_run_store_path_defaults(tmp_path: Path) -> None: def test_installed_marker_is_valid_json_with_trailing_newline(tmp_path: Path) -> None: write_installed_marker( - tmp_path, version="0.1.0", run_store_path=".runs", limina_vendor_sha="x" + tmp_path, version="0.1.0", run_store_path=".runs", vendor_sha="x" ) raw = (tmp_path / INSTALLED_MARKER_REL).read_text(encoding="utf-8") assert raw.endswith("\n") diff --git a/tests/test_validate.py b/tests/test_validate.py index 8b1ccc2..ec0a417 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -144,7 +144,7 @@ def test_validate_flags_missing_template_header_on_experiment( Calls ``kb_validate.validate_kb`` directly because the per-check codes (``missing_template_header`` etc.) live there; ``validate_repo`` flattens - the inner kb-validate output into a single ``limina.validation_failed`` + the inner kb-validate output into a single ``aexp.validation_failed`` issue with the formatted text bundled into the message. """ from aexp.kb_validate import validate_kb @@ -205,7 +205,7 @@ def test_validate_repo_surfaces_missing_template_header_in_bundled_message( installed_repo: Path, ) -> None: """The outer ``validate_repo`` doesn't preserve per-check codes, but its - bundled ``limina.validation_failed`` message must still mention the + bundled ``aexp.validation_failed`` message must still mention the missing header so users see what to fix.""" kb = installed_repo / "kb" _write_artifact( @@ -248,7 +248,7 @@ def test_validate_repo_surfaces_missing_template_header_in_bundled_message( result = validate_repo(installed_repo) assert not result.ok bundled = next( - i for i in result.errors if i.code == "limina.validation_failed" + i for i in result.errors if i.code == "aexp.validation_failed" ) assert "Caveats" in bundled.message assert "missing_template_header" in bundled.message @@ -294,7 +294,7 @@ def test_validate_surfaces_kb_validate_errors(installed_repo: Path) -> None: bad.write_text("not a valid artifact\n", encoding="utf-8") result = validate_repo(installed_repo) codes = [i.code for i in result.errors] - assert "limina.validation_failed" in codes + assert "aexp.validation_failed" in codes # --------------------------------------------------------------------------- @@ -309,7 +309,7 @@ def test_validate_flags_orphan_run(installed_repo: Path) -> None: statepoint={"c": "f"}, repo_root=installed_repo, ) - del job.doc["limina"] + del job.doc["aexp"] result = validate_repo(installed_repo, mode="runs-only") codes = [i.code for i in result.errors] @@ -591,7 +591,7 @@ def test_mode_kb_only_skips_run_checks(installed_repo: Path) -> None: job = create_run( experiment_id="E001", statepoint={"c": "f"}, repo_root=installed_repo ) - del job.doc["limina"] + del job.doc["aexp"] result = validate_repo(installed_repo, mode="kb-only") # Might still fail on kb_validate (e.g. if the orphan kb/ wasn't touched), # but no run.* codes should appear. @@ -605,4 +605,4 @@ def test_mode_runs_only_skips_kb_validate(installed_repo: Path) -> None: bad.write_text("garbage\n", encoding="utf-8") result = validate_repo(installed_repo, mode="runs-only") codes = [i.code for i in result.issues] - assert "limina.validation_failed" not in codes + assert "aexp.validation_failed" not in codes From 2d77fb555bfb8ac821d09450487b40c65b2e617e Mon Sep 17 00:00:00 2001 From: Kaden McKeen Date: Wed, 20 May 2026 23:10:32 -0400 Subject: [PATCH 03/10] =?UTF-8?q?refactor(debrand):=20stage=20C=20?= =?UTF-8?q?=E2=80=94=20rename=20the=20"limina"-branded=20code=20identifier?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename the public-API and internal identifiers that carried the "limina" name so the package surface reads as plain aexp: - install_limina() -> install_scaffold() - is_limina_installed() -> is_scaffold_installed() - VENDOR_LIMINA -> VENDOR_ROOT - LiminaArtifactRef -> ArtifactRef - module aexp.limina_io -> aexp.kb_io (sits beside aexp.kb_validate) - conftest fixtures limina_project -> scaffold_project, vendored_limina_tree -> vendored_tree aexp/limina_io.py is kept as a thin deprecation shim (re-exports aexp.kb_io, emits a DeprecationWarning) for one release -- it covers the single external import (an electricrag notebook). All in-package imports, __init__.py __all__, and the tests move to the new names. The vendored directory src/aexp/vendor/limina/ keeps its name (honest provenance for a vendored snapshot). Part of the limina de-brand (one PR, four staged commits A-D). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/aexp/__init__.py | 16 +- src/aexp/artifacts.py | 4 +- src/aexp/cli.py | 8 +- src/aexp/hooks/__init__.py | 2 +- src/aexp/install.py | 22 +- src/aexp/kb_io.py | 261 ++++++++++++++++++++ src/aexp/limina_io.py | 268 +-------------------- src/aexp/mcp_server.py | 6 +- src/aexp/queue.py | 2 +- src/aexp/runs.py | 2 +- src/aexp/schema.py | 10 +- src/aexp/trackers/base.py | 2 +- src/aexp/utils/paths.py | 2 +- src/aexp/validate.py | 6 +- tests/conftest.py | 14 +- tests/test_artifacts.py | 4 +- tests/test_cli.py | 6 +- tests/test_hooks_python.py | 68 +++--- tests/test_install.py | 116 ++++----- tests/{test_limina_io.py => test_kb_io.py} | 12 +- tests/test_linking.py | 4 +- tests/test_mcp_server.py | 4 +- tests/test_queue.py | 6 +- tests/test_runs.py | 6 +- tests/test_schema.py | 6 +- tests/test_trackers_context.py | 4 +- tests/test_trackers_noop.py | 4 +- tests/test_trackers_wandb.py | 4 +- tests/test_validate.py | 4 +- 29 files changed, 446 insertions(+), 427 deletions(-) create mode 100644 src/aexp/kb_io.py rename tests/{test_limina_io.py => test_kb_io.py} (96%) diff --git a/src/aexp/__init__.py b/src/aexp/__init__.py index edc68a8..d403222 100644 --- a/src/aexp/__init__.py +++ b/src/aexp/__init__.py @@ -33,12 +33,12 @@ InstallAction, InstallRefused, compute_vendor_sha, - install_limina, - is_limina_installed, + install_scaffold, + is_scaffold_installed, ) # Limina readers ------------------------------------------------------------ -from aexp.limina_io import ( +from aexp.kb_io import ( ArtifactNotFoundError, ArtifactReadError, list_kb_artifacts, @@ -94,10 +94,10 @@ # Schema / types ------------------------------------------------------------ from aexp.schema import ( + ArtifactRef, BatchSelector, BatchSummary, Issue, - LiminaArtifactRef, MaterializeResult, QueueEntry, RunLink, @@ -145,8 +145,8 @@ def __getattr__(name: str): "InstallAction", "InstallRefused", "compute_vendor_sha", - "install_limina", - "is_limina_installed", + "install_scaffold", + "is_scaffold_installed", # artifacts (H/E/F/T creation + backlink patching + thread lifecycle) "ArtifactCreateError", "ArtifactCreateResult", @@ -191,7 +191,7 @@ def __getattr__(name: str): "run_queue", "run_queued", "stop_queued", - # limina_io + # kb_io "ArtifactNotFoundError", "ArtifactReadError", "list_kb_artifacts", @@ -204,7 +204,7 @@ def __getattr__(name: str): "BatchSelector", "BatchSummary", "Issue", - "LiminaArtifactRef", + "ArtifactRef", "MaterializeResult", "QueueEntry", "RunLink", diff --git a/src/aexp/artifacts.py b/src/aexp/artifacts.py index a11f89e..b22eb29 100644 --- a/src/aexp/artifacts.py +++ b/src/aexp/artifacts.py @@ -1,6 +1,6 @@ """Create Limina ``kb/`` artifacts (H/E/F) with bidirectional backlinks. -This is the v1.1 surface flagged as planned in ``aexp.limina_io``: rather +This is the v1.1 surface flagged as planned in ``aexp.kb_io``: rather than hand-rolling markdown from templates per slash-command invocation, agents call :func:`new_hypothesis` / :func:`new_experiment` / :func:`new_finding` and get a validator-clean file on disk plus every @@ -29,7 +29,7 @@ from pathlib import Path from aexp.backlinks import add_backlink -from aexp.limina_io import ( +from aexp.kb_io import ( ArtifactNotFoundError, find_artifact_path, kind_dir, diff --git a/src/aexp/cli.py b/src/aexp/cli.py index f10c600..6ac3ada 100644 --- a/src/aexp/cli.py +++ b/src/aexp/cli.py @@ -25,8 +25,8 @@ new_hypothesis, new_thread, ) -from aexp.install import InstallRefused, install_limina -from aexp.limina_io import ( +from aexp.install import InstallRefused, install_scaffold +from aexp.kb_io import ( ArtifactNotFoundError, list_kb_artifacts, load_thread, @@ -285,7 +285,7 @@ def install( if dry_run: console.print(_INSTALL_HEADS_UP) try: - actions = install_limina( + actions = install_scaffold( cwd, run_store=run_store, force=force, @@ -313,7 +313,7 @@ def install( raise typer.Exit(code=1) try: - actions = install_limina( + actions = install_scaffold( cwd, run_store=run_store, force=force, diff --git a/src/aexp/hooks/__init__.py b/src/aexp/hooks/__init__.py index b6e48be..0944e65 100644 --- a/src/aexp/hooks/__init__.py +++ b/src/aexp/hooks/__init__.py @@ -1,7 +1,7 @@ """Claude Code hooks — shipped inside the ``aexp`` package. Each hook is a small ``python -m aexp.hooks.`` entry point referenced -from the ``.claude/settings.json`` that :func:`aexp.install.install_limina` +from the ``.claude/settings.json`` that :func:`aexp.install.install_scaffold` writes into a consumer repo. The hook scripts **do not** get copied into the repo — they live here and upgrade with ``pip install -U agentic-experiments``. diff --git a/src/aexp/install.py b/src/aexp/install.py index 4c8cb36..285131d 100644 --- a/src/aexp/install.py +++ b/src/aexp/install.py @@ -1,6 +1,6 @@ """Install the vendored Limina harness into a consumer repo. -``install_limina`` walks the vendored snapshot at +``install_scaffold`` walks the vendored snapshot at ``src/aexp/vendor/limina/`` and applies it to a target repo: - ``kb/``, ``templates/``, ``scripts/`` -> copied verbatim (skipped if the @@ -37,7 +37,7 @@ class InstallRefused(RuntimeError): - """Raised by :func:`install_limina` when install must not proceed. + """Raised by :func:`install_scaffold` when install must not proceed. Currently only raised by the source-tree-self-install guard (refusing to install a consumer-side scaffold inside the agentic-experiments @@ -87,7 +87,7 @@ def _find_aexp_source_tree(start: Path) -> Path | None: return None cur = cur.parent -VENDOR_LIMINA = Path(__file__).resolve().parent / "vendor" / "limina" +VENDOR_ROOT = Path(__file__).resolve().parent / "vendor" / "limina" # Subdirectories of the vendor tree that get copied verbatim into the consumer repo. # @@ -124,7 +124,7 @@ def _find_aexp_source_tree(start: Path) -> Path | None: @dataclass(frozen=True) class InstallAction: - """A single side-effect recorded by ``install_limina``.""" + """A single side-effect recorded by ``install_scaffold``.""" kind: ActionKind path: str # relative to repo root @@ -136,7 +136,7 @@ class InstallAction: # --------------------------------------------------------------------------- -def compute_vendor_sha(vendor_root: Path = VENDOR_LIMINA) -> str: +def compute_vendor_sha(vendor_root: Path = VENDOR_ROOT) -> str: """Compute a deterministic hash of every file under ``vendor_root``. Files are sorted by POSIX-style relative path, then hashed as @@ -686,7 +686,7 @@ def _install_skills(root: Path, *, force: bool, dry_run: bool = False) -> list[I actions: list[InstallAction] = [] dst_skills = root / ".claude" / "skills" - skills_src = VENDOR_LIMINA / "skills" + skills_src = VENDOR_ROOT / "skills" if skills_src.is_dir(): for skill_dir in sorted(p for p in skills_src.iterdir() if p.is_dir()): dst = dst_skills / skill_dir.name @@ -765,7 +765,7 @@ def _copy_tree( # --------------------------------------------------------------------------- -def install_limina( +def install_scaffold( repo_root: str | Path, *, run_store: str = ".runs", @@ -920,7 +920,7 @@ def install_limina( for name in _TREES_VERBATIM: actions.extend( _copy_tree( - VENDOR_LIMINA / name, + VENDOR_ROOT / name, root / name, force=force, dry_run=dry_run, @@ -930,7 +930,7 @@ def install_limina( # 2. Copy / block-merge top-level Markdown docs. for name in _MERGE_FILES: - src = VENDOR_LIMINA / name + src = VENDOR_ROOT / name if not src.is_file(): continue actions.append(_merge_or_copy_markdown(src, root / name, dry_run=dry_run)) @@ -946,7 +946,7 @@ def install_limina( # Copied unconditionally (not gated on --with-jupyter): the doc is # small, harmless, and lets a consumer read about the integration # before deciding to opt in. - jupyter_doc_src = VENDOR_LIMINA / "docs" / "setup" / "jupyter-mcp.md" + jupyter_doc_src = VENDOR_ROOT / "docs" / "setup" / "jupyter-mcp.md" if jupyter_doc_src.is_file(): actions.append( _copy_file( @@ -1033,7 +1033,7 @@ def _ensure_signac_project(path: Path) -> signac.Project: return signac.init_project(path=str(path)) -def is_limina_installed(repo_root: str | Path) -> bool: +def is_scaffold_installed(repo_root: str | Path) -> bool: """True if ``repo_root`` has an install marker AND the expected tree shape. We check for ``kb/`` and ``.claude/settings.json`` — the two consumer-repo diff --git a/src/aexp/kb_io.py b/src/aexp/kb_io.py new file mode 100644 index 0000000..c67be5d --- /dev/null +++ b/src/aexp/kb_io.py @@ -0,0 +1,261 @@ +"""Typed read wrappers over Limina ``kb/`` artifacts. + +Reads parse the YAML frontmatter with ``python-frontmatter`` and return a +:class:`~aexp.schema.ArtifactRef`. Everything here is read-only; +artifact creation happens by writing from ``templates/`` (v1) or via the +planned ``aexp new-hypothesis`` / ``new-experiment`` / ``new-finding`` +CLI verbs (v1.1). +""" +from __future__ import annotations + +import re +from pathlib import Path +from typing import Any + +import frontmatter + +from aexp.schema import ArtifactKind, ArtifactRef + +# --------------------------------------------------------------------------- +# Layout constants (mirror kb_validate.CORE_ARTIFACTS) +# --------------------------------------------------------------------------- + +_KIND_DIRS: dict[ArtifactKind, Path] = { + "H": Path("research") / "hypotheses", + "E": Path("research") / "experiments", + "F": Path("research") / "findings", + "L": Path("research") / "literature", + "CR": Path("reports"), + "SR": Path("reports"), + "T": Path("research") / "threads", +} + +_ID_RE = re.compile(r"^(CR|SR|H|E|F|L|T)(\d{3})$") +_FILENAME_ID_RE = re.compile(r"^(CR|SR|H|E|F|L|T)(\d{3})-") +_H1_RE = re.compile(r"^#\s+(.*?)\s*$", re.MULTILINE) + + +class ArtifactNotFoundError(FileNotFoundError): + """No file matching an artifact id was found under the expected directory.""" + + +class ArtifactReadError(RuntimeError): + """An artifact file existed but could not be parsed (YAML error, missing id, etc.).""" + + +# --------------------------------------------------------------------------- +# ID / path helpers +# --------------------------------------------------------------------------- + + +def parse_artifact_id(artifact_id: str) -> tuple[ArtifactKind, int]: + """Split an artifact id like ``"E018"`` into ``("E", 18)``. + + Raises ``ValueError`` if ``artifact_id`` does not match ``^(CR|SR|H|E|F|L|T)\\d{3}$``. + """ + m = _ID_RE.match(artifact_id) + if not m: + raise ValueError( + f"invalid artifact id {artifact_id!r}; expected e.g. 'H001', 'E018', 'F007'" + ) + kind = m.group(1) + number = int(m.group(2)) + return kind, number # type: ignore[return-value] + + +def kind_dir(kind: ArtifactKind, kb_root: Path) -> Path: + """Return the directory under ``kb_root`` that stores artifacts of ``kind``.""" + if kind not in _KIND_DIRS: + raise ValueError(f"unknown artifact kind: {kind!r}") + return kb_root / _KIND_DIRS[kind] + + +def find_artifact_path(artifact_id: str, *, kb_root: Path) -> Path: + """Return the on-disk path of the artifact with id ``artifact_id``. + + Matches the filename pattern ``-*.md`` that ``kb_new_artifact.py`` + produces. + + Raises + ------ + ArtifactNotFoundError + If no matching file exists. + """ + kind, _ = parse_artifact_id(artifact_id) + directory = kind_dir(kind, kb_root) + if not directory.is_dir(): + raise ArtifactNotFoundError( + f"expected directory {directory} to exist for kind {kind!r}" + ) + matches = sorted(directory.glob(f"{artifact_id}-*.md")) + if not matches: + raise ArtifactNotFoundError( + f"no artifact file matching {artifact_id}-*.md under {directory}" + ) + if len(matches) > 1: + # Limina requires one file per id; kb_validate flags duplicates. Be + # conservative here and raise rather than pick silently. + raise ArtifactReadError( + f"multiple files match {artifact_id}-*.md under {directory}: " + + ", ".join(p.name for p in matches) + ) + return matches[0] + + +# --------------------------------------------------------------------------- +# Reading +# --------------------------------------------------------------------------- + + +def _extract_title(body: str, fallback: str) -> str: + """Pull the first ``# `` line from ``body``; fall back if absent.""" + m = _H1_RE.search(body) + if not m: + return fallback + raw = m.group(1).strip() + # Limina uses "{ID} — {Title}"; strip the leading id if present. + for sep in (" — ", " - ", "— ", "- "): + if sep in raw: + _id, _, rest = raw.partition(sep) + if _FILENAME_ID_RE.match(_id + "-"): + return rest.strip() or raw + return raw + + +def _load_artifact(path: Path, kb_root: Path) -> ArtifactRef: + """Parse a Limina markdown artifact file into a typed reference.""" + try: + text = path.read_text(encoding="utf-8") + except OSError as exc: + raise ArtifactReadError(f"could not read {path}: {exc}") from exc + + try: + post = frontmatter.loads(text) + except Exception as exc: # frontmatter can raise many exception types + raise ArtifactReadError(f"could not parse frontmatter in {path}: {exc}") from exc + + metadata: dict[str, Any] = dict(post.metadata) + body = post.content + + # Derive id: prefer frontmatter, fall back to filename parse. + fm_id = str(metadata.get("id", "")).strip() + artifact_id: str + if _ID_RE.match(fm_id): + artifact_id = fm_id + else: + fname_match = _FILENAME_ID_RE.match(path.name) + if fname_match: + artifact_id = f"{fname_match.group(1)}{fname_match.group(2)}" + else: + raise ArtifactReadError( + f"cannot determine artifact id for {path}: frontmatter id={fm_id!r}, " + "and filename does not start with <PREFIX><NUMBER>-" + ) + + kind, _ = parse_artifact_id(artifact_id) + + # Relative path (POSIX) for display. + try: + rel = path.resolve().relative_to(kb_root.resolve().parent).as_posix() + except ValueError: + rel = path.as_posix() + + title = _extract_title(body, fallback=artifact_id) + + return ArtifactRef( + kind=kind, + id=artifact_id, + path=rel, + title=title, + metadata=metadata, + body=body, + ) + + +def load_artifact(artifact_id: str, *, kb_root: Path) -> ArtifactRef: + """Load any artifact by id (H/E/F/L/CR/SR).""" + path = find_artifact_path(artifact_id, kb_root=kb_root) + return _load_artifact(path, kb_root=kb_root) + + +def load_hypothesis(artifact_id: str, *, kb_root: Path) -> ArtifactRef: + """Load a hypothesis artifact, asserting the kind for callers that care.""" + ref = load_artifact(artifact_id, kb_root=kb_root) + if ref.kind != "H": + raise ArtifactReadError(f"{artifact_id} is {ref.kind}, not H") + return ref + + +def load_experiment(artifact_id: str, *, kb_root: Path) -> ArtifactRef: + """Load an experiment artifact, asserting the kind.""" + ref = load_artifact(artifact_id, kb_root=kb_root) + if ref.kind != "E": + raise ArtifactReadError(f"{artifact_id} is {ref.kind}, not E") + return ref + + +def load_finding(artifact_id: str, *, kb_root: Path) -> ArtifactRef: + """Load a finding artifact, asserting the kind.""" + ref = load_artifact(artifact_id, kb_root=kb_root) + if ref.kind != "F": + raise ArtifactReadError(f"{artifact_id} is {ref.kind}, not F") + return ref + + +def load_thread(artifact_id: str, *, kb_root: Path) -> ArtifactRef: + """Load a thread artifact, asserting the kind.""" + ref = load_artifact(artifact_id, kb_root=kb_root) + if ref.kind != "T": + raise ArtifactReadError(f"{artifact_id} is {ref.kind}, not T") + return ref + + +def list_kb_artifacts( + kb_root: Path, + *, + kind: ArtifactKind | None = None, +) -> list[ArtifactRef]: + """Walk ``kb_root`` and load every artifact of the given kind(s). + + Parameters + ---------- + kb_root : Path + Root of the ``kb/`` tree. + kind : ArtifactKind | None + If given, only return artifacts of that kind. Default: all kinds. + """ + kinds: list[ArtifactKind] = [kind] if kind else list(_KIND_DIRS.keys()) + results: list[ArtifactRef] = [] + for k in kinds: + directory = kind_dir(k, kb_root) + if not directory.is_dir(): + continue + # reports/ is shared between CR and SR; filter by filename prefix. + for p in sorted(directory.glob("*.md")): + if not _FILENAME_ID_RE.match(p.name): + continue + # When walking reports/ for both CR and SR, only pick files whose + # filename id matches the current kind. + if directory == kind_dir(k, kb_root) and not p.name.startswith(f"{k}"): + continue + try: + results.append(_load_artifact(p, kb_root=kb_root)) + except ArtifactReadError: + # Malformed artifact — skip silently; kb_validate surfaces it. + continue + return results + + +__all__ = [ + "ArtifactNotFoundError", + "ArtifactReadError", + "find_artifact_path", + "kind_dir", + "list_kb_artifacts", + "load_artifact", + "load_experiment", + "load_finding", + "load_hypothesis", + "load_thread", + "parse_artifact_id", +] diff --git a/src/aexp/limina_io.py b/src/aexp/limina_io.py index 9d7db6e..3ecc004 100644 --- a/src/aexp/limina_io.py +++ b/src/aexp/limina_io.py @@ -1,261 +1,19 @@ -"""Typed read wrappers over Limina ``kb/`` artifacts. +"""Deprecated alias for :mod:`aexp.kb_io`. -Reads parse the YAML frontmatter with ``python-frontmatter`` and return a -:class:`~aexp.schema.LiminaArtifactRef`. Everything here is read-only; -artifact creation happens by writing from ``templates/`` (v1) or via the -planned ``aexp new-hypothesis`` / ``new-experiment`` / ``new-finding`` -CLI verbs (v1.1). +``aexp.limina_io`` was renamed to :mod:`aexp.kb_io` during the limina +de-brand. This shim re-exports the new module so existing imports keep +working; importing it emits a :class:`DeprecationWarning`. It will be +removed in a future release -- migrate to ``from aexp.kb_io import ...``. """ from __future__ import annotations -import re -from pathlib import Path -from typing import Any +import warnings as _warnings -import frontmatter +from aexp.kb_io import * # noqa: F401,F403 (back-compat re-export) -from aexp.schema import ArtifactKind, LiminaArtifactRef - -# --------------------------------------------------------------------------- -# Layout constants (mirror kb_validate.CORE_ARTIFACTS) -# --------------------------------------------------------------------------- - -_KIND_DIRS: dict[ArtifactKind, Path] = { - "H": Path("research") / "hypotheses", - "E": Path("research") / "experiments", - "F": Path("research") / "findings", - "L": Path("research") / "literature", - "CR": Path("reports"), - "SR": Path("reports"), - "T": Path("research") / "threads", -} - -_ID_RE = re.compile(r"^(CR|SR|H|E|F|L|T)(\d{3})$") -_FILENAME_ID_RE = re.compile(r"^(CR|SR|H|E|F|L|T)(\d{3})-") -_H1_RE = re.compile(r"^#\s+(.*?)\s*$", re.MULTILINE) - - -class ArtifactNotFoundError(FileNotFoundError): - """No file matching an artifact id was found under the expected directory.""" - - -class ArtifactReadError(RuntimeError): - """An artifact file existed but could not be parsed (YAML error, missing id, etc.).""" - - -# --------------------------------------------------------------------------- -# ID / path helpers -# --------------------------------------------------------------------------- - - -def parse_artifact_id(artifact_id: str) -> tuple[ArtifactKind, int]: - """Split an artifact id like ``"E018"`` into ``("E", 18)``. - - Raises ``ValueError`` if ``artifact_id`` does not match ``^(CR|SR|H|E|F|L|T)\\d{3}$``. - """ - m = _ID_RE.match(artifact_id) - if not m: - raise ValueError( - f"invalid artifact id {artifact_id!r}; expected e.g. 'H001', 'E018', 'F007'" - ) - kind = m.group(1) - number = int(m.group(2)) - return kind, number # type: ignore[return-value] - - -def kind_dir(kind: ArtifactKind, kb_root: Path) -> Path: - """Return the directory under ``kb_root`` that stores artifacts of ``kind``.""" - if kind not in _KIND_DIRS: - raise ValueError(f"unknown artifact kind: {kind!r}") - return kb_root / _KIND_DIRS[kind] - - -def find_artifact_path(artifact_id: str, *, kb_root: Path) -> Path: - """Return the on-disk path of the artifact with id ``artifact_id``. - - Matches the filename pattern ``<id>-*.md`` that ``kb_new_artifact.py`` - produces. - - Raises - ------ - ArtifactNotFoundError - If no matching file exists. - """ - kind, _ = parse_artifact_id(artifact_id) - directory = kind_dir(kind, kb_root) - if not directory.is_dir(): - raise ArtifactNotFoundError( - f"expected directory {directory} to exist for kind {kind!r}" - ) - matches = sorted(directory.glob(f"{artifact_id}-*.md")) - if not matches: - raise ArtifactNotFoundError( - f"no artifact file matching {artifact_id}-*.md under {directory}" - ) - if len(matches) > 1: - # Limina requires one file per id; kb_validate flags duplicates. Be - # conservative here and raise rather than pick silently. - raise ArtifactReadError( - f"multiple files match {artifact_id}-*.md under {directory}: " - + ", ".join(p.name for p in matches) - ) - return matches[0] - - -# --------------------------------------------------------------------------- -# Reading -# --------------------------------------------------------------------------- - - -def _extract_title(body: str, fallback: str) -> str: - """Pull the first ``# <title>`` line from ``body``; fall back if absent.""" - m = _H1_RE.search(body) - if not m: - return fallback - raw = m.group(1).strip() - # Limina uses "{ID} — {Title}"; strip the leading id if present. - for sep in (" — ", " - ", "— ", "- "): - if sep in raw: - _id, _, rest = raw.partition(sep) - if _FILENAME_ID_RE.match(_id + "-"): - return rest.strip() or raw - return raw - - -def _load_artifact(path: Path, kb_root: Path) -> LiminaArtifactRef: - """Parse a Limina markdown artifact file into a typed reference.""" - try: - text = path.read_text(encoding="utf-8") - except OSError as exc: - raise ArtifactReadError(f"could not read {path}: {exc}") from exc - - try: - post = frontmatter.loads(text) - except Exception as exc: # frontmatter can raise many exception types - raise ArtifactReadError(f"could not parse frontmatter in {path}: {exc}") from exc - - metadata: dict[str, Any] = dict(post.metadata) - body = post.content - - # Derive id: prefer frontmatter, fall back to filename parse. - fm_id = str(metadata.get("id", "")).strip() - artifact_id: str - if _ID_RE.match(fm_id): - artifact_id = fm_id - else: - fname_match = _FILENAME_ID_RE.match(path.name) - if fname_match: - artifact_id = f"{fname_match.group(1)}{fname_match.group(2)}" - else: - raise ArtifactReadError( - f"cannot determine artifact id for {path}: frontmatter id={fm_id!r}, " - "and filename does not start with <PREFIX><NUMBER>-" - ) - - kind, _ = parse_artifact_id(artifact_id) - - # Relative path (POSIX) for display. - try: - rel = path.resolve().relative_to(kb_root.resolve().parent).as_posix() - except ValueError: - rel = path.as_posix() - - title = _extract_title(body, fallback=artifact_id) - - return LiminaArtifactRef( - kind=kind, - id=artifact_id, - path=rel, - title=title, - metadata=metadata, - body=body, - ) - - -def load_artifact(artifact_id: str, *, kb_root: Path) -> LiminaArtifactRef: - """Load any artifact by id (H/E/F/L/CR/SR).""" - path = find_artifact_path(artifact_id, kb_root=kb_root) - return _load_artifact(path, kb_root=kb_root) - - -def load_hypothesis(artifact_id: str, *, kb_root: Path) -> LiminaArtifactRef: - """Load a hypothesis artifact, asserting the kind for callers that care.""" - ref = load_artifact(artifact_id, kb_root=kb_root) - if ref.kind != "H": - raise ArtifactReadError(f"{artifact_id} is {ref.kind}, not H") - return ref - - -def load_experiment(artifact_id: str, *, kb_root: Path) -> LiminaArtifactRef: - """Load an experiment artifact, asserting the kind.""" - ref = load_artifact(artifact_id, kb_root=kb_root) - if ref.kind != "E": - raise ArtifactReadError(f"{artifact_id} is {ref.kind}, not E") - return ref - - -def load_finding(artifact_id: str, *, kb_root: Path) -> LiminaArtifactRef: - """Load a finding artifact, asserting the kind.""" - ref = load_artifact(artifact_id, kb_root=kb_root) - if ref.kind != "F": - raise ArtifactReadError(f"{artifact_id} is {ref.kind}, not F") - return ref - - -def load_thread(artifact_id: str, *, kb_root: Path) -> LiminaArtifactRef: - """Load a thread artifact, asserting the kind.""" - ref = load_artifact(artifact_id, kb_root=kb_root) - if ref.kind != "T": - raise ArtifactReadError(f"{artifact_id} is {ref.kind}, not T") - return ref - - -def list_kb_artifacts( - kb_root: Path, - *, - kind: ArtifactKind | None = None, -) -> list[LiminaArtifactRef]: - """Walk ``kb_root`` and load every artifact of the given kind(s). - - Parameters - ---------- - kb_root : Path - Root of the ``kb/`` tree. - kind : ArtifactKind | None - If given, only return artifacts of that kind. Default: all kinds. - """ - kinds: list[ArtifactKind] = [kind] if kind else list(_KIND_DIRS.keys()) - results: list[LiminaArtifactRef] = [] - for k in kinds: - directory = kind_dir(k, kb_root) - if not directory.is_dir(): - continue - # reports/ is shared between CR and SR; filter by filename prefix. - for p in sorted(directory.glob("*.md")): - if not _FILENAME_ID_RE.match(p.name): - continue - # When walking reports/ for both CR and SR, only pick files whose - # filename id matches the current kind. - if directory == kind_dir(k, kb_root) and not p.name.startswith(f"{k}"): - continue - try: - results.append(_load_artifact(p, kb_root=kb_root)) - except ArtifactReadError: - # Malformed artifact — skip silently; kb_validate surfaces it. - continue - return results - - -__all__ = [ - "ArtifactNotFoundError", - "ArtifactReadError", - "find_artifact_path", - "kind_dir", - "list_kb_artifacts", - "load_artifact", - "load_experiment", - "load_finding", - "load_hypothesis", - "load_thread", - "parse_artifact_id", -] +_warnings.warn( + "aexp.limina_io has been renamed to aexp.kb_io; " + "update imports to `from aexp.kb_io import ...`.", + DeprecationWarning, + stacklevel=2, +) diff --git a/src/aexp/mcp_server.py b/src/aexp/mcp_server.py index 957f5a7..7682cb0 100644 --- a/src/aexp/mcp_server.py +++ b/src/aexp/mcp_server.py @@ -44,13 +44,13 @@ from aexp.artifacts import ( new_thread as _new_thread, ) -from aexp.limina_io import ( +from aexp.kb_io import ( ArtifactNotFoundError as _ArtifactNotFoundError, ) -from aexp.limina_io import ( +from aexp.kb_io import ( list_kb_artifacts as _list_kb_artifacts, ) -from aexp.limina_io import ( +from aexp.kb_io import ( load_thread as _load_thread, ) from aexp.linking import ( diff --git a/src/aexp/queue.py b/src/aexp/queue.py index fc2a972..b25dbc6 100644 --- a/src/aexp/queue.py +++ b/src/aexp/queue.py @@ -42,7 +42,7 @@ import signac -from aexp.limina_io import ArtifactNotFoundError, load_experiment +from aexp.kb_io import ArtifactNotFoundError, load_experiment from aexp.runs import ( create_run, find_runs, diff --git a/src/aexp/runs.py b/src/aexp/runs.py index 19ecc57..3bbad06 100644 --- a/src/aexp/runs.py +++ b/src/aexp/runs.py @@ -46,7 +46,7 @@ class RunStoreNotInitialized(RuntimeError): """Raised when we cannot find an initialized signac project for this repo. - Usually means ``install_limina`` hasn't been run yet. + Usually means ``install_scaffold`` hasn't been run yet. """ diff --git a/src/aexp/schema.py b/src/aexp/schema.py index 3959c32..5d8e8d0 100644 --- a/src/aexp/schema.py +++ b/src/aexp/schema.py @@ -9,7 +9,7 @@ - ``RunLink``: the canonical shape of ``job.doc["aexp"]``. - ``SupportingRun`` / ``BatchSelector``: entries in a Finding's ``supporting_runs:`` frontmatter list (see plan §2, §8). -- ``LiminaArtifactRef``: typed handle returned by ``limina_io`` readers. +- ``ArtifactRef``: typed handle returned by ``kb_io`` readers. - ``RunSummary``: flat summary row for CLI ``list-runs`` / ``list-batches``. - ``Issue``: an actionable validator finding; the CLI prints these. """ @@ -172,15 +172,15 @@ class BatchSelector(BaseModel): # --------------------------------------------------------------------------- -# Limina artifact reference (read-only handle returned by limina_io) +# Limina artifact reference (read-only handle returned by kb_io) # --------------------------------------------------------------------------- @dataclass(frozen=True) -class LiminaArtifactRef: +class ArtifactRef: """A typed pointer to one Limina artifact on disk. - Returned by ``limina_io.load_*`` helpers. The raw frontmatter dict is + Returned by ``kb_io.load_*`` helpers. The raw frontmatter dict is exposed as ``metadata`` so callers can read fields we don't model. """ @@ -316,7 +316,7 @@ def iso_utc_now() -> str: "BatchSummary", "Issue", "IssueSeverity", - "LiminaArtifactRef", + "ArtifactRef", "MaterializeResult", "RUN_LINK_KEY", "QueueEntry", diff --git a/src/aexp/trackers/base.py b/src/aexp/trackers/base.py index ce6ce6e..3d5a9d8 100644 --- a/src/aexp/trackers/base.py +++ b/src/aexp/trackers/base.py @@ -17,7 +17,7 @@ import signac -from aexp.limina_io import ( +from aexp.kb_io import ( load_experiment, load_hypothesis, ) diff --git a/src/aexp/utils/paths.py b/src/aexp/utils/paths.py index c7d32b8..3c1f3df 100644 --- a/src/aexp/utils/paths.py +++ b/src/aexp/utils/paths.py @@ -26,7 +26,7 @@ class InstalledMarker(TypedDict, total=False): installed_at: str run_store_path: str vendor_sha: str - python_exe: str # absolute path to the Python that ran install_limina + python_exe: str # absolute path to the Python that ran install_scaffold conda_env_name: str # CONDA_DEFAULT_ENV at install time, or "" for venv/system Python jupyter_enabled: bool # True if any prior install used --with-jupyter; sticky once set diff --git a/src/aexp/validate.py b/src/aexp/validate.py index 9a19d7b..4771e9c 100644 --- a/src/aexp/validate.py +++ b/src/aexp/validate.py @@ -22,13 +22,13 @@ from pathlib import Path from typing import Any, Literal -from aexp.kb_validate import format_text as _kb_format_text -from aexp.kb_validate import validate_kb -from aexp.limina_io import ( +from aexp.kb_io import ( ArtifactNotFoundError, find_artifact_path, list_kb_artifacts, ) +from aexp.kb_validate import format_text as _kb_format_text +from aexp.kb_validate import validate_kb from aexp.linking import list_batches from aexp.runs import get_run_store from aexp.schema import Issue, RunStatus, read_run_link diff --git a/tests/conftest.py b/tests/conftest.py index cb68be4..aa929db 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,26 +8,26 @@ import pytest PACKAGE_ROOT = Path(__file__).resolve().parents[1] -VENDOR_LIMINA = PACKAGE_ROOT / "src" / "aexp" / "vendor" / "limina" +VENDOR_ROOT = PACKAGE_ROOT / "src" / "aexp" / "vendor" / "limina" @pytest.fixture -def vendored_limina_tree() -> Path: +def vendored_tree() -> Path: """Absolute path to the vendored Limina snapshot in this repo.""" - assert VENDOR_LIMINA.is_dir(), f"vendored Limina missing at {VENDOR_LIMINA}" - return VENDOR_LIMINA + assert VENDOR_ROOT.is_dir(), f"vendored Limina missing at {VENDOR_ROOT}" + return VENDOR_ROOT @pytest.fixture -def limina_project(tmp_path: Path) -> Path: +def scaffold_project(tmp_path: Path) -> Path: """Copy the vendored Limina snapshot into a tmp dir. Gives each test an isolated ``PROJECT_ROOT`` — the ported hooks derive their root from ``Path(__file__).resolve().parents[2]``, so running a copied hook sets ``PROJECT_ROOT`` to this tmp dir. """ - dest = tmp_path / "limina_project" - shutil.copytree(VENDOR_LIMINA, dest) + dest = tmp_path / "scaffold_project" + shutil.copytree(VENDOR_ROOT, dest) return dest diff --git a/tests/test_artifacts.py b/tests/test_artifacts.py index e66cf3c..d7f1709 100644 --- a/tests/test_artifacts.py +++ b/tests/test_artifacts.py @@ -18,7 +18,7 @@ slugify, ) from aexp.backlinks import add_backlink -from aexp.install import install_limina +from aexp.install import install_scaffold from aexp.kb_validate import validate_kb @@ -41,7 +41,7 @@ def installed_repo(tmp_path: Path) -> Path: repo = tmp_path / "repo" repo.mkdir() _git_commit(repo) - install_limina(repo) + install_scaffold(repo) return repo diff --git a/tests/test_cli.py b/tests/test_cli.py index e2843e0..a699d0e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,7 +8,7 @@ from typer.testing import CliRunner from aexp.cli import app -from aexp.install import install_limina +from aexp.install import install_scaffold def _git_commit(repo: Path) -> None: @@ -39,7 +39,7 @@ def installed_repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: repo = tmp_path / "repo" repo.mkdir() _git_commit(repo) - install_limina(repo) + install_scaffold(repo) monkeypatch.chdir(repo) return repo @@ -472,7 +472,7 @@ def _seed_hypothesis_and_experiment( if runner_command is not None: import frontmatter # type: ignore[import-not-found] - from aexp.limina_io import find_artifact_path + from aexp.kb_io import find_artifact_path exp_path = find_artifact_path("E001", kb_root=repo / "kb") post = frontmatter.load(str(exp_path)) diff --git a/tests/test_hooks_python.py b/tests/test_hooks_python.py index 203dc91..3b38328 100644 --- a/tests/test_hooks_python.py +++ b/tests/test_hooks_python.py @@ -8,7 +8,7 @@ - ``aexp.hooks.stop_validate`` full-KB validation. Strategy: hooks derive the repo root from ``os.getcwd()``. Each test -constructs a project directory (via the ``limina_project`` fixture) and +constructs a project directory (via the ``scaffold_project`` fixture) and invokes the hook as ``python -m aexp.hooks.<name>`` with ``cwd`` set to that directory. """ @@ -120,7 +120,7 @@ def test_parse_hook_input_invalid_json_yields_empty() -> None: # --------------------------------------------------------------------------- -def test_enforce_hef_allows_non_hef_path(limina_project: Path, python_exe: str) -> None: +def test_enforce_hef_allows_non_hef_path(scaffold_project: Path, python_exe: str) -> None: """Writes outside experiment/finding paths are not blocked.""" payload = { "tool_input": { @@ -128,12 +128,12 @@ def test_enforce_hef_allows_non_hef_path(limina_project: Path, python_exe: str) "content": "any content", } } - r = _run_hook(limina_project, "enforce_hef_chain", payload, python_exe) + r = _run_hook(scaffold_project, "enforce_hef_chain", payload, python_exe) assert r.returncode == 0, (r.returncode, r.stderr) def test_enforce_hef_blocks_experiment_missing_hypothesis_ref( - limina_project: Path, python_exe: str + scaffold_project: Path, python_exe: str ) -> None: """Experiment without any hypothesis reference is blocked.""" payload = { @@ -142,13 +142,13 @@ def test_enforce_hef_blocks_experiment_missing_hypothesis_ref( "content": "no reference here", } } - r = _run_hook(limina_project, "enforce_hef_chain", payload, python_exe) + r = _run_hook(scaffold_project, "enforce_hef_chain", payload, python_exe) assert r.returncode == 2 assert "without a hypothesis reference" in r.stderr def test_enforce_hef_blocks_experiment_referencing_missing_hypothesis( - limina_project: Path, python_exe: str + scaffold_project: Path, python_exe: str ) -> None: """Experiment with hypothesis ref but no matching hypothesis file is blocked.""" payload = { @@ -157,17 +157,17 @@ def test_enforce_hef_blocks_experiment_referencing_missing_hypothesis( "content": "> **Hypothesis**: H999\n", } } - r = _run_hook(limina_project, "enforce_hef_chain", payload, python_exe) + r = _run_hook(scaffold_project, "enforce_hef_chain", payload, python_exe) assert r.returncode == 2 assert "no hypothesis file found" in r.stderr def test_enforce_hef_allows_experiment_with_existing_hypothesis( - limina_project: Path, python_exe: str + scaffold_project: Path, python_exe: str ) -> None: """Experiment + valid hypothesis ref + matching hypothesis file -> allowed.""" - (limina_project / "kb" / "research" / "hypotheses").mkdir(parents=True, exist_ok=True) - (limina_project / "kb" / "research" / "hypotheses" / "H007-smoke.md").write_text( + (scaffold_project / "kb" / "research" / "hypotheses").mkdir(parents=True, exist_ok=True) + (scaffold_project / "kb" / "research" / "hypotheses" / "H007-smoke.md").write_text( "hypothesis body", encoding="utf-8" ) payload = { @@ -176,12 +176,12 @@ def test_enforce_hef_allows_experiment_with_existing_hypothesis( "content": "> **Hypothesis**: H007\n", } } - r = _run_hook(limina_project, "enforce_hef_chain", payload, python_exe) + r = _run_hook(scaffold_project, "enforce_hef_chain", payload, python_exe) assert r.returncode == 0, (r.returncode, r.stderr) def test_enforce_hef_blocks_finding_missing_experiment_ref( - limina_project: Path, python_exe: str + scaffold_project: Path, python_exe: str ) -> None: """Finding without any experiment reference is blocked.""" payload = { @@ -190,17 +190,17 @@ def test_enforce_hef_blocks_finding_missing_experiment_ref( "content": "bare finding body", } } - r = _run_hook(limina_project, "enforce_hef_chain", payload, python_exe) + r = _run_hook(scaffold_project, "enforce_hef_chain", payload, python_exe) assert r.returncode == 2 assert "without an experiment reference" in r.stderr def test_enforce_hef_allows_finding_with_existing_experiment( - limina_project: Path, python_exe: str + scaffold_project: Path, python_exe: str ) -> None: """Finding + valid experiment ref + matching file -> allowed.""" - (limina_project / "kb" / "research" / "experiments").mkdir(parents=True, exist_ok=True) - (limina_project / "kb" / "research" / "experiments" / "E042-bar.md").write_text( + (scaffold_project / "kb" / "research" / "experiments").mkdir(parents=True, exist_ok=True) + (scaffold_project / "kb" / "research" / "experiments" / "E042-bar.md").write_text( "experiment body", encoding="utf-8" ) payload = { @@ -209,16 +209,16 @@ def test_enforce_hef_allows_finding_with_existing_experiment( "content": "> **Experiment**: E042\n", } } - r = _run_hook(limina_project, "enforce_hef_chain", payload, python_exe) + r = _run_hook(scaffold_project, "enforce_hef_chain", payload, python_exe) assert r.returncode == 0, (r.returncode, r.stderr) def test_enforce_hef_accepts_frontmatter_hypothesis_key( - limina_project: Path, python_exe: str + scaffold_project: Path, python_exe: str ) -> None: """YAML frontmatter ``hypothesis: H###`` is picked up alongside blockquote metadata.""" - (limina_project / "kb" / "research" / "hypotheses").mkdir(parents=True, exist_ok=True) - (limina_project / "kb" / "research" / "hypotheses" / "H003-fm.md").write_text( + (scaffold_project / "kb" / "research" / "hypotheses").mkdir(parents=True, exist_ok=True) + (scaffold_project / "kb" / "research" / "hypotheses" / "H003-fm.md").write_text( "hypothesis body", encoding="utf-8" ) payload = { @@ -227,7 +227,7 @@ def test_enforce_hef_accepts_frontmatter_hypothesis_key( "content": "---\nhypothesis: \"H003\"\n---\n\nbody\n", } } - r = _run_hook(limina_project, "enforce_hef_chain", payload, python_exe) + r = _run_hook(scaffold_project, "enforce_hef_chain", payload, python_exe) assert r.returncode == 0, (r.returncode, r.stderr) @@ -247,18 +247,18 @@ def test_enforce_hef_accepts_frontmatter_hypothesis_key( ], ) def test_kb_write_guard_skips_non_guarded_paths( - limina_project: Path, python_exe: str, path: str + scaffold_project: Path, python_exe: str, path: str ) -> None: payload = {"tool_input": {"file_path": path, "content": "any"}} - r = _run_hook(limina_project, "kb_write_guard", payload, python_exe) + r = _run_hook(scaffold_project, "kb_write_guard", payload, python_exe) assert r.returncode == 0, (r.returncode, r.stdout, r.stderr) def test_kb_write_guard_blocks_invalid_md( - limina_project: Path, python_exe: str + scaffold_project: Path, python_exe: str ) -> None: """A malformed artifact under kb/research/ triggers kb_validate -> blocked.""" - target = limina_project / "kb" / "research" / "hypotheses" / "H050-bogus.md" + target = scaffold_project / "kb" / "research" / "hypotheses" / "H050-bogus.md" target.parent.mkdir(parents=True, exist_ok=True) target.write_text("this is not a valid Limina artifact", encoding="utf-8") @@ -268,7 +268,7 @@ def test_kb_write_guard_blocks_invalid_md( "content": "this is not a valid Limina artifact", } } - r = _run_hook(limina_project, "kb_write_guard", payload, python_exe) + r = _run_hook(scaffold_project, "kb_write_guard", payload, python_exe) assert r.returncode == 2, (r.returncode, r.stdout, r.stderr) assert "BLOCKED" in r.stderr @@ -279,32 +279,32 @@ def test_kb_write_guard_blocks_invalid_md( def test_stop_validate_passes_on_clean_kb( - limina_project: Path, python_exe: str + scaffold_project: Path, python_exe: str ) -> None: """Vendored Limina's shipped kb/ template validates cleanly out of the box.""" - r = _run_hook(limina_project, "stop_validate", None, python_exe, timeout=30) + r = _run_hook(scaffold_project, "stop_validate", None, python_exe, timeout=30) assert r.returncode == 0, (r.returncode, r.stdout, r.stderr) def test_stop_validate_no_kb_dir_is_noop( - limina_project: Path, python_exe: str + scaffold_project: Path, python_exe: str ) -> None: """Missing kb/ dir -> hook returns 0 without running the validator.""" import shutil as _shutil - _shutil.rmtree(limina_project / "kb") - r = _run_hook(limina_project, "stop_validate", None, python_exe, timeout=10) + _shutil.rmtree(scaffold_project / "kb") + r = _run_hook(scaffold_project, "stop_validate", None, python_exe, timeout=10) assert r.returncode == 0 def test_stop_validate_blocks_on_broken_kb( - limina_project: Path, python_exe: str + scaffold_project: Path, python_exe: str ) -> None: """Introduce a broken artifact -> stop_validate exits 2 with BLOCKED.""" - broken = limina_project / "kb" / "research" / "experiments" / "E999-broken.md" + broken = scaffold_project / "kb" / "research" / "experiments" / "E999-broken.md" broken.parent.mkdir(parents=True, exist_ok=True) broken.write_text("no frontmatter, no valid structure", encoding="utf-8") - r = _run_hook(limina_project, "stop_validate", None, python_exe, timeout=30) + r = _run_hook(scaffold_project, "stop_validate", None, python_exe, timeout=30) assert r.returncode == 2, (r.returncode, r.stdout, r.stderr) assert "BLOCKED" in r.stderr diff --git a/tests/test_install.py b/tests/test_install.py index e5d26c8..8fa9edd 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -1,4 +1,4 @@ -"""Tests for ``install_limina``.""" +"""Tests for ``install_scaffold``.""" from __future__ import annotations import json @@ -12,8 +12,8 @@ InstallRefused, block_merge_markdown, compute_vendor_sha, - install_limina, - is_limina_installed, + install_scaffold, + is_scaffold_installed, merge_claude_settings, ) from aexp.utils.paths import ( @@ -142,23 +142,23 @@ def test_block_merge_replaces_existing_block() -> None: # --------------------------------------------------------------------------- -# install_limina — end-to-end +# install_scaffold — end-to-end # --------------------------------------------------------------------------- def test_install_requires_git_by_default(tmp_path: Path) -> None: with pytest.raises(RuntimeError, match="not a git repo"): - install_limina(tmp_path) + install_scaffold(tmp_path) def test_install_allows_no_git_when_flag_false(tmp_path: Path) -> None: # Should not raise. - actions = install_limina(tmp_path, assert_git=False) + actions = install_scaffold(tmp_path, assert_git=False) assert any(a.kind == "wrote_marker" for a in actions) def test_install_populates_fresh_repo(fresh_git_repo: Path) -> None: - actions = install_limina(fresh_git_repo) + actions = install_scaffold(fresh_git_repo) kinds = {a.kind for a in actions} assert "copied" in kinds assert "initialized_runs" in kinds @@ -190,7 +190,7 @@ def test_install_drops_slash_commands_without_a_second_step( a second step; this test pins the folded-in behaviour so the standalone verb can't become load-bearing again. """ - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) commands = fresh_git_repo / ".claude" / "commands" assert commands.is_dir() for name in ( @@ -207,7 +207,7 @@ def test_install_drops_slash_commands_without_a_second_step( def test_install_initializes_signac_project(fresh_git_repo: Path) -> None: - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) assert (fresh_git_repo / ".runs").is_dir() assert (fresh_git_repo / ".runs" / "signac.rc").is_file() or ( fresh_git_repo / ".runs" / "workspace" @@ -217,7 +217,7 @@ def test_install_initializes_signac_project(fresh_git_repo: Path) -> None: def test_install_writes_valid_marker(fresh_git_repo: Path) -> None: - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) marker = read_installed_marker(fresh_git_repo) assert marker is not None assert marker["version"] @@ -228,22 +228,22 @@ def test_install_writes_valid_marker(fresh_git_repo: Path) -> None: assert Path(marker["python_exe"]).exists() # conda_env_name is present (may be "" when running under a venv). assert "conda_env_name" in marker - assert is_limina_installed(fresh_git_repo) + assert is_scaffold_installed(fresh_git_repo) def test_install_is_idempotent(fresh_git_repo: Path) -> None: - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) # Second run: every action should be either already_installed (short-circuit) # or a no-op kind. Because the marker matches the vendor sha, we short-circuit. - actions = install_limina(fresh_git_repo) + actions = install_scaffold(fresh_git_repo) assert any(a.kind == "already_installed" for a in actions) # Critically: nothing got copied again assert not any(a.kind == "copied" for a in actions) def test_install_force_bypasses_idempotence(fresh_git_repo: Path) -> None: - install_limina(fresh_git_repo) - actions = install_limina(fresh_git_repo, force=True) + install_scaffold(fresh_git_repo) + actions = install_scaffold(fresh_git_repo, force=True) # On force, we re-walk the trees; all files should match -> skipped_identical # (nothing on disk changed since last install). assert not any(a.kind == "already_installed" for a in actions) @@ -261,7 +261,7 @@ def test_install_preserves_user_modified_kb_content_without_force( target.parent.mkdir(parents=True) target.write_text("user-owned content", encoding="utf-8") - actions = install_limina(fresh_git_repo) + actions = install_scaffold(fresh_git_repo) entries = [a for a in actions if a.path.endswith("kb/ACTIVE.md")] assert entries assert entries[0].kind == "preserved_user_modified" @@ -286,7 +286,7 @@ def test_install_preserves_user_modified_kb_content_even_under_force( "# My research mission\n\nSpecific objective X.\n", encoding="utf-8" ) - actions = install_limina(fresh_git_repo, force=True) + actions = install_scaffold(fresh_git_repo, force=True) entries = [a for a in actions if a.path.endswith("kb/mission/CHALLENGE.md")] assert entries assert entries[0].kind == "preserved_user_modified" @@ -302,7 +302,7 @@ def test_install_preserves_user_modified_templates(fresh_git_repo: Path) -> None target.parent.mkdir(parents=True) target.write_text("# My custom hypothesis template\n", encoding="utf-8") - actions = install_limina(fresh_git_repo, force=True) + actions = install_scaffold(fresh_git_repo, force=True) entries = [ a for a in actions if a.path.endswith("templates/hypothesis.md") ] @@ -317,8 +317,8 @@ def test_install_refreshes_default_kb_content_under_force( """If a kb/ file still byte-matches the shipped default, a re-install should report ``skipped_identical`` — no preservation noise, and no copy since the content is already correct.""" - install_limina(fresh_git_repo) # first run lays down the defaults - actions = install_limina(fresh_git_repo, force=True) + install_scaffold(fresh_git_repo) # first run lays down the defaults + actions = install_scaffold(fresh_git_repo, force=True) # Path filter: files shipped under kb/ always end with "kb/<...>.md". kb_entries = [ a @@ -343,7 +343,7 @@ def test_install_force_still_overwrites_stale_slash_commands( target.parent.mkdir(parents=True) target.write_text("# stale hand-edited content\n", encoding="utf-8") - actions = install_limina(fresh_git_repo, force=True) + actions = install_scaffold(fresh_git_repo, force=True) entries = [ a for a in actions @@ -377,7 +377,7 @@ def test_install_json_merges_existing_claude_settings(fresh_git_repo: Path) -> N encoding="utf-8", ) - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) merged = json.loads((claude / "settings.json").read_text("utf-8")) # User's hook survived post_tool = merged["hooks"]["PostToolUse"] @@ -393,7 +393,7 @@ def test_install_block_merges_existing_agents_md(fresh_git_repo: Path) -> None: (fresh_git_repo / "AGENTS.md").write_text( "# Existing Agents\n\nuser-defined agent rules\n", encoding="utf-8" ) - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) content = (fresh_git_repo / "AGENTS.md").read_text("utf-8") assert "user-defined agent rules" in content assert "<!-- agentic-experiments:begin -->" in content @@ -402,7 +402,7 @@ def test_install_block_merges_existing_agents_md(fresh_git_repo: Path) -> None: def test_install_creates_claude_dir_if_missing(fresh_git_repo: Path) -> None: assert not (fresh_git_repo / ".claude").exists() - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) assert (fresh_git_repo / ".claude" / "settings.json").is_file() @@ -410,14 +410,14 @@ def test_installed_kb_validates_cleanly(fresh_git_repo: Path) -> None: """After install, the seeded kb/ passes structural validation.""" from aexp.kb_validate import validate_kb - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) result = validate_kb(fresh_git_repo / "kb") assert result.ok, result.errors def test_install_action_kinds_are_expected(fresh_git_repo: Path) -> None: """Every returned InstallAction carries a known kind + a non-empty path.""" - actions = install_limina(fresh_git_repo) + actions = install_scaffold(fresh_git_repo) valid_kinds = { "copied", "skipped_identical", @@ -441,7 +441,7 @@ def test_install_copies_skills_to_claude_skills(fresh_git_repo: Path) -> None: Without these, the AGENTS.md references like $experiment-rigor are broken on every consumer repo. """ - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) skills_root = fresh_git_repo / ".claude" / "skills" assert skills_root.is_dir() # Research-methodology skills (from vendor/limina/skills/) @@ -458,7 +458,7 @@ def test_install_copies_skills_to_claude_skills(fresh_git_repo: Path) -> None: def test_install_skills_emits_installed_skill_actions(fresh_git_repo: Path) -> None: - actions = install_limina(fresh_git_repo) + actions = install_scaffold(fresh_git_repo) skill_actions = [a for a in actions if a.kind == "installed_skill"] # 4 research-methodology skills = 4 installed_skill entries. assert len(skill_actions) == 4, [a.path for a in skill_actions] @@ -478,7 +478,7 @@ def test_install_writes_mcp_json_at_repo_root(fresh_git_repo: Path) -> None: This is the file Claude Code reads for project-scope MCP servers; ``.claude/settings.json`` does NOT drive MCP config. """ - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) mcp_path = fresh_git_repo / ".mcp.json" assert mcp_path.is_file(), "install must write .mcp.json at repo root" mcp = json.loads(mcp_path.read_text("utf-8")) @@ -498,7 +498,7 @@ def test_install_does_not_write_mcp_servers_to_settings_json( """``mcpServers`` must NOT end up in ``.claude/settings.json`` — Claude Code would ignore it there. All MCP config belongs in ``.mcp.json``. """ - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) settings = json.loads( (fresh_git_repo / ".claude" / "settings.json").read_text("utf-8") ) @@ -515,7 +515,7 @@ def test_install_mcp_entry_uses_uvx(fresh_git_repo: Path) -> None: MCP quickstart). It lets ``.mcp.json`` be committed to git because every teammate with ``uv`` installed gets the same invocation. """ - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) mcp = json.loads((fresh_git_repo / ".mcp.json").read_text("utf-8")) entry = mcp["mcpServers"]["aexp"] assert entry["command"] == "uvx" @@ -533,12 +533,12 @@ def test_install_mcp_entry_uses_uvx(fresh_git_repo: Path) -> None: def test_install_dev_mcp_entry_uses_current_interpreter(fresh_git_repo: Path) -> None: - """``install_limina(..., dev=True)`` writes a direct-Python MCP invocation + """``install_scaffold(..., dev=True)`` writes a direct-Python MCP invocation so editable installs (``pip install -e``) take effect on the MCP surface. """ import sys - install_limina(fresh_git_repo, dev=True) + install_scaffold(fresh_git_repo, dev=True) mcp = json.loads((fresh_git_repo / ".mcp.json").read_text("utf-8")) entry = mcp["mcpServers"]["aexp"] # Command is the running interpreter, not uvx. @@ -553,11 +553,11 @@ def test_install_dev_mcp_entry_uses_current_interpreter(fresh_git_repo: Path) -> def test_install_dev_flag_can_be_toggled_on_reinstall(fresh_git_repo: Path) -> None: """Running install without --dev after a dev install rewrites back to uvx.""" - install_limina(fresh_git_repo, dev=True) + install_scaffold(fresh_git_repo, dev=True) mcp_dev = json.loads((fresh_git_repo / ".mcp.json").read_text("utf-8")) assert mcp_dev["mcpServers"]["aexp"]["command"] != "uvx" - install_limina(fresh_git_repo, force=True, dev=False) + install_scaffold(fresh_git_repo, force=True, dev=False) mcp_prod = json.loads((fresh_git_repo / ".mcp.json").read_text("utf-8")) assert mcp_prod["mcpServers"]["aexp"]["command"] == "uvx" @@ -573,7 +573,7 @@ def test_install_mcp_entry_is_env_independent( # Set a distinctive conda env to confirm we DON'T leak it. monkeypatch.setenv("CONDA_DEFAULT_ENV", "some-distinctive-env-name") - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) content = (fresh_git_repo / ".mcp.json").read_text("utf-8") # No env-specific strings should appear anywhere in the written file. assert "some-distinctive-env-name" not in content @@ -605,7 +605,7 @@ def test_install_preserves_user_mcp_servers_in_mcp_json( ), encoding="utf-8", ) - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) merged = json.loads(user_mcp.read_text("utf-8")) assert "user-mcp" in merged["mcpServers"] assert "aexp" in merged["mcpServers"] @@ -630,7 +630,7 @@ def test_install_overwrites_stale_aexp_mcp_entry_on_reinstall( ), encoding="utf-8", ) - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) mcp = json.loads(user_mcp.read_text("utf-8")) combined = ( [mcp["mcpServers"]["aexp"]["command"]] @@ -667,10 +667,10 @@ def _plant_aexp_source_tree(path: Path) -> None: def test_install_refuses_aexp_source_tree(fresh_git_repo: Path) -> None: - """``install_limina`` refuses when cwd is the aexp source tree itself.""" + """``install_scaffold`` refuses when cwd is the aexp source tree itself.""" _plant_aexp_source_tree(fresh_git_repo) with pytest.raises(InstallRefused) as exc_info: - install_limina(fresh_git_repo) + install_scaffold(fresh_git_repo) msg = str(exc_info.value) assert "agentic-experiments source tree" in msg assert "--allow-self-install" in msg @@ -694,7 +694,7 @@ def test_install_refuses_subdirectory_of_source_tree( subdir.mkdir() # `assert_git=False` because the subdir doesn't have its own .git. with pytest.raises(InstallRefused): - install_limina(subdir, assert_git=False) + install_scaffold(subdir, assert_git=False) def test_install_allow_self_install_overrides_guard( @@ -707,7 +707,7 @@ def test_install_allow_self_install_overrides_guard( an inescapable hard block. """ _plant_aexp_source_tree(fresh_git_repo) - actions = install_limina(fresh_git_repo, allow_self_install=True) + actions = install_scaffold(fresh_git_repo, allow_self_install=True) # Got past the guard and produced a normal install action list. assert any(a.kind == "wrote_marker" for a in actions) assert (fresh_git_repo / ".aexp" / "installed.json").is_file() @@ -721,7 +721,7 @@ def test_install_allows_consumer_repo_with_different_name( '[project]\nname = "my-research"\nversion = "0.1.0"\n', encoding="utf-8", ) - actions = install_limina(fresh_git_repo) + actions = install_scaffold(fresh_git_repo) assert any(a.kind == "wrote_marker" for a in actions) assert (fresh_git_repo / ".aexp" / "installed.json").is_file() @@ -735,7 +735,7 @@ def test_install_allows_repo_without_pyproject(fresh_git_repo: Path) -> None: install path. """ # No pyproject.toml planted. - actions = install_limina(fresh_git_repo) + actions = install_scaffold(fresh_git_repo) assert any(a.kind == "wrote_marker" for a in actions) assert (fresh_git_repo / ".aexp" / "installed.json").is_file() @@ -748,7 +748,7 @@ def test_install_dry_run_also_refuses_source_tree(fresh_git_repo: Path) -> None: """ _plant_aexp_source_tree(fresh_git_repo) with pytest.raises(InstallRefused): - install_limina(fresh_git_repo, dry_run=True) + install_scaffold(fresh_git_repo, dry_run=True) # Confirm dry_run path didn't leak anything before the raise. assert not (fresh_git_repo / "kb").exists() assert not (fresh_git_repo / ".aexp").exists() @@ -762,7 +762,7 @@ def test_install_dry_run_also_refuses_source_tree(fresh_git_repo: Path) -> None: def test_install_with_jupyter_writes_mcp_entries(fresh_git_repo: Path) -> None: """--with-jupyter writes the jupyter entry to .mcp.json alongside the existing aexp entry. The legacy jupyter-compute server is not emitted.""" - install_limina(fresh_git_repo, with_jupyter=True, dev=True) + install_scaffold(fresh_git_repo, with_jupyter=True, dev=True) mcp_json = json.loads((fresh_git_repo / ".mcp.json").read_text(encoding="utf-8")) servers = mcp_json["mcpServers"] assert "aexp" in servers @@ -776,7 +776,7 @@ def test_install_with_jupyter_writes_mcp_entries(fresh_git_repo: Path) -> None: def test_install_without_jupyter_omits_mcp_entries(fresh_git_repo: Path) -> None: """Default install (no --with-jupyter) does NOT write the jupyter entry.""" - install_limina(fresh_git_repo, dev=True) + install_scaffold(fresh_git_repo, dev=True) mcp_json = json.loads((fresh_git_repo / ".mcp.json").read_text(encoding="utf-8")) servers = mcp_json["mcpServers"] assert "aexp" in servers @@ -786,7 +786,7 @@ def test_install_without_jupyter_omits_mcp_entries(fresh_git_repo: Path) -> None def test_install_with_jupyter_records_marker(fresh_git_repo: Path) -> None: """Marker records jupyter_enabled=True after --with-jupyter install.""" - install_limina(fresh_git_repo, with_jupyter=True, dev=True) + install_scaffold(fresh_git_repo, with_jupyter=True, dev=True) marker = read_installed_marker(fresh_git_repo) assert marker is not None assert marker.get("jupyter_enabled") is True @@ -794,7 +794,7 @@ def test_install_with_jupyter_records_marker(fresh_git_repo: Path) -> None: def test_install_without_jupyter_marker_omits_field(fresh_git_repo: Path) -> None: """Default install does not add jupyter_enabled to the marker (sticky-true semantics).""" - install_limina(fresh_git_repo, dev=True) + install_scaffold(fresh_git_repo, dev=True) marker = read_installed_marker(fresh_git_repo) assert marker is not None assert "jupyter_enabled" not in marker @@ -802,9 +802,9 @@ def test_install_without_jupyter_marker_omits_field(fresh_git_repo: Path) -> Non def test_install_jupyter_marker_is_sticky_true(fresh_git_repo: Path) -> None: """Once --with-jupyter is set, a later install without the flag preserves jupyter_enabled=True.""" - install_limina(fresh_git_repo, with_jupyter=True, dev=True) + install_scaffold(fresh_git_repo, with_jupyter=True, dev=True) # Re-install with force to bypass the already-installed short-circuit. - install_limina(fresh_git_repo, dev=True, force=True) + install_scaffold(fresh_git_repo, dev=True, force=True) marker = read_installed_marker(fresh_git_repo) assert marker is not None assert marker.get("jupyter_enabled") is True @@ -812,7 +812,7 @@ def test_install_jupyter_marker_is_sticky_true(fresh_git_repo: Path) -> None: def test_install_with_jupyter_vendors_setup_doc(fresh_git_repo: Path) -> None: """docs/setup/jupyter-mcp.md is copied verbatim from vendor when --with-jupyter.""" - install_limina(fresh_git_repo, with_jupyter=True, dev=True) + install_scaffold(fresh_git_repo, with_jupyter=True, dev=True) setup_doc = fresh_git_repo / "docs" / "setup" / "jupyter-mcp.md" assert setup_doc.is_file() body = setup_doc.read_text(encoding="utf-8") @@ -823,9 +823,9 @@ def test_install_with_jupyter_vendors_setup_doc(fresh_git_repo: Path) -> None: def test_install_with_jupyter_idempotent(fresh_git_repo: Path) -> None: """Running install twice with the same flags doesn't change .mcp.json.""" - install_limina(fresh_git_repo, with_jupyter=True, dev=True) + install_scaffold(fresh_git_repo, with_jupyter=True, dev=True) first = (fresh_git_repo / ".mcp.json").read_text(encoding="utf-8") - install_limina(fresh_git_repo, with_jupyter=True, dev=True, force=True) + install_scaffold(fresh_git_repo, with_jupyter=True, dev=True, force=True) second = (fresh_git_repo / ".mcp.json").read_text(encoding="utf-8") assert json.loads(first) == json.loads(second) @@ -838,7 +838,7 @@ def test_install_with_jupyter_preserves_user_entries(fresh_git_repo: Path) -> No } } (fresh_git_repo / ".mcp.json").write_text(json.dumps(custom), encoding="utf-8") - install_limina(fresh_git_repo, with_jupyter=True, dev=True) + install_scaffold(fresh_git_repo, with_jupyter=True, dev=True) mcp_json = json.loads((fresh_git_repo / ".mcp.json").read_text(encoding="utf-8")) servers = mcp_json["mcpServers"] assert "my_custom" in servers @@ -862,7 +862,7 @@ def test_install_with_jupyter_preserves_existing_jupyter_entry(fresh_git_repo: P } } (fresh_git_repo / ".mcp.json").write_text(json.dumps(custom), encoding="utf-8") - install_limina(fresh_git_repo, with_jupyter=True, dev=True) + install_scaffold(fresh_git_repo, with_jupyter=True, dev=True) servers = json.loads( (fresh_git_repo / ".mcp.json").read_text(encoding="utf-8") )["mcpServers"] @@ -876,7 +876,7 @@ def test_install_with_jupyter_preserves_existing_jupyter_entry(fresh_git_repo: P def test_install_with_jupyter_slash_command_always_present(fresh_git_repo: Path) -> None: """The /aexp-jupyter-iterate slash command is installed regardless of --with-jupyter (it self-checks tool availability at runtime).""" - install_limina(fresh_git_repo, dev=True) # NOTE: no --with-jupyter + install_scaffold(fresh_git_repo, dev=True) # NOTE: no --with-jupyter slash_cmd = fresh_git_repo / ".claude" / "commands" / "aexp-jupyter-iterate.md" assert slash_cmd.is_file() @@ -886,7 +886,7 @@ def test_install_writes_promote_nb_slash_command(fresh_git_repo: Path) -> None: and its body contains the load-bearing guardrails (refuses without an experiment ID, references the jupyter MCP family, refuses to invent a tracked_notebook_run API).""" - install_limina(fresh_git_repo, dev=True) + install_scaffold(fresh_git_repo, dev=True) slash_cmd = fresh_git_repo / ".claude" / "commands" / "aexp-promote-nb.md" assert slash_cmd.is_file() body = slash_cmd.read_text(encoding="utf-8") diff --git a/tests/test_limina_io.py b/tests/test_kb_io.py similarity index 96% rename from tests/test_limina_io.py rename to tests/test_kb_io.py index 6830d8e..5cc4c8a 100644 --- a/tests/test_limina_io.py +++ b/tests/test_kb_io.py @@ -1,12 +1,12 @@ -"""Tests for ``limina_io`` artifact readers.""" +"""Tests for ``kb_io`` artifact readers.""" from __future__ import annotations from pathlib import Path import pytest -from aexp.install import install_limina -from aexp.limina_io import ( +from aexp.install import install_scaffold +from aexp.kb_io import ( ArtifactNotFoundError, ArtifactReadError, find_artifact_path, @@ -50,7 +50,7 @@ def _write_artifact( @pytest.fixture def populated_kb(tmp_path: Path) -> Path: """A fresh kb/ with one artifact of each kind for read-side tests.""" - install_limina(tmp_path, assert_git=False) + install_scaffold(tmp_path, assert_git=False) kb = tmp_path / "kb" _write_artifact( @@ -195,7 +195,7 @@ def test_load_artifact_path_is_repo_relative_posix(populated_kb: Path) -> None: def test_load_artifact_recovers_id_from_filename(tmp_path: Path) -> None: """Frontmatter without an ``id`` field: we fall back to the filename.""" - install_limina(tmp_path, assert_git=False) + install_scaffold(tmp_path, assert_git=False) kb = tmp_path / "kb" p = kb / "research" / "hypotheses" / "H042-nofm-id.md" p.parent.mkdir(parents=True, exist_ok=True) @@ -226,7 +226,7 @@ def test_list_kb_artifacts_by_kind(populated_kb: Path) -> None: def test_list_kb_artifacts_skips_malformed(tmp_path: Path) -> None: - install_limina(tmp_path, assert_git=False) + install_scaffold(tmp_path, assert_git=False) kb = tmp_path / "kb" # Drop a broken file that can't be parsed or lacks a recoverable id. (kb / "research" / "hypotheses" / "H999-broken.md").write_text( diff --git a/tests/test_linking.py b/tests/test_linking.py index 9b12f09..6e04342 100644 --- a/tests/test_linking.py +++ b/tests/test_linking.py @@ -7,7 +7,7 @@ import pytest from pydantic import ValidationError -from aexp.install import install_limina +from aexp.install import install_scaffold from aexp.linking import ( link_to_experiment, list_batches, @@ -37,7 +37,7 @@ def installed_repo(tmp_path: Path) -> Path: repo = tmp_path / "repo" repo.mkdir() _git_commit(repo) - install_limina(repo) + install_scaffold(repo) return repo diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index 7b64a0e..c9018d0 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -15,7 +15,7 @@ pytest.importorskip("mcp") -from aexp.install import install_limina # noqa: E402 +from aexp.install import install_scaffold # noqa: E402 def _git_commit(repo: Path) -> None: @@ -37,7 +37,7 @@ def installed_repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: repo = tmp_path / "repo" repo.mkdir() _git_commit(repo) - install_limina(repo) + install_scaffold(repo) monkeypatch.chdir(repo) return repo diff --git a/tests/test_queue.py b/tests/test_queue.py index dfadad4..fa1f3ce 100644 --- a/tests/test_queue.py +++ b/tests/test_queue.py @@ -13,7 +13,7 @@ import pytest -from aexp.install import install_limina +from aexp.install import install_scaffold from aexp.queue import ( DuplicatePendingJobWarning, RunnerCommandMissing, @@ -67,7 +67,7 @@ def installed_repo(tmp_path: Path) -> Path: repo = tmp_path / "repo" repo.mkdir() _git_commit(repo) - install_limina(repo) + install_scaffold(repo) return repo @@ -123,7 +123,7 @@ def _patch_experiment_frontmatter( """Rewrite only the specified fields in an existing E###'s frontmatter.""" import frontmatter # type: ignore[import-not-found] - from aexp.limina_io import find_artifact_path + from aexp.kb_io import find_artifact_path exp_path = find_artifact_path(experiment_id, kb_root=repo / "kb") post = frontmatter.load(str(exp_path)) diff --git a/tests/test_runs.py b/tests/test_runs.py index 70894d3..431f9c0 100644 --- a/tests/test_runs.py +++ b/tests/test_runs.py @@ -6,7 +6,7 @@ import pytest -from aexp.install import install_limina +from aexp.install import install_scaffold from aexp.runs import ( RunNotFound, RunStoreNotInitialized, @@ -54,7 +54,7 @@ def installed_repo(tmp_path: Path) -> Path: check=True, capture_output=True, ) - install_limina(repo) + install_scaffold(repo) return repo @@ -110,7 +110,7 @@ def test_create_run_adds_commit_by_default(installed_repo: Path) -> None: ) assert "code_commit" in job.sp assert len(job.sp["code_commit"]) == 40 - # After install_limina, the fresh repo's working tree is dirty (we just + # After install_scaffold, the fresh repo's working tree is dirty (we just # wrote kb/, scripts/, etc.) — ensure the flag is present as a bool. assert isinstance(job.sp["code_dirty"], bool) diff --git a/tests/test_schema.py b/tests/test_schema.py index 7e100e0..7f0e19f 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -5,9 +5,9 @@ from pydantic import ValidationError from aexp.schema import ( + ArtifactRef, BatchSelector, Issue, - LiminaArtifactRef, RunLink, RunSummary, SupportingJobRun, @@ -89,8 +89,8 @@ def test_tracker_binding_minimal() -> None: # --------------------------------------------------------------------------- -def test_limina_artifact_ref_is_frozen() -> None: - ref = LiminaArtifactRef( +def test_artifact_ref_is_frozen() -> None: + ref = ArtifactRef( kind="E", id="E001", path="kb/research/experiments/E001-foo.md", diff --git a/tests/test_trackers_context.py b/tests/test_trackers_context.py index c07e9f9..e5aa063 100644 --- a/tests/test_trackers_context.py +++ b/tests/test_trackers_context.py @@ -15,7 +15,7 @@ import pytest -from aexp.install import install_limina +from aexp.install import install_scaffold from aexp.runs import create_run from aexp.trackers import ( TrackerContext, @@ -48,7 +48,7 @@ def installed_repo(tmp_path: Path) -> Path: repo = tmp_path / "repo" repo.mkdir() _git_commit(repo) - install_limina(repo) + install_scaffold(repo) return repo diff --git a/tests/test_trackers_noop.py b/tests/test_trackers_noop.py index c59aff4..347b06f 100644 --- a/tests/test_trackers_noop.py +++ b/tests/test_trackers_noop.py @@ -7,7 +7,7 @@ import pytest -from aexp.install import install_limina +from aexp.install import install_scaffold from aexp.runs import create_run from aexp.trackers import NoopAdapter, RunHandle, bind_tracker @@ -31,7 +31,7 @@ def installed_repo(tmp_path: Path) -> Path: repo = tmp_path / "repo" repo.mkdir() _git_commit(repo) - install_limina(repo) + install_scaffold(repo) return repo diff --git a/tests/test_trackers_wandb.py b/tests/test_trackers_wandb.py index 2461a8d..9465cab 100644 --- a/tests/test_trackers_wandb.py +++ b/tests/test_trackers_wandb.py @@ -13,7 +13,7 @@ wandb = pytest.importorskip("wandb") -from aexp.install import install_limina # noqa: E402 +from aexp.install import install_scaffold # noqa: E402 from aexp.runs import create_run # noqa: E402 from aexp.trackers import ( # noqa: E402 TrackerInitError, @@ -128,7 +128,7 @@ def installed_repo(tmp_path: Path) -> Path: repo = tmp_path / "repo" repo.mkdir() _git_commit(repo) - install_limina(repo) + install_scaffold(repo) return repo diff --git a/tests/test_validate.py b/tests/test_validate.py index ec0a417..158def9 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -6,7 +6,7 @@ import pytest -from aexp.install import install_limina +from aexp.install import install_scaffold from aexp.runs import create_run from aexp.validate import VALID_STATUSES, validate_repo @@ -83,7 +83,7 @@ def installed_repo(tmp_path: Path) -> Path: repo = tmp_path / "repo" repo.mkdir() _git_commit(repo) - install_limina(repo) + install_scaffold(repo) return repo From fdd00694d1314db24392a7bf96393e91d1fe3c11 Mon Sep 17 00:00:00 2001 From: Kaden McKeen <mckeenkaden@gmail.com> Date: Wed, 20 May 2026 23:31:23 -0400 Subject: [PATCH 04/10] =?UTF-8?q?refactor(debrand):=20stage=20D=20?= =?UTF-8?q?=E2=80=94=20de-brand=20the=20prose=20(docs,=20comments,=20READM?= =?UTF-8?q?E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final prose pass of the limina de-brand. Drops the proper noun "Limina" from docstrings, comments, slash-command descriptions, and docs where the H/E/F vocabulary already carries the meaning: - docs/*.md, code docstrings/comments, slash-command `description:` lines, pyproject `description` + ruff comments. - docs/concepts.md + docs/mapping.md also correct the run-link key references (`job.doc["limina"]` -> `job.doc["aexp"]`, error codes, W&B config key) to match the stage-B rename. - README: the `limina` credit moves out of the "Integrate, don't reinvent" bullet into a muted "Acknowledgements" section that keeps the KadenMc/limina link. - CHANGELOG: an [Unreleased] entry documenting the de-brand; history left verbatim. Intentionally still say "limina": the vendored directory path src/aexp/vendor/limina/, the legacy-key dual-read fallback, the aexp.limina_io deprecation shim, and the README/CHANGELOG credit. Completes the limina de-brand (one PR, four staged commits A-D). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- CHANGELOG.md | 26 +++++++++++++++++++ README.md | 10 +++++-- docs/cli.md | 10 +++---- docs/concepts.md | 26 +++++++++---------- docs/mapping.md | 14 +++++----- docs/mcp.md | 4 +-- docs/quickstart.md | 2 +- docs/tracker-adapters.md | 8 +++--- pyproject.toml | 10 +++---- src/aexp/__init__.py | 4 +-- src/aexp/artifacts.py | 2 +- src/aexp/backlinks.py | 2 +- src/aexp/cli.py | 16 ++++++------ src/aexp/hooks/__init__.py | 4 +-- src/aexp/install.py | 10 +++---- src/aexp/kb_io.py | 8 +++--- src/aexp/kb_validate.py | 10 +++---- src/aexp/mcp_server.py | 16 ++++++------ src/aexp/sandbox.py | 2 +- src/aexp/schema.py | 4 +-- .../slash_commands/aexp-finding-from-batch.md | 2 +- .../slash_commands/aexp-finding-from-run.md | 2 +- .../aexp-finding-placeholder.md | 2 +- src/aexp/slash_commands/aexp-list-threads.md | 2 +- .../slash_commands/aexp-new-experiment.md | 2 +- .../slash_commands/aexp-new-hypothesis.md | 2 +- src/aexp/slash_commands/aexp-new-run.md | 4 +-- src/aexp/slash_commands/aexp-new-sandbox.md | 2 +- src/aexp/slash_commands/aexp-new-thread.md | 2 +- src/aexp/slash_commands/aexp-show-run.md | 2 +- tests/conftest.py | 6 ++--- tests/test_cli.py | 2 +- tests/test_hooks_python.py | 8 +++--- tests/test_kb_io.py | 2 +- tests/test_runs.py | 4 +-- tests/test_sandbox.py | 2 +- tests/test_validate.py | 4 +-- 37 files changed, 135 insertions(+), 103 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 806aee9..aba6af7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 cleanup. The cluster-side `[jupyter]` extra and `aexp jupyter setup` extension recipe are unchanged. +- **De-branded the vendored "limina" research harness.** `limina` + (vendored 2026-04-20) is no longer surfaced as a named centerpiece — + the harness reads as plain `aexp`. **Breaking** public-API renames + (old names removed; one shim — see below): + - `install_limina()` → `install_scaffold()` + - `is_limina_installed()` → `is_scaffold_installed()` + - `LiminaArtifactRef` → `ArtifactRef` + - module `aexp.limina_io` → `aexp.kb_io`. `aexp.limina_io` is kept as + a deprecation shim (re-exports `aexp.kb_io`, emits a + `DeprecationWarning`) for one release. + + Persisted keys are renamed with a **read-side fallback**, so existing + signac projects and install markers keep resolving with no migration: + - run-link key `job.doc["limina"]` → `job.doc["aexp"]` + - install-marker field `limina_vendor_sha` → `vendor_sha` + - W&B run-config block `config["limina"]` → `config["aexp"]` (past + W&B runs keep `config.limina`; new runs get `config.aexp`) + - validator error codes `limina.validation_failed` / + `limina.validator_unavailable` → `aexp.*` + + The vendored directory `src/aexp/vendor/limina/` keeps its name (a + vendoring convention; the upstream credit is in the README). Its + contents, the slash commands, `AGENTS.md` / `CLAUDE.md`, and the docs + are de-branded. The stale top-level "limina" skill was removed — + `aexp install` already scaffolds a project. + ### Fixed - **`aexp install --with-jupyter` now pins the `.mcp.json` `jupyter` diff --git a/README.md b/README.md index 560178a..4d6872c 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ - **Hypothesis-first, not metric-first** — you can't start a run without a live hypothesis; you can't ship a finding without cited runs - **Git is the source of truth** — every run carries its commit SHA; the knowledge base lives in git; nothing load-bearing is ephemeral -- **Integrate, don't reinvent** — [signac](https://signac.readthedocs.io) for run state, [W&B](https://wandb.ai/) for observability, [Limina](https://github.com/KadenMc/limina) for the research-graph primitives (the H→E→F artifact model, templates, and methodology skills this project builds on). `aexp` is the glue and the discipline +- **Integrate, don't reinvent** — [signac](https://signac.readthedocs.io) for run state, [W&B](https://wandb.ai/) for observability, and a vendored research harness for the H→E→F artifact model, templates, and methodology skills. `aexp` is the glue and the discipline - **Portable by default** — the MCP server runs via `uvx` from PyPI; `.mcp.json` is identical on every machine and committable to git --- @@ -260,7 +260,7 @@ src/aexp/ install.py # apply the harness into a consumer repo runs.py # signac wrappers: create_run, open_run, find_runs, run_lifecycle linking.py # batch queries + retroactive run-to-experiment linking - limina_io.py # typed read wrappers for H/E/F/L/CR/SR artifacts + kb_io.py # typed read wrappers for H/E/F/L/CR/SR artifacts validate.py # composes KB structural + run-link + citation integrity kb_validate.py # KB structural validator (frontmatter, aliases, chain) schema.py # pydantic + dataclass types @@ -329,6 +329,12 @@ Every edit to `src/aexp/*.py` is now live in: --- +## Acknowledgements + +The vendored research harness — the H→E→F artifact model, the `kb/` layout, +artifact templates, and methodology skills — was adapted from +[limina](https://github.com/KadenMc/limina). + ## License [MIT](LICENSE) diff --git a/docs/cli.md b/docs/cli.md index 1223414..769edf8 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -124,8 +124,8 @@ marker matches the current sha, the command short-circuits with an ### `aexp new-run` -Create (or re-open) a signac job linked to a Limina experiment. Always -writes `job.doc["limina"]` and `job.doc["status"] = "created"`. `--sp` takes +Create (or re-open) a signac job linked to an experiment. Always +writes `job.doc["aexp"]` and `job.doc["status"] = "created"`. `--sp` takes `KEY=VAL,KEY=VAL` — all values stay as strings; use the Python API when you need typed values (bools, ints, lists). @@ -137,7 +137,7 @@ if bound. ### `aexp show-run` -Print the full state point + doc + linked Limina frame for one run. +Print the full state point + doc + linked research frame for one run. ### `aexp new-sandbox` @@ -179,14 +179,14 @@ Change the grouping via the Python API: `list_batches(selector_keys=("condition" ### `aexp link` -Retroactively stamp `doc["limina"]` onto an existing job. Used when a job +Retroactively stamp `doc["aexp"]` onto an existing job. Used when a job was created outside `create_run` (e.g. from a notebook directly calling signac) and you want to link it to an experiment after the fact. ### `aexp bind-tracker` Start a tracker run and wire it to the job: group = `hypothesis/experiment/condition`, -tags auto-derived, config includes the full Limina chain + `job.sp` + a +tags auto-derived, config includes the full run-link chain + `job.sp` + a curated frame (hypothesis statement, local hypothesis, success criteria). `job.doc["tracker"]` stores the handle. diff --git a/docs/concepts.md b/docs/concepts.md index 7c50487..a16e110 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -1,8 +1,8 @@ # Concepts -`agentic-experiments` is a **fusion layer**, not a new framework. It builds -on [Limina](https://github.com/KadenMc/limina) for the research-graph -primitives (H→E→F artifact model, templates, methodology skills), uses +`agentic-experiments` is a **fusion layer**, not a new framework. It pairs a +research-graph harness (the H→E→F artifact model, templates, and methodology +skills — vendored from [limina](https://github.com/KadenMc/limina)) with [signac](https://signac.readthedocs.io) for local execution and run state, and bridges to W&B for optional remote observability. @@ -18,12 +18,12 @@ and bridges to W&B for optional remote observability. For any claim a user holds, they can trace: -- **down** to the runs that produced it: a `Finding` cites `supporting_runs:` → each run's `.runs/workspace/<id>/` preserves outputs + `job.doc["limina"]` to navigate back. -- **up** to the question it was meant to answer: from a run, `job.doc["limina"]["experiment_id"]` → `kb/research/experiments/E###-*.md` (frame + protocol) → `Hypothesis: H###` → `kb/research/hypotheses/H###-*.md`. +- **down** to the runs that produced it: a `Finding` cites `supporting_runs:` → each run's `.runs/workspace/<id>/` preserves outputs + `job.doc["aexp"]` to navigate back. +- **up** to the question it was meant to answer: from a run, `job.doc["aexp"]["experiment_id"]` → `kb/research/experiments/E###-*.md` (frame + protocol) → `Hypothesis: H###` → `kb/research/hypotheses/H###-*.md`. The bidirectional traversal *is* the coupling. If it breaks anywhere, the whole collapses into "some logs and some notes." -## Limina ↔ signac mapping +## Artifact ↔ signac mapping - One `E###` artifact = one research-level experiment (intent, protocol, success criteria). Human/agent-facing. - One signac job = one concrete execution instance. Many jobs per `E###`. @@ -32,7 +32,7 @@ The bidirectional traversal *is* the coupling. If it breaks anywhere, the whole ### State point vs job document - `job.sp` — identity-defining: `experiment_id`, `hypothesis_id` (optional), `condition`, `model`, `dataset_slice`, `seed`, `prompt_rev`, `code_commit`, and any consumer-specific params. -- `job.doc` — mutable: `limina` link dict, `status`, `started_at` / `ended_at` / `wallclock_s`, `tracker` (backend + run_id + url), `summary_metrics`, `tags`. +- `job.doc` — mutable: `aexp` run-link dict, `status`, `started_at` / `ended_at` / `wallclock_s`, `tracker` (backend + run_id + url), `summary_metrics`, `tags`. ### Sub-hypotheses @@ -43,15 +43,15 @@ hypothesis: "H012" # primary sub_hypotheses: ["H013", "H014"] # optional, tested within this experiment ``` -Runs may link to `H012`, `H013`, or `H014` via `sp.hypothesis_id` or `job.doc["limina"]["sub_hypothesis_id"]`. `aexp validate` checks that any claimed sub-hypothesis is in the experiment's listed `Sub-hypotheses`. +Runs may link to `H012`, `H013`, or `H014` via `sp.hypothesis_id` or `job.doc["aexp"]["sub_hypothesis_id"]`. `aexp validate` checks that any claimed sub-hypothesis is in the experiment's listed `Sub-hypotheses`. ### Batch as a query-level concept -A *batch* is NOT a Limina artifact. It's a slice over `.runs/` defined by shared state-point values — most commonly `(experiment_id, condition)` — mapping 1:1 to a W&B group string. Use `aexp list-batches` / `aexp show-batch` to browse them. `batch_slug(hypothesis_id, experiment_id, condition, fallback)` is the single function that derives this slug everywhere (CLI tables, W&B group, closing findings). +A *batch* is NOT a research artifact. It's a slice over `.runs/` defined by shared state-point values — most commonly `(experiment_id, condition)` — mapping 1:1 to a W&B group string. Use `aexp list-batches` / `aexp show-batch` to browse them. `batch_slug(hypothesis_id, experiment_id, condition, fallback)` is the single function that derives this slug everywhere (CLI tables, W&B group, closing findings). ## Linking direction of truth -- **Job → Limina**: `job.doc["limina"] = {"experiment_id": "E018", "hypothesis_id": "H012", "sub_hypothesis_id": null, "experiment_path": "kb/.../E018-*.md"}`. +- **Job → run link**: `job.doc["aexp"] = {"experiment_id": "E018", "hypothesis_id": "H012", "sub_hypothesis_id": null, "experiment_path": "kb/.../E018-*.md"}`. - **Finding → Runs**: finding frontmatter field `supporting_runs:` — a list of `{type: job, id: ...}` OR `{type: batch, experiment_id, selector: {...}}` entries. Validated by `aexp validate`. - **Job → Tracker**: `job.doc["tracker"] = {"backend": "wandb", "run_id": "...", "url": "...", "project": "...", "group": "..."}` — written by `bind_tracker`. @@ -88,16 +88,16 @@ There are two pieces of validation machinery, and they check different things: | Validator | Runs when | Scope | Exit code surfaces | |---|---|---|---| | `aexp.kb_validate.validate_kb()` | `PostToolUse` on every kb-write (via `aexp.hooks.kb_write_guard`) and `Stop` at turn end (via `aexp.hooks.stop_validate`) | **KB structural only** — frontmatter required fields, filename format, ID aliases, wikilinks resolve, bidirectional backlinks (H↔E↔F), required H2 sections. | Claude Code hook (blocks turn / write) | -| `aexp.validate.validate_repo()` / `aexp validate` | Manually by the user or agent | **Everything above** (calls `validate_kb()` in-process) **plus** run-link integrity (`doc["limina"]`), `supporting_runs` citation checks, hypothesis-consistency between run and experiment. | CLI exit code 1 | +| `aexp.validate.validate_repo()` / `aexp validate` | Manually by the user or agent | **Everything above** (calls `validate_kb()` in-process) **plus** run-link integrity (`doc["aexp"]`), `supporting_runs` citation checks, hypothesis-consistency between run and experiment. | CLI exit code 1 | **Practical implication:** a Claude Code session can end cleanly (Stop hook passes) while still containing broken `supporting_runs` citations. The Stop hook does not catch them. Run `python -m aexp validate` explicitly before considering a session "complete." -## Why fork, not depend, on Limina +## Why vendor the research harness -Limina upstream ships a template-clone flow (`clone + rm .git + re-init`) that doesn't compose with *applying* a harness to an existing repo. So `aexp` forks the pieces it needs: +The upstream [limina](https://github.com/KadenMc/limina) project ships a template-clone flow (`clone + rm .git + re-init`) that doesn't compose with *applying* a harness to an existing repo. So `aexp` vendors the pieces it needs: - Hook behavior has been ported into `aexp.hooks.*` and is invoked as Python modules from the installed package. - The KB structural validator lives at `aexp.kb_validate` — in-process, no subprocess dance. diff --git a/docs/mapping.md b/docs/mapping.md index 5e4c50c..caaacf5 100644 --- a/docs/mapping.md +++ b/docs/mapping.md @@ -1,4 +1,4 @@ -# Limina IDs ↔ signac jobs ↔ W&B runs +# Artifact IDs ↔ signac jobs ↔ W&B runs The single most important doc. If you read one file, read this one. @@ -13,11 +13,11 @@ Experiment (E###) ── in kb/research/experiments/ ▼ signac Job ── in .runs/workspace/<job_id>/ │ sp: experiment_id, hypothesis_id, condition, model, seed, code_commit, ... - │ doc: limina={...}, status, tracker={...}, summary_metrics, ... + │ doc: aexp={...}, status, tracker={...}, summary_metrics, ... ▼ W&B Run ── project=<user-supplied> group=<hypothesis_id>/<experiment_id>/<condition> - config={**sp, limina, frame, job_id} + config={**sp, aexp, frame, job_id} │ Finding (F###) ── in kb/research/findings/ supporting_runs: [{type: job, id: <job_id>} | {type: batch, experiment_id, selector}] @@ -30,7 +30,7 @@ directory → new run. These are the keys `create_run` knows about: | Key | Source | Meaning | |---|---|---| -| `experiment_id` | always auto-added | Limina `E###` link (mirror of `doc["limina"]`) | +| `experiment_id` | always auto-added | `E###` link (mirror of `doc["aexp"]`) | | `hypothesis_id` | if passed to `create_run` | Primary or sub-hypothesis link | | `sub_hypothesis_id` | if passed | Narrower framing within an experiment | | `code_commit`, `code_dirty` | `git rev-parse HEAD` at creation, unless `include_commit=False` | Reproducibility pin | @@ -45,7 +45,7 @@ commit for replay by passing `code_commit="abc1234"` yourself. ```python { - "limina": { + "aexp": { "experiment_id": "E018", "experiment_path": "kb/research/experiments/E018-paired-ablation.md", "hypothesis_id": "H012", @@ -114,8 +114,8 @@ supporting_runs: | Code | Meaning | Fix | |---|---|---| -| `limina.validation_failed` | Vendored `kb_validate.py` reported errors | Read the details; usually missing `## Links`, missing frontmatter field, broken wikilink | -| `run.orphan` | A signac job has no `doc["limina"]` | `aexp link <job_id> --experiment E###` | +| `aexp.validation_failed` | Vendored `kb_validate.py` reported errors | Read the details; usually missing `## Links`, missing frontmatter field, broken wikilink | +| `run.orphan` | A signac job has no `doc["aexp"]` | `aexp link <job_id> --experiment E###` | | `run.broken_experiment_link` | Run references an E### with no file on disk | Fix the link, or create the experiment via `kb_new_artifact.py` | | `run.hypothesis_mismatch` | Run's `hypothesis_id` isn't the experiment's primary or a sub | Fix the run or add the hypothesis to the experiment's `sub_hypotheses:` | | `run.sub_hypothesis_unlisted` | Run claims a sub-hypothesis not in experiment's `sub_hypotheses:` | Same fix | diff --git a/docs/mcp.md b/docs/mcp.md index 6bda7fc..f2ec66e 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -89,12 +89,12 @@ teammates get the MCP server on clone. | Tool | Purpose | Required args | |---|---|---| -| `new_run` | Create a signac job linked to a Limina experiment | `experiment_id` | +| `new_run` | Create a signac job linked to an experiment | `experiment_id` | | `list_runs` | Filter runs by experiment / hypothesis / status | — | | `list_batches` | Group runs into `(experiment_id, condition)` slices | — | | `show_run` | Full state point + doc + workspace for one run | `job_id` | | `show_batch` | Runs matching a batch selector | `experiment_id` | -| `link_run` | Retroactively stamp `doc["limina"]` onto a job | `job_id`, `experiment_id` | +| `link_run` | Retroactively stamp `doc["aexp"]` onto a job | `job_id`, `experiment_id` | | `bind_tracker` | Attach a noop or wandb tracker to a run | `job_id` | | `validate` | Compose KB + run-link + finding-citation checks | — | | `sync_offline` | `wandb sync` every offline run in the store | — | diff --git a/docs/quickstart.md b/docs/quickstart.md index ffbaee7..1b6a2bf 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -27,7 +27,7 @@ aexp install After 10 seconds: -- `kb/` seeded with Limina's template (`ACTIVE.md`, `DASHBOARD.md`, `mission/CHALLENGE.md`, `research/...`). +- `kb/` seeded with the research-graph template (`ACTIVE.md`, `DASHBOARD.md`, `mission/CHALLENGE.md`, `research/...`). - `.claude/settings.json` with Python hooks enforcing the H→E→F chain. - `.runs/` initialized as a signac project. - `AGENTS.md` + `CLAUDE.md` merged in. diff --git a/docs/tracker-adapters.md b/docs/tracker-adapters.md index 326de31..811c8fe 100644 --- a/docs/tracker-adapters.md +++ b/docs/tracker-adapters.md @@ -46,7 +46,7 @@ with tracked_run(job, project="my-project", offline=True) as run: What `tracked_run` does: - Derives the deterministic group slug `H###/E###/condition` from the linked - Limina artifacts and `job.sp`. + research artifacts and `job.sp`. - Assembles tags (`kind=experiment`, `H###`, `E###`, `condition=X`), pulls the hypothesis statement / local hypothesis / success criteria into `notes`, flattens the state point into `config`, and sets `dir` to the @@ -252,8 +252,8 @@ aexp sync-offline --dry-run Or drive wandb directly: `wandb sync --sync-all .runs/`. Run IDs are stable between offline and online, so synced runs show up in -W&B with the same id, group (`H012/E018/full`), tags, and full Limina -config (`limina.experiment_id`, `limina.hypothesis_id`, etc.) regardless of +W&B with the same id, group (`H012/E018/full`), tags, and full run-link +config (`aexp.experiment_id`, `aexp.hypothesis_id`, etc.) regardless of which mode initialized them. ### Python API @@ -298,5 +298,5 @@ OpenTelemetry is a plausible v1.1 extra (`pip install agentic-experiments[otel]`): Claude Code itself emits OTEL under `CLAUDE_CODE_ENABLE_TELEMETRY=1`, so our spans could land in the same collector and correlate by session id. Not shipping in v1 — we don't yet -know whether structured JSON logs to stderr (which the Limina hooks already +know whether structured JSON logs to stderr (which the aexp hooks already produce) are enough. diff --git a/pyproject.toml b/pyproject.toml index 2085698..a861e42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "agentic-experiments" version = "0.4.0" -description = "Git-first, hypothesis-forcing experiment tracking for agent-driven ML research. Vendors Limina as the research harness, uses signac for local execution/run state, and bridges to W&B for remote observability." +description = "Git-first, hypothesis-forcing experiment tracking for agent-driven ML research. Vendors a research harness for the H->E->F artifact model, uses signac for local execution/run state, and bridges to W&B for remote observability." authors = [ {name = "Kaden McKeen", email = "mckeenkaden@gmail.com"} ] @@ -114,9 +114,9 @@ exclude = [ [tool.ruff] line-length = 100 target-version = "py312" -# `reference/` is the pristine upstream Limina snapshot (dev-only, never shipped). -# `src/aexp/vendor/` is the forked Limina vendor tree — we preserve upstream style -# for diff clarity against reference/. Neither should be linted as our own code. +# `src/aexp/vendor/` is the vendored research-harness tree -- we preserve its +# upstream style for diff clarity, so it is not linted as our own code. +# `reference/` (dev-only diff provenance) is excluded for the same reason. extend-exclude = ["reference", "src/aexp/vendor"] [tool.ruff.lint] @@ -133,7 +133,7 @@ extend-immutable-calls = ["typer.Option", "typer.Argument"] # Tests contain long literal artifact-body strings used as fixtures; breaking # them up hurts readability without any real benefit. "tests/*" = ["E501"] -# Ported near-verbatim from the vendored Limina kb_validate.py; keeping the +# Ported near-verbatim from the vendored kb_validate.py; keeping the # upstream line-break style makes future diffs against upstream intelligible. "src/aexp/kb_validate.py" = ["E501"] # The install command's multi-line heads-up text contains intentionally wide diff --git a/src/aexp/__init__.py b/src/aexp/__init__.py index d403222..6111f2e 100644 --- a/src/aexp/__init__.py +++ b/src/aexp/__init__.py @@ -1,4 +1,4 @@ -"""agentic-experiments: Limina-fork + signac + W&B fusion layer. +"""agentic-experiments: research harness + signac + W&B fusion layer. Top-level public API. Import from here; sub-modules may be reorganized. """ @@ -37,7 +37,7 @@ is_scaffold_installed, ) -# Limina readers ------------------------------------------------------------ +# kb/ artifact readers ------------------------------------------------------ from aexp.kb_io import ( ArtifactNotFoundError, ArtifactReadError, diff --git a/src/aexp/artifacts.py b/src/aexp/artifacts.py index b22eb29..6222e61 100644 --- a/src/aexp/artifacts.py +++ b/src/aexp/artifacts.py @@ -1,4 +1,4 @@ -"""Create Limina ``kb/`` artifacts (H/E/F) with bidirectional backlinks. +"""Create ``kb/`` artifacts (H/E/F) with bidirectional backlinks. This is the v1.1 surface flagged as planned in ``aexp.kb_io``: rather than hand-rolling markdown from templates per slash-command invocation, diff --git a/src/aexp/backlinks.py b/src/aexp/backlinks.py index 05eae84..1f6afc8 100644 --- a/src/aexp/backlinks.py +++ b/src/aexp/backlinks.py @@ -1,4 +1,4 @@ -"""Bidirectional wiki-link maintenance for Limina ``kb/`` artifacts. +"""Bidirectional wiki-link maintenance for ``kb/`` artifacts. ``kb_validate`` enforces that a child artifact (``F###``) and its parent (``H###``, ``E###``) link each other in their ``## Links`` sections. Creating diff --git a/src/aexp/cli.py b/src/aexp/cli.py index 6ac3ada..0d61b24 100644 --- a/src/aexp/cli.py +++ b/src/aexp/cli.py @@ -66,7 +66,7 @@ app = typer.Typer( name="aex", - help="Agentic Experiments — Limina + signac + W&B fusion layer.", + help="Agentic Experiments — signac + W&B experiment-tracking fusion layer.", no_args_is_help=True, pretty_exceptions_show_locals=False, ) @@ -505,7 +505,7 @@ def new_sandbox_cmd( ) -> None: """Scaffold a new sandbox experiment subdirectory under `notebooks/_sandbox/`. - A sandbox is exploratory free-form work. It's NOT a tracked Limina + A sandbox is exploratory free-form work. It's NOT a tracked research artifact (no H###/E### allocation, no kb_write_guard validation). Promote to the tracked-artifact graph with `/aexp-promote-nb` once a directional hypothesis lands. @@ -643,13 +643,13 @@ def close_thread_cmd( @app.command("new-run") def new_run( - experiment: str = typer.Option(..., "--experiment", help="Limina E### id."), + experiment: str = typer.Option(..., "--experiment", help="E### id."), hypothesis: str | None = typer.Option(None, "--hypothesis"), sub_hypothesis: str | None = typer.Option(None, "--sub-hypothesis"), sp: str | None = typer.Option(None, "--sp", help="KEY=VAL,KEY=VAL state-point params."), no_commit: bool = typer.Option(False, "--no-commit", help="Skip code_commit/code_dirty in sp."), ) -> None: - """Create a signac job linked to a Limina experiment.""" + """Create a signac job linked to an experiment.""" statepoint = _parse_sp_kv(sp) job = create_run( experiment_id=experiment, @@ -669,7 +669,7 @@ def list_runs_cmd( status: str | None = typer.Option(None, "--status"), sp: str | None = typer.Option(None, "--sp", help="Exact-match filter KEY=VAL,..."), ) -> None: - """List signac jobs filtered by Limina link + sp.""" + """List signac jobs filtered by run-link + sp.""" sp_filters = _parse_sp_kv(sp) jobs = find_runs( experiment_id=experiment, @@ -720,7 +720,7 @@ def list_batches_cmd( @app.command("show-run") def show_run(job_id: str) -> None: - """Show state point, doc, linked Limina frame for a run.""" + """Show state point, doc, linked research frame for a run.""" job = open_run(job_id) s = summarize_run(job) console.print(f"[bold]{job.id}[/bold] ({s.batch_slug})") @@ -834,7 +834,7 @@ def validate( kb_only: bool = typer.Option(False, "--kb-only"), runs_only: bool = typer.Option(False, "--runs-only"), ) -> None: - """Validate Limina KB + run-link integrity.""" + """Validate the KB + run-link integrity.""" if kb_only and runs_only: console.print("[red]cannot combine --kb-only and --runs-only[/red]") _exit(2) @@ -1205,7 +1205,7 @@ def _parse_slurm_kwargs( @queue_app.command("add") def queue_add_cmd( - experiment: str = typer.Option(..., "--experiment", help="Limina E### id."), + experiment: str = typer.Option(..., "--experiment", help="E### id."), hypothesis: str | None = typer.Option(None, "--hypothesis"), sp: str | None = typer.Option( None, "--sp", help="Fixed sp values: KEY=VAL,KEY=VAL." diff --git a/src/aexp/hooks/__init__.py b/src/aexp/hooks/__init__.py index 0944e65..918ff88 100644 --- a/src/aexp/hooks/__init__.py +++ b/src/aexp/hooks/__init__.py @@ -13,6 +13,6 @@ is used as a fallback when that assumption does not hold. - Hooks never subprocess into ``scripts/`` files. Validation calls :func:`aexp.kb_validate.validate_kb` in-process. -- Limina upstream's telemetry has been intentionally stripped — ``aexp`` does - not emit to Limina's sink. +- The upstream harness's telemetry has been intentionally stripped — ``aexp`` + does not emit to any external sink. """ diff --git a/src/aexp/install.py b/src/aexp/install.py index 285131d..dadaba6 100644 --- a/src/aexp/install.py +++ b/src/aexp/install.py @@ -1,4 +1,4 @@ -"""Install the vendored Limina harness into a consumer repo. +"""Install the vendored research harness into a consumer repo. ``install_scaffold`` walks the vendored snapshot at ``src/aexp/vendor/limina/`` and applies it to a target repo: @@ -70,7 +70,7 @@ def _find_aexp_source_tree(start: Path) -> Path | None: No legitimate consumer would set this name in their pyproject, so a match is unambiguous evidence that the caller is pointing ``install`` at the dev repo itself — almost always a mistake (the - install would materialize a Limina/signac consumer scaffold inside + install would materialize a signac consumer scaffold inside the package source tree). """ cur = start.resolve() @@ -776,7 +776,7 @@ def install_scaffold( allow_self_install: bool = False, with_jupyter: bool = False, ) -> list[InstallAction]: - """Install the vendored Limina harness into ``repo_root``. + """Install the vendored research harness into ``repo_root``. Parameters ---------- @@ -864,7 +864,7 @@ def install_scaffold( f"`aexp install` materializes a consumer-side scaffold " f"(kb/, templates/, .claude/, .runs/, etc.) — running it " f"inside the package's own source tree pollutes the dev " - f"repo with non-package files and creates a Limina/signac " + f"repo with non-package files and creates a signac " f"project layered on top of itself.\n\n" f"You almost certainly meant to install into a separate " f"consumer repo. `cd` there and re-run.\n\n" @@ -988,7 +988,7 @@ def install_scaffold( ) ) - # 3b. Install Limina's Claude Code skills into <repo>/.claude/skills/. + # 3b. Install the vendored Claude Code skills into <repo>/.claude/skills/. # AGENTS.md references skills like $experiment-rigor; without this step # those references are broken for every consumer repo. actions.extend(_install_skills(root, force=force, dry_run=dry_run)) diff --git a/src/aexp/kb_io.py b/src/aexp/kb_io.py index c67be5d..5f097fd 100644 --- a/src/aexp/kb_io.py +++ b/src/aexp/kb_io.py @@ -1,4 +1,4 @@ -"""Typed read wrappers over Limina ``kb/`` artifacts. +"""Typed read wrappers over the ``kb/`` research artifacts. Reads parse the YAML frontmatter with ``python-frontmatter`` and return a :class:`~aexp.schema.ArtifactRef`. Everything here is read-only; @@ -93,7 +93,7 @@ def find_artifact_path(artifact_id: str, *, kb_root: Path) -> Path: f"no artifact file matching {artifact_id}-*.md under {directory}" ) if len(matches) > 1: - # Limina requires one file per id; kb_validate flags duplicates. Be + # The kb/ layout requires one file per id; kb_validate flags duplicates. Be # conservative here and raise rather than pick silently. raise ArtifactReadError( f"multiple files match {artifact_id}-*.md under {directory}: " @@ -113,7 +113,7 @@ def _extract_title(body: str, fallback: str) -> str: if not m: return fallback raw = m.group(1).strip() - # Limina uses "{ID} — {Title}"; strip the leading id if present. + # Artifact H1s use "{ID} — {Title}"; strip the leading id if present. for sep in (" — ", " - ", "— ", "- "): if sep in raw: _id, _, rest = raw.partition(sep) @@ -123,7 +123,7 @@ def _extract_title(body: str, fallback: str) -> str: def _load_artifact(path: Path, kb_root: Path) -> ArtifactRef: - """Parse a Limina markdown artifact file into a typed reference.""" + """Parse a kb/ markdown artifact file into a typed reference.""" try: text = path.read_text(encoding="utf-8") except OSError as exc: diff --git a/src/aexp/kb_validate.py b/src/aexp/kb_validate.py index d7063f4..1f96750 100644 --- a/src/aexp/kb_validate.py +++ b/src/aexp/kb_validate.py @@ -1,11 +1,11 @@ """KB structural validator — frontmatter, aliases, wikilinks, H->E->F chain. -Ported from the vendored Limina ``kb_validate.py`` into the package. Callable +Ported from the vendored ``kb_validate.py`` into the package. Callable in-process via :func:`validate_kb`; :func:`main` preserves a ``python -m aexp.kb_validate`` CLI for parity with the old script invocation. -Upstream Limina's optional telemetry hooks are intentionally dropped — ``aexp`` -does not emit to Limina's telemetry sink. +The upstream validator's optional telemetry hooks are intentionally dropped +— ``aexp`` does not emit to any external telemetry sink. """ from __future__ import annotations @@ -121,7 +121,7 @@ def as_json(self) -> dict[str, object]: def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Validate the Limina kb/ research graph.") + parser = argparse.ArgumentParser(description="Validate the kb/ research graph.") parser.add_argument("--kb-root", default="./kb", help="Path to kb/ (default: ./kb)") parser.add_argument("--check-file", default=None, help="Validate one file in isolation") parser.add_argument("--format", choices=("text", "json"), default="text") @@ -752,7 +752,7 @@ def validate_kb( *, check_file: Path | str | None = None, ) -> ValidationResult: - """Validate a Limina ``kb/`` tree (or a single file inside it). + """Validate a ``kb/`` tree (or a single file inside it). Pure in-process entry point — no I/O beyond reading the KB tree, no CLI side effects. Hooks, ``aexp validate``, and tests all call this directly. diff --git a/src/aexp/mcp_server.py b/src/aexp/mcp_server.py index 7682cb0..64af933 100644 --- a/src/aexp/mcp_server.py +++ b/src/aexp/mcp_server.py @@ -156,7 +156,7 @@ def new_hypothesis( extra_links: list[str] | None = None, thread_id: str | None = None, ) -> dict[str, Any]: - """Create a new Limina hypothesis (H###). + """Create a new hypothesis (H###). Writes ``kb/research/hypotheses/H###-<slug>.md`` with a validator-clean skeleton (frontmatter, blockquote metadata, ``## Links`` pre-populated @@ -190,7 +190,7 @@ def new_experiment( artifact_id: str | None = None, extra_links: list[str] | None = None, ) -> dict[str, Any]: - """Create a new Limina experiment (E###) under an existing hypothesis. + """Create a new experiment (E###) under an existing hypothesis. Writes the experiment skeleton and patches the parent H###'s ``## Links`` section with ``- [[E###]]`` so ``kb_validate`` passes. @@ -222,7 +222,7 @@ def new_finding( artifact_id: str | None = None, extra_links: list[str] | None = None, ) -> dict[str, Any]: - """Create a new Limina finding (F###) citing a hypothesis + experiment. + """Create a new finding (F###) citing a hypothesis + experiment. Writes the finding skeleton and patches both parent files' ``## Links`` sections. The ``supporting_runs:`` frontmatter list stays empty — add it @@ -261,7 +261,7 @@ def new_thread( artifact_id: str | None = None, extra_links: list[str] | None = None, ) -> dict[str, Any]: - """Create a new Limina thread (T###). + """Create a new thread (T###). A thread is a forward-looking research concern broader than a single hypothesis — it captures exploration that may spawn 2–5 hypotheses @@ -392,10 +392,10 @@ def new_run( experiment_path: str | None = None, include_commit: bool = True, ) -> dict[str, Any]: - """Create (or reopen) a signac job linked to a Limina experiment. + """Create (or reopen) a signac job linked to an experiment. Args: - experiment_id: Limina E### id this run is testing. + experiment_id: The E### id this run is testing. statepoint: Identity-defining params (model, condition, seed, ...). hypothesis_id: Optional H### the run tests; defaults to the experiment's primary. sub_hypothesis_id: Optional narrower hypothesis within the experiment. @@ -599,7 +599,7 @@ def bind_tracker( @mcp.tool() def validate(mode: str = "full") -> dict[str, Any]: - """Validate the Limina KB + signac run-link integrity. + """Validate the KB + signac run-link integrity. Args: mode: "full" (default), "kb-only", or "runs-only". @@ -706,7 +706,7 @@ def queue_add( """Register one or more pending runs (``status="queued"``). Args: - experiment_id: Limina E### the runs test. + experiment_id: The E### the runs test. statepoint: Fixed sp values applied to every job. sweep: Optional Cartesian-sweep spec, e.g. ``"condition=full|classify_only, seed=0..3"``. Expanded and diff --git a/src/aexp/sandbox.py b/src/aexp/sandbox.py index 7ed5353..9e2ea80 100644 --- a/src/aexp/sandbox.py +++ b/src/aexp/sandbox.py @@ -7,7 +7,7 @@ initializes the sandbox root (``notebooks/_sandbox/`` + its README + ``.gitignore``). -A sandbox is *not* a tracked Limina artifact (no ``H###``/``E###`` +A sandbox is *not* a tracked research artifact (no ``H###``/``E###`` allocated, no ``kb_write_guard`` validation). It's a free-form exploratory workspace whose conventions are encoded by the scaffolder. Promote a sandbox experiment to the tracked-artifact graph with diff --git a/src/aexp/schema.py b/src/aexp/schema.py index 5d8e8d0..9173ac6 100644 --- a/src/aexp/schema.py +++ b/src/aexp/schema.py @@ -172,13 +172,13 @@ class BatchSelector(BaseModel): # --------------------------------------------------------------------------- -# Limina artifact reference (read-only handle returned by kb_io) +# Artifact reference (read-only handle returned by kb_io) # --------------------------------------------------------------------------- @dataclass(frozen=True) class ArtifactRef: - """A typed pointer to one Limina artifact on disk. + """A typed pointer to one research artifact on disk. Returned by ``kb_io.load_*`` helpers. The raw frontmatter dict is exposed as ``metadata`` so callers can read fields we don't model. diff --git a/src/aexp/slash_commands/aexp-finding-from-batch.md b/src/aexp/slash_commands/aexp-finding-from-batch.md index 4f74bb3..49b3ffe 100644 --- a/src/aexp/slash_commands/aexp-finding-from-batch.md +++ b/src/aexp/slash_commands/aexp-finding-from-batch.md @@ -1,5 +1,5 @@ --- -description: "Create a Limina finding (F###) citing a batch of runs by selector." +description: "Create a finding (F###) citing a batch of runs by selector." --- Create a new finding that cites a batch selector — typically all runs diff --git a/src/aexp/slash_commands/aexp-finding-from-run.md b/src/aexp/slash_commands/aexp-finding-from-run.md index 2d3cd75..74366ac 100644 --- a/src/aexp/slash_commands/aexp-finding-from-run.md +++ b/src/aexp/slash_commands/aexp-finding-from-run.md @@ -1,5 +1,5 @@ --- -description: "Create a Limina finding (F###) citing one specific signac run." +description: "Create a finding (F###) citing one specific signac run." --- Create a new finding that cites a single signac run as its supporting diff --git a/src/aexp/slash_commands/aexp-finding-placeholder.md b/src/aexp/slash_commands/aexp-finding-placeholder.md index b1b2d4e..c1750d1 100644 --- a/src/aexp/slash_commands/aexp-finding-placeholder.md +++ b/src/aexp/slash_commands/aexp-finding-placeholder.md @@ -1,5 +1,5 @@ --- -description: "Create a Limina finding (F###) with no run citations yet (synthesis / deferred)." +description: "Create a finding (F###) with no run citations yet (synthesis / deferred)." --- Create a new finding skeleton that cites a hypothesis + experiment but has diff --git a/src/aexp/slash_commands/aexp-list-threads.md b/src/aexp/slash_commands/aexp-list-threads.md index 4d27832..b17b828 100644 --- a/src/aexp/slash_commands/aexp-list-threads.md +++ b/src/aexp/slash_commands/aexp-list-threads.md @@ -1,5 +1,5 @@ --- -description: "List Limina threads, optionally filtered by status or tag." +description: "List research threads, optionally filtered by status or tag." --- Print every thread under ``kb/research/threads/`` with its current diff --git a/src/aexp/slash_commands/aexp-new-experiment.md b/src/aexp/slash_commands/aexp-new-experiment.md index 04738a7..b744db0 100644 --- a/src/aexp/slash_commands/aexp-new-experiment.md +++ b/src/aexp/slash_commands/aexp-new-experiment.md @@ -1,5 +1,5 @@ --- -description: "Create a new Limina experiment (E###) under an existing hypothesis." +description: "Create a new experiment (E###) under an existing hypothesis." --- Create a new experiment artifact under an existing hypothesis. ``aexp`` diff --git a/src/aexp/slash_commands/aexp-new-hypothesis.md b/src/aexp/slash_commands/aexp-new-hypothesis.md index 7889827..48a2f61 100644 --- a/src/aexp/slash_commands/aexp-new-hypothesis.md +++ b/src/aexp/slash_commands/aexp-new-hypothesis.md @@ -1,5 +1,5 @@ --- -description: "Create a new Limina hypothesis (H###) with a validator-clean skeleton." +description: "Create a new hypothesis (H###) with a validator-clean skeleton." --- Create a new hypothesis artifact. `aexp` handles id allocation, template diff --git a/src/aexp/slash_commands/aexp-new-run.md b/src/aexp/slash_commands/aexp-new-run.md index 3718dc4..e8ccae0 100644 --- a/src/aexp/slash_commands/aexp-new-run.md +++ b/src/aexp/slash_commands/aexp-new-run.md @@ -1,8 +1,8 @@ --- -description: "Create a new signac run linked to a Limina experiment." +description: "Create a new signac run linked to an experiment." --- -Create a tracked run for an existing Limina experiment. +Create a tracked run for an existing experiment. > **Invocation note.** The examples below use `python -m aexp` > directly. If running from a Claude Code session where `python` does not diff --git a/src/aexp/slash_commands/aexp-new-sandbox.md b/src/aexp/slash_commands/aexp-new-sandbox.md index c43c49d..04ad823 100644 --- a/src/aexp/slash_commands/aexp-new-sandbox.md +++ b/src/aexp/slash_commands/aexp-new-sandbox.md @@ -12,7 +12,7 @@ Create a new sandbox experiment directory for exploratory notebook work. `aexp` ## What this is -A **sandbox** is exploratory free-form work that hasn't yet promoted to a tracked Limina artifact. It's *not* in the H/E/F chain — no `kb_write_guard` validation applies, no artifact id is allocated. It's a working surface where iteration is cheap and reversible (`git checkout <slug-dir>` undoes everything). +A **sandbox** is exploratory free-form work that hasn't yet promoted to a tracked research artifact. It's *not* in the H/E/F chain — no `kb_write_guard` validation applies, no artifact id is allocated. It's a working surface where iteration is cheap and reversible (`git checkout <slug-dir>` undoes everything). Promote a sandbox experiment to the tracked-artifact graph when its result is going to be cited as a paper finding: diff --git a/src/aexp/slash_commands/aexp-new-thread.md b/src/aexp/slash_commands/aexp-new-thread.md index ad28e96..a17c533 100644 --- a/src/aexp/slash_commands/aexp-new-thread.md +++ b/src/aexp/slash_commands/aexp-new-thread.md @@ -1,5 +1,5 @@ --- -description: "Create a new Limina thread (T###) — forward-looking research concern broader than a hypothesis." +description: "Create a new research thread (T###) — forward-looking research concern broader than a hypothesis." --- Create a thread artifact. **Threads are not hypotheses.** A hypothesis diff --git a/src/aexp/slash_commands/aexp-show-run.md b/src/aexp/slash_commands/aexp-show-run.md index 736cca7..b07d819 100644 --- a/src/aexp/slash_commands/aexp-show-run.md +++ b/src/aexp/slash_commands/aexp-show-run.md @@ -1,5 +1,5 @@ --- -description: "Show state point, doc, and linked Limina frame for one signac run." +description: "Show state point, doc, and linked research frame for one signac run." --- Show full detail for a single signac run — its state point, doc, status, diff --git a/tests/conftest.py b/tests/conftest.py index aa929db..afd8d0a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,14 +13,14 @@ @pytest.fixture def vendored_tree() -> Path: - """Absolute path to the vendored Limina snapshot in this repo.""" - assert VENDOR_ROOT.is_dir(), f"vendored Limina missing at {VENDOR_ROOT}" + """Absolute path to the vendored research-harness snapshot in this repo.""" + assert VENDOR_ROOT.is_dir(), f"vendored research harness missing at {VENDOR_ROOT}" return VENDOR_ROOT @pytest.fixture def scaffold_project(tmp_path: Path) -> Path: - """Copy the vendored Limina snapshot into a tmp dir. + """Copy the vendored research-harness snapshot into a tmp dir. Gives each test an isolated ``PROJECT_ROOT`` — the ported hooks derive their root from ``Path(__file__).resolve().parents[2]``, so running a diff --git a/tests/test_cli.py b/tests/test_cli.py index a699d0e..938d641 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -35,7 +35,7 @@ def runner(monkeypatch: pytest.MonkeyPatch) -> CliRunner: @pytest.fixture def installed_repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: - """Install Limina, chdir into the repo, and return its path.""" + """Install the aexp scaffold, chdir into the repo, and return its path.""" repo = tmp_path / "repo" repo.mkdir() _git_commit(repo) diff --git a/tests/test_hooks_python.py b/tests/test_hooks_python.py index 3b38328..1d293cf 100644 --- a/tests/test_hooks_python.py +++ b/tests/test_hooks_python.py @@ -1,4 +1,4 @@ -"""Tests for the aexp hooks (ported from vendored Limina scripts). +"""Tests for the aexp hooks (ported from the vendored harness scripts). Covers: @@ -260,12 +260,12 @@ def test_kb_write_guard_blocks_invalid_md( """A malformed artifact under kb/research/ triggers kb_validate -> blocked.""" target = scaffold_project / "kb" / "research" / "hypotheses" / "H050-bogus.md" target.parent.mkdir(parents=True, exist_ok=True) - target.write_text("this is not a valid Limina artifact", encoding="utf-8") + target.write_text("this is not a valid kb/ artifact", encoding="utf-8") payload = { "tool_input": { "file_path": str(target), - "content": "this is not a valid Limina artifact", + "content": "this is not a valid kb/ artifact", } } r = _run_hook(scaffold_project, "kb_write_guard", payload, python_exe) @@ -281,7 +281,7 @@ def test_kb_write_guard_blocks_invalid_md( def test_stop_validate_passes_on_clean_kb( scaffold_project: Path, python_exe: str ) -> None: - """Vendored Limina's shipped kb/ template validates cleanly out of the box.""" + """The vendored shipped kb/ template validates cleanly out of the box.""" r = _run_hook(scaffold_project, "stop_validate", None, python_exe, timeout=30) assert r.returncode == 0, (r.returncode, r.stdout, r.stderr) diff --git a/tests/test_kb_io.py b/tests/test_kb_io.py index 5cc4c8a..287bdde 100644 --- a/tests/test_kb_io.py +++ b/tests/test_kb_io.py @@ -31,7 +31,7 @@ def _write_artifact( frontmatter: dict[str, object], body: str, ) -> Path: - """Write a minimal Limina-shaped artifact to disk.""" + """Write a minimal kb/-shaped artifact to disk.""" fm_lines = ["---"] for k, v in frontmatter.items(): if isinstance(v, list): diff --git a/tests/test_runs.py b/tests/test_runs.py index 431f9c0..4b1a76f 100644 --- a/tests/test_runs.py +++ b/tests/test_runs.py @@ -1,4 +1,4 @@ -"""Tests for the signac-backed run store + Limina-aware run API.""" +"""Tests for the signac-backed run store + research-aware run API.""" from __future__ import annotations import subprocess @@ -36,7 +36,7 @@ def _git_init(path: Path) -> None: @pytest.fixture def installed_repo(tmp_path: Path) -> Path: - """A tmp dir with a git repo + Limina installed + signac initialized.""" + """A tmp dir with a git repo + aexp scaffold installed + signac initialized.""" repo = tmp_path / "repo" repo.mkdir() _git_init(repo) diff --git a/tests/test_sandbox.py b/tests/test_sandbox.py index 3079fe4..829e785 100644 --- a/tests/test_sandbox.py +++ b/tests/test_sandbox.py @@ -31,7 +31,7 @@ def _git_init_repo(repo: Path) -> None: def repo(tmp_path: Path) -> Path: """A fresh git-initialized repo for sandbox tests. - Sandbox scaffolding doesn't require Limina to be installed + Sandbox scaffolding doesn't require the aexp scaffold to be installed (it's not a tracked artifact), so we skip the install step that test_artifacts.py uses. """ diff --git a/tests/test_validate.py b/tests/test_validate.py index 158def9..4afc7a3 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -34,7 +34,7 @@ def _write_artifact( *, links: list[str] | None = None, ) -> Path: - """Write a minimally-conforming Limina artifact (aliases + Links section). + """Write a minimally-conforming kb/ artifact (aliases + Links section). kb_validate requires an ``aliases`` frontmatter entry matching the id and a ``## Links`` section listing wikilinks to related artifacts; without @@ -303,7 +303,7 @@ def test_validate_surfaces_kb_validate_errors(installed_repo: Path) -> None: def test_validate_flags_orphan_run(installed_repo: Path) -> None: - # Create a job then wipe its Limina link so it becomes orphan. + # Create a job then wipe its run link so it becomes orphan. job = create_run( experiment_id="E001", statepoint={"c": "f"}, From 9fb822d329197acbaab34a0a3772e2704d7ab970 Mon Sep 17 00:00:00 2001 From: Kaden McKeen <mckeenkaden@gmail.com> Date: Wed, 20 May 2026 23:33:03 -0400 Subject: [PATCH 05/10] test(debrand): cover the run-link dual-read fallback Stage B introduced schema.read_run_link() / write_run_link() -- the read-side fallback that lets pre-de-brand signac job docs (legacy job.doc["limina"]) keep resolving with no migration. Add direct unit tests for that contract: current-key read, legacy-key fallback, current-wins-over-legacy, empty-when-absent, and write-clears-legacy (self-heal). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- tests/test_schema.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_schema.py b/tests/test_schema.py index 7f0e19f..a3b678b 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -14,6 +14,8 @@ TrackerBinding, batch_slug, iso_utc_now, + read_run_link, + write_run_link, ) # --------------------------------------------------------------------------- @@ -51,6 +53,38 @@ def test_runlink_is_frozen() -> None: link.experiment_id = "E002" # type: ignore[misc] +# --------------------------------------------------------------------------- +# read_run_link / write_run_link — run-link key with legacy fallback +# --------------------------------------------------------------------------- + + +def test_read_run_link_reads_current_key() -> None: + assert read_run_link({"aexp": {"experiment_id": "E001"}}) == {"experiment_id": "E001"} + + +def test_read_run_link_falls_back_to_legacy_limina_key() -> None: + # Runs stamped before the de-brand used the "limina" key; they must + # still resolve so existing signac projects need no migration. + assert read_run_link({"limina": {"experiment_id": "E009"}}) == {"experiment_id": "E009"} + + +def test_read_run_link_prefers_current_key_over_legacy() -> None: + doc = {"aexp": {"experiment_id": "E_new"}, "limina": {"experiment_id": "E_old"}} + assert read_run_link(doc)["experiment_id"] == "E_new" + + +def test_read_run_link_empty_when_absent() -> None: + assert read_run_link({"status": "created"}) == {} + + +def test_write_run_link_writes_current_and_clears_legacy() -> None: + doc = {"limina": {"experiment_id": "E_old"}} + write_run_link(doc, {"experiment_id": "E_new"}) + assert doc["aexp"] == {"experiment_id": "E_new"} + # Legacy key is cleared so a re-stamped job self-heals to the new key. + assert "limina" not in doc + + # --------------------------------------------------------------------------- # SupportingRun union # --------------------------------------------------------------------------- From c710cb6e06bb5a0d40d10bac679f90da7cf34510 Mon Sep 17 00:00:00 2001 From: Kaden McKeen <mckeenkaden@gmail.com> Date: Thu, 21 May 2026 10:42:10 -0400 Subject: [PATCH 06/10] =?UTF-8?q?refactor(debrand):=20stage=20E=20?= =?UTF-8?q?=E2=80=94=20kill=20the=20vendor/=20directory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The de-brand left `src/aexp/vendor/limina/` in place — a `vendor/` directory with a single child, which reads as careless. Move the bundled research-harness scaffold to its real home and retire the "vendor" vocabulary: - `src/aexp/vendor/limina/` -> `src/aexp/scaffold/`; both the `vendor/` and `limina/` directories are gone. Drop the vendoring-ceremony files `VENDORED_FROM.txt` and `VERSION` (nothing read them; the upstream credit lives in the README). - Identifiers: `compute_vendor_sha` -> `compute_scaffold_sha`, `VENDOR_ROOT` -> `SCAFFOLD_ROOT`, install-marker field `vendor_sha` -> `scaffold_sha`, `_VENDOR_TEMPLATES*` -> `_SCAFFOLD_TEMPLATES*`, the `merge_claude_settings` / `block_merge_markdown` `vendor*` params/locals -> `shipped*`, the `vendored_tree` fixture -> `scaffold_tree`. - Docstrings, comments, docs, README, CHANGELOG, pyproject: "vendor" / "vendored" -> "scaffold" / "bundled" / rephrased. - pyproject: drop the dead `reference/` excludes (no such dir) and the now-unnecessary ruff `extend-exclude` (the scaffold tree has no .py). `scaffold_sha` keeps the dual-read fallback to the shipped legacy marker key `limina_vendor_sha`, so existing install markers still resolve. `src/aexp/scaffold/` ships in the wheel the same way the old `vendor/` tree did — it is under the `aexp` package. Part of the limina de-brand (PR #23: stages A-E). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- CHANGELOG.md | 20 ++-- README.md | 6 +- docs/concepts.md | 10 +- docs/mapping.md | 2 +- pyproject.toml | 12 +-- src/aexp/__init__.py | 4 +- src/aexp/artifacts.py | 10 +- src/aexp/cli.py | 4 +- src/aexp/install.py | 96 +++++++++---------- src/aexp/kb_validate.py | 10 +- src/aexp/sandbox.py | 2 +- .../{vendor/limina => scaffold}/AGENTS.md | 0 .../{vendor/limina => scaffold}/CLAUDE.md | 0 .../docs/setup/jupyter-mcp.md | 2 +- .../{vendor/limina => scaffold}/kb/ACTIVE.md | 0 .../limina => scaffold}/kb/DASHBOARD.md | 0 .../limina => scaffold}/kb/lessons/README.md | 0 .../kb/mission/CHALLENGE.md | 0 .../limina => scaffold}/kb/reports/.gitkeep | 0 .../kb/research/data/.gitkeep | 0 .../kb/research/experiments/.gitkeep | 0 .../kb/research/findings/.gitkeep | 0 .../kb/research/hypotheses/.gitkeep | 0 .../kb/research/literature/.gitkeep | 0 .../kb/research/threads/.gitkeep | 0 .../build-maintainable-software/SKILL.md | 0 .../agents/openai.yaml | 0 .../evals/trigger-prompts.csv | 0 .../references/design-principles.md | 0 .../references/review-checklist.md | 0 .../skills/experiment-rigor/SKILL.md | 0 .../experiment-rigor/agents/openai.yaml | 0 .../evals/trigger-prompts.csv | 0 .../references/experiment-rubric.md | 0 .../references/hypothesis-rubric.md | 0 .../references/metrics-storage.md | 0 .../skills/exploratory-sota-research/SKILL.md | 0 .../agents/openai.yaml | 0 .../evals/trigger-prompts.csv | 0 .../references/output-template.md | 0 .../references/source-selection.md | 0 .../worked-example-information-retrieval.md | 0 .../skills/research-devil-advocate/SKILL.md | 0 .../agents/openai.yaml | 0 .../evals/trigger-prompts.csv | 0 .../references/review-rubric.md | 0 .../limina => scaffold}/templates/active.md | 0 .../templates/challenge-review.md | 0 .../templates/experiment.md | 0 .../limina => scaffold}/templates/finding.md | 0 .../templates/hypothesis.md | 0 .../templates/literature.md | 0 .../limina => scaffold}/templates/report.md | 0 .../templates/strategic-review.md | 0 .../limina => scaffold}/templates/thread.md | 0 src/aexp/schema.py | 2 +- src/aexp/utils/atomic.py | 4 +- src/aexp/utils/paths.py | 10 +- src/aexp/validate.py | 2 +- src/aexp/vendor/limina/VENDORED_FROM.txt | 16 ---- src/aexp/vendor/limina/VERSION | 1 - tests/conftest.py | 14 +-- tests/test_artifacts.py | 8 +- tests/test_e2e_smoke.py | 2 +- tests/test_hooks_python.py | 4 +- tests/test_install.py | 58 +++++------ tests/test_utils.py | 14 +-- 67 files changed, 147 insertions(+), 166 deletions(-) rename src/aexp/{vendor/limina => scaffold}/AGENTS.md (100%) rename src/aexp/{vendor/limina => scaffold}/CLAUDE.md (100%) rename src/aexp/{vendor/limina => scaffold}/docs/setup/jupyter-mcp.md (99%) rename src/aexp/{vendor/limina => scaffold}/kb/ACTIVE.md (100%) rename src/aexp/{vendor/limina => scaffold}/kb/DASHBOARD.md (100%) rename src/aexp/{vendor/limina => scaffold}/kb/lessons/README.md (100%) rename src/aexp/{vendor/limina => scaffold}/kb/mission/CHALLENGE.md (100%) rename src/aexp/{vendor/limina => scaffold}/kb/reports/.gitkeep (100%) rename src/aexp/{vendor/limina => scaffold}/kb/research/data/.gitkeep (100%) rename src/aexp/{vendor/limina => scaffold}/kb/research/experiments/.gitkeep (100%) rename src/aexp/{vendor/limina => scaffold}/kb/research/findings/.gitkeep (100%) rename src/aexp/{vendor/limina => scaffold}/kb/research/hypotheses/.gitkeep (100%) rename src/aexp/{vendor/limina => scaffold}/kb/research/literature/.gitkeep (100%) rename src/aexp/{vendor/limina => scaffold}/kb/research/threads/.gitkeep (100%) rename src/aexp/{vendor/limina => scaffold}/skills/build-maintainable-software/SKILL.md (100%) rename src/aexp/{vendor/limina => scaffold}/skills/build-maintainable-software/agents/openai.yaml (100%) rename src/aexp/{vendor/limina => scaffold}/skills/build-maintainable-software/evals/trigger-prompts.csv (100%) rename src/aexp/{vendor/limina => scaffold}/skills/build-maintainable-software/references/design-principles.md (100%) rename src/aexp/{vendor/limina => scaffold}/skills/build-maintainable-software/references/review-checklist.md (100%) rename src/aexp/{vendor/limina => scaffold}/skills/experiment-rigor/SKILL.md (100%) rename src/aexp/{vendor/limina => scaffold}/skills/experiment-rigor/agents/openai.yaml (100%) rename src/aexp/{vendor/limina => scaffold}/skills/experiment-rigor/evals/trigger-prompts.csv (100%) rename src/aexp/{vendor/limina => scaffold}/skills/experiment-rigor/references/experiment-rubric.md (100%) rename src/aexp/{vendor/limina => scaffold}/skills/experiment-rigor/references/hypothesis-rubric.md (100%) rename src/aexp/{vendor/limina => scaffold}/skills/experiment-rigor/references/metrics-storage.md (100%) rename src/aexp/{vendor/limina => scaffold}/skills/exploratory-sota-research/SKILL.md (100%) rename src/aexp/{vendor/limina => scaffold}/skills/exploratory-sota-research/agents/openai.yaml (100%) rename src/aexp/{vendor/limina => scaffold}/skills/exploratory-sota-research/evals/trigger-prompts.csv (100%) rename src/aexp/{vendor/limina => scaffold}/skills/exploratory-sota-research/references/output-template.md (100%) rename src/aexp/{vendor/limina => scaffold}/skills/exploratory-sota-research/references/source-selection.md (100%) rename src/aexp/{vendor/limina => scaffold}/skills/exploratory-sota-research/references/worked-example-information-retrieval.md (100%) rename src/aexp/{vendor/limina => scaffold}/skills/research-devil-advocate/SKILL.md (100%) rename src/aexp/{vendor/limina => scaffold}/skills/research-devil-advocate/agents/openai.yaml (100%) rename src/aexp/{vendor/limina => scaffold}/skills/research-devil-advocate/evals/trigger-prompts.csv (100%) rename src/aexp/{vendor/limina => scaffold}/skills/research-devil-advocate/references/review-rubric.md (100%) rename src/aexp/{vendor/limina => scaffold}/templates/active.md (100%) rename src/aexp/{vendor/limina => scaffold}/templates/challenge-review.md (100%) rename src/aexp/{vendor/limina => scaffold}/templates/experiment.md (100%) rename src/aexp/{vendor/limina => scaffold}/templates/finding.md (100%) rename src/aexp/{vendor/limina => scaffold}/templates/hypothesis.md (100%) rename src/aexp/{vendor/limina => scaffold}/templates/literature.md (100%) rename src/aexp/{vendor/limina => scaffold}/templates/report.md (100%) rename src/aexp/{vendor/limina => scaffold}/templates/strategic-review.md (100%) rename src/aexp/{vendor/limina => scaffold}/templates/thread.md (100%) delete mode 100644 src/aexp/vendor/limina/VENDORED_FROM.txt delete mode 100644 src/aexp/vendor/limina/VERSION diff --git a/CHANGELOG.md b/CHANGELOG.md index aba6af7..442a55d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,12 +32,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 cleanup. The cluster-side `[jupyter]` extra and `aexp jupyter setup` extension recipe are unchanged. -- **De-branded the vendored "limina" research harness.** `limina` - (vendored 2026-04-20) is no longer surfaced as a named centerpiece — - the harness reads as plain `aexp`. **Breaking** public-API renames - (old names removed; one shim — see below): +- **De-branded and de-vendored the "limina" research harness.** `limina` + (the upstream project the harness was adapted from) is no longer + surfaced as a named centerpiece, and the `vendor/` directory framing + is gone — the harness reads as plain `aexp`. **Breaking** public-API + renames (old names removed; one shim — see below): - `install_limina()` → `install_scaffold()` - `is_limina_installed()` → `is_scaffold_installed()` + - `compute_vendor_sha()` → `compute_scaffold_sha()` - `LiminaArtifactRef` → `ArtifactRef` - module `aexp.limina_io` → `aexp.kb_io`. `aexp.limina_io` is kept as a deprecation shim (re-exports `aexp.kb_io`, emits a @@ -46,17 +48,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Persisted keys are renamed with a **read-side fallback**, so existing signac projects and install markers keep resolving with no migration: - run-link key `job.doc["limina"]` → `job.doc["aexp"]` - - install-marker field `limina_vendor_sha` → `vendor_sha` + - install-marker field `limina_vendor_sha` → `scaffold_sha` - W&B run-config block `config["limina"]` → `config["aexp"]` (past W&B runs keep `config.limina`; new runs get `config.aexp`) - validator error codes `limina.validation_failed` / `limina.validator_unavailable` → `aexp.*` - The vendored directory `src/aexp/vendor/limina/` keeps its name (a - vendoring convention; the upstream credit is in the README). Its + The bundled harness moved from `src/aexp/vendor/limina/` to + `src/aexp/scaffold/` — the `vendor/` directory and the vendoring + ceremony files (`VENDORED_FROM.txt`, `VERSION`) are gone. Its contents, the slash commands, `AGENTS.md` / `CLAUDE.md`, and the docs are de-branded. The stale top-level "limina" skill was removed — - `aexp install` already scaffolds a project. + `aexp install` already scaffolds a project. The upstream credit lives + in the README. ### Fixed diff --git a/README.md b/README.md index 4d6872c..e6bcbc3 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ - **Hypothesis-first, not metric-first** — you can't start a run without a live hypothesis; you can't ship a finding without cited runs - **Git is the source of truth** — every run carries its commit SHA; the knowledge base lives in git; nothing load-bearing is ephemeral -- **Integrate, don't reinvent** — [signac](https://signac.readthedocs.io) for run state, [W&B](https://wandb.ai/) for observability, and a vendored research harness for the H→E→F artifact model, templates, and methodology skills. `aexp` is the glue and the discipline +- **Integrate, don't reinvent** — [signac](https://signac.readthedocs.io) for run state, [W&B](https://wandb.ai/) for observability, and a bundled research harness for the H→E→F artifact model, templates, and methodology skills. `aexp` is the glue and the discipline - **Portable by default** — the MCP server runs via `uvx` from PyPI; `.mcp.json` is identical on every machine and committable to git --- @@ -271,7 +271,7 @@ src/aexp/ slash_commands/ # /aexp-* templates trackers/ # TrackerAdapter ABC + noop + wandb adapters utils/ # paths, git, atomic writes - vendor/ # forked research-graph templates, skills, and kb/ scaffold + scaffold/ # research-graph scaffold: kb/, templates, skills, agent contracts tests/ # pytest suite; CI on Ubuntu + Windows × Py 3.11/3.12/3.13 docs/ # concepts, quickstart, cli, mcp, mapping, tracker-adapters, queue, threads, sandbox, airgapped ``` @@ -331,7 +331,7 @@ Every edit to `src/aexp/*.py` is now live in: ## Acknowledgements -The vendored research harness — the H→E→F artifact model, the `kb/` layout, +The research harness — the H→E→F artifact model, the `kb/` layout, artifact templates, and methodology skills — was adapted from [limina](https://github.com/KadenMc/limina). diff --git a/docs/concepts.md b/docs/concepts.md index a16e110..287aae1 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -2,7 +2,7 @@ `agentic-experiments` is a **fusion layer**, not a new framework. It pairs a research-graph harness (the H→E→F artifact model, templates, and methodology -skills — vendored from [limina](https://github.com/KadenMc/limina)) with +skills — adapted from [limina](https://github.com/KadenMc/limina)) with [signac](https://signac.readthedocs.io) for local execution and run state, and bridges to W&B for optional remote observability. @@ -74,7 +74,7 @@ consumer-repo/ .signac/ workspace/<job_id>/ .aexp/ - installed.json # version + run_store_path + python_exe + vendor sha + installed.json # version + run_store_path + python_exe + scaffold sha ``` Hook scripts and validator code live inside the installed `aexp` @@ -95,13 +95,13 @@ passes) while still containing broken `supporting_runs` citations. The Stop hook does not catch them. Run `python -m aexp validate` explicitly before considering a session "complete." -## Why vendor the research harness +## Why bundle the research harness -The upstream [limina](https://github.com/KadenMc/limina) project ships a template-clone flow (`clone + rm .git + re-init`) that doesn't compose with *applying* a harness to an existing repo. So `aexp` vendors the pieces it needs: +The upstream [limina](https://github.com/KadenMc/limina) project ships a template-clone flow (`clone + rm .git + re-init`) that doesn't compose with *applying* a harness to an existing repo. So `aexp` bundles the pieces it needs: - Hook behavior has been ported into `aexp.hooks.*` and is invoked as Python modules from the installed package. - The KB structural validator lives at `aexp.kb_validate` — in-process, no subprocess dance. -- `src/aexp/vendor/` retains the research-graph data assets that *do* belong in every consumer repo: the `kb/` scaffold, artifact `templates/`, and the four methodology skills (`experiment-rigor`, `exploratory-sota-research`, `research-devil-advocate`, `build-maintainable-software`). These are the parts the agent actually reads and writes; keeping them checked into `aexp` lets `aexp install` drop them in verbatim, with merge policies that preserve user customizations. +- `src/aexp/scaffold/` holds the research-graph data assets that *do* belong in every consumer repo: the `kb/` scaffold, artifact `templates/`, and the four methodology skills (`experiment-rigor`, `exploratory-sota-research`, `research-devil-advocate`, `build-maintainable-software`). These are the parts the agent actually reads and writes; keeping them checked into `aexp` lets `aexp install` drop them in verbatim, with merge policies that preserve user customizations. One-time fork — no resync. diff --git a/docs/mapping.md b/docs/mapping.md index caaacf5..0b0febd 100644 --- a/docs/mapping.md +++ b/docs/mapping.md @@ -114,7 +114,7 @@ supporting_runs: | Code | Meaning | Fix | |---|---|---| -| `aexp.validation_failed` | Vendored `kb_validate.py` reported errors | Read the details; usually missing `## Links`, missing frontmatter field, broken wikilink | +| `aexp.validation_failed` | The bundled `kb_validate.py` reported errors | Read the details; usually missing `## Links`, missing frontmatter field, broken wikilink | | `run.orphan` | A signac job has no `doc["aexp"]` | `aexp link <job_id> --experiment E###` | | `run.broken_experiment_link` | Run references an E### with no file on disk | Fix the link, or create the experiment via `kb_new_artifact.py` | | `run.hypothesis_mismatch` | Run's `hypothesis_id` isn't the experiment's primary or a sub | Fix the run or add the hypothesis to the experiment's `sub_hypotheses:` | diff --git a/pyproject.toml b/pyproject.toml index a861e42..5f506f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "agentic-experiments" version = "0.4.0" -description = "Git-first, hypothesis-forcing experiment tracking for agent-driven ML research. Vendors a research harness for the H->E->F artifact model, uses signac for local execution/run state, and bridges to W&B for remote observability." +description = "Git-first, hypothesis-forcing experiment tracking for agent-driven ML research. Bundles a research harness for the H->E->F artifact model, uses signac for local execution/run state, and bridges to W&B for remote observability." authors = [ {name = "Kaden McKeen", email = "mckeenkaden@gmail.com"} ] @@ -59,7 +59,7 @@ mcp = ["mcp (>=1.2,<2.0)"] # combo from the electricrag deployment (see docs/setup/jupyter-mcp.md # "Environment reference"). `jupyterlab` is intentionally NOT pinned here: # consumers may want their own JupyterLab version; the tested 4.4.1 value -# is documented in the vendored setup doc but not forced via resolution. +# is documented in the bundled setup doc but not forced via resolution. jupyter = [ "jupyter-collaboration (>=4.0.0,<5.0.0)", "jupyter-mcp-tools (>=0.1.6,<0.2.0)", @@ -99,9 +99,7 @@ include = [ { path = "CHANGELOG.md", format = ["sdist"] }, { path = "docs/**/*.md", format = ["sdist"] }, ] -# Everything under reference/ is dev-only diff provenance, never shipped. exclude = [ - "reference/**", "**/__pycache__", "**/*.pyc", "**/.pytest_cache", @@ -114,10 +112,6 @@ exclude = [ [tool.ruff] line-length = 100 target-version = "py312" -# `src/aexp/vendor/` is the vendored research-harness tree -- we preserve its -# upstream style for diff clarity, so it is not linted as our own code. -# `reference/` (dev-only diff provenance) is excluded for the same reason. -extend-exclude = ["reference", "src/aexp/vendor"] [tool.ruff.lint] select = ["E", "F", "W", "I", "B", "UP"] @@ -133,7 +127,7 @@ extend-immutable-calls = ["typer.Option", "typer.Argument"] # Tests contain long literal artifact-body strings used as fixtures; breaking # them up hurts readability without any real benefit. "tests/*" = ["E501"] -# Ported near-verbatim from the vendored kb_validate.py; keeping the +# Ported near-verbatim from the upstream kb_validate.py; keeping the # upstream line-break style makes future diffs against upstream intelligible. "src/aexp/kb_validate.py" = ["E501"] # The install command's multi-line heads-up text contains intentionally wide diff --git a/src/aexp/__init__.py b/src/aexp/__init__.py index 6111f2e..cc72939 100644 --- a/src/aexp/__init__.py +++ b/src/aexp/__init__.py @@ -32,7 +32,7 @@ from aexp.install import ( InstallAction, InstallRefused, - compute_vendor_sha, + compute_scaffold_sha, install_scaffold, is_scaffold_installed, ) @@ -144,7 +144,7 @@ def __getattr__(name: str): # install "InstallAction", "InstallRefused", - "compute_vendor_sha", + "compute_scaffold_sha", "install_scaffold", "is_scaffold_installed", # artifacts (H/E/F/T creation + backlink patching + thread lifecycle) diff --git a/src/aexp/artifacts.py b/src/aexp/artifacts.py index 6222e61..63b9de8 100644 --- a/src/aexp/artifacts.py +++ b/src/aexp/artifacts.py @@ -60,10 +60,10 @@ _SLUG_RE = re.compile(r"[^a-z0-9]+") _ID_FROM_FILENAME_RE = re.compile(r"^(H|E|F|L|CR|SR)(\d{3})-") -# Where vendored templates live (fall-back if the consumer repo has no +# Where the bundled templates live (fall-back if the consumer repo has no # ``templates/`` directory yet). Mirrors the layout install.py copies from. -_VENDOR_TEMPLATES = ( - Path(__file__).resolve().parent / "vendor" / "limina" / "templates" +_SCAFFOLD_TEMPLATES = ( + Path(__file__).resolve().parent / "scaffold" / "templates" ) @@ -128,7 +128,7 @@ def _load_template(kind: ArtifactKind, *, repo_root: Path) -> str: """Return the canonical template text for an artifact kind. Always reads from the package-shipped templates at - ``src/aexp/vendor/limina/templates/`` — the same source the + ``src/aexp/scaffold/templates/`` — the same source the validator's ``missing_template_header`` check uses (:mod:`aexp.kb_validate`). Single source of truth means creation and validation can never disagree about "what the template is." @@ -151,7 +151,7 @@ def _load_template(kind: ArtifactKind, *, repo_root: Path) -> str: intentionally unused. """ filename = _TEMPLATE_FILENAMES[kind] - return (_VENDOR_TEMPLATES / filename).read_text(encoding="utf-8") + return (_SCAFFOLD_TEMPLATES / filename).read_text(encoding="utf-8") def _render_template(tpl: str, substitutions: dict[str, str]) -> str: diff --git a/src/aexp/cli.py b/src/aexp/cli.py index 0d61b24..6dfba6c 100644 --- a/src/aexp/cli.py +++ b/src/aexp/cli.py @@ -154,7 +154,7 @@ def version() -> None: - [cyan].mcp.json[/cyan] JSON-merge: our `aexp` MCP server added, your other servers preserved - [cyan]AGENTS.md[/cyan], [cyan]CLAUDE.md[/cyan] block-merge: your content outside our `<!-- agentic-experiments:begin/end -->` markers is preserved - [cyan].runs/[/cyan] signac project (idempotent; initialised if missing) - - [cyan].aexp/installed.json[/cyan] install marker with interpreter path + vendor sha + - [cyan].aexp/installed.json[/cyan] install marker with interpreter path + scaffold sha By default, conflicting existing files are [yellow]skipped with a warning[/yellow] — pass [bold]--force[/bold] to overwrite. [bold]User-authored scaffold content under `kb/` and `templates/` is preserved even under --force[/bold] (see `preserved_user_modified` in the summary); only tooling files (slash commands, skills, hooks, `.mcp.json`) are refreshed. @@ -252,7 +252,7 @@ def install( "Opt into the Jupyter MCP integration: writes the `jupyter` " "server entry to `.mcp.json`, sets " "`jupyter_enabled: true` (sticky) in the install marker, and " - "ensures `docs/setup/jupyter-mcp.md` is vendored. Requires " + "ensures `docs/setup/jupyter-mcp.md` is copied in. Requires " "`pip install agentic-experiments[jupyter]` for the Python " "deps (jupyter-collaboration, jupyter-mcp-server, " "jupyter-mcp-tools). After install, follow the cluster-side " diff --git a/src/aexp/install.py b/src/aexp/install.py index dadaba6..3293a50 100644 --- a/src/aexp/install.py +++ b/src/aexp/install.py @@ -1,7 +1,7 @@ -"""Install the vendored research harness into a consumer repo. +"""Install the bundled research-harness scaffold into a consumer repo. -``install_scaffold`` walks the vendored snapshot at -``src/aexp/vendor/limina/`` and applies it to a target repo: +``install_scaffold`` walks the bundled scaffold at +``src/aexp/scaffold/`` and applies it to a target repo: - ``kb/``, ``templates/``, ``scripts/`` -> copied verbatim (skipped if the target already has identical content; conflicting target files are skipped @@ -87,9 +87,9 @@ def _find_aexp_source_tree(start: Path) -> Path | None: return None cur = cur.parent -VENDOR_ROOT = Path(__file__).resolve().parent / "vendor" / "limina" +SCAFFOLD_ROOT = Path(__file__).resolve().parent / "scaffold" -# Subdirectories of the vendor tree that get copied verbatim into the consumer repo. +# Subdirectories of the scaffold tree that get copied verbatim into the consumer repo. # # Intentionally does NOT include ``scripts/``. Hook scripts, kb_validate, and # other package code live inside ``aexp.*`` and are invoked via @@ -132,21 +132,21 @@ class InstallAction: # --------------------------------------------------------------------------- -# Vendor-tree fingerprinting +# Scaffold-tree fingerprinting # --------------------------------------------------------------------------- -def compute_vendor_sha(vendor_root: Path = VENDOR_ROOT) -> str: - """Compute a deterministic hash of every file under ``vendor_root``. +def compute_scaffold_sha(scaffold_root: Path = SCAFFOLD_ROOT) -> str: + """Compute a deterministic hash of every file under ``scaffold_root``. Files are sorted by POSIX-style relative path, then hashed as ``<relpath>\\0<bytes>\\0`` into a SHA-256. Two installations with the - same vendor tree produce identical hashes regardless of OS. + same scaffold tree produce identical hashes regardless of OS. """ h = hashlib.sha256() - files = sorted(p for p in vendor_root.rglob("*") if p.is_file()) + files = sorted(p for p in scaffold_root.rglob("*") if p.is_file()) for p in files: - rel = p.relative_to(vendor_root).as_posix().encode("utf-8") + rel = p.relative_to(scaffold_root).as_posix().encode("utf-8") h.update(rel) h.update(b"\x00") h.update(p.read_bytes()) @@ -172,7 +172,7 @@ def _is_text_file(path: Path) -> bool: """True if ``path`` should be treated as text for EOL normalization. We key on file extension rather than content sniffing because the source - side of every install copy is a known set of vendored package files — + side of every install copy is a known set of bundled package files — we already know which are text. """ return path.suffix.lower() in _TEXT_SUFFIXES or path.name in {".gitignore", ".gitattributes"} @@ -284,15 +284,15 @@ def _display_relpath(path: Path) -> str: def merge_claude_settings( - vendor_settings: dict[str, Any], + shipped_settings: dict[str, Any], existing_settings: dict[str, Any], ) -> dict[str, Any]: """Merge our hook block into an existing Claude Code settings dict. - If a top-level key (e.g. ``"hooks"``, ``"permissions"``) is absent in the - existing settings, it is copied from the vendor settings. + existing settings, it is copied from the shipped settings. - Within ``hooks`` (the only block we care about today), matchers from the - vendor are appended to matchers from the user; identical (matcher, + shipped block are appended to matchers from the user; identical (matcher, command) pairs are deduplicated. - Non-``hooks`` user keys are preserved untouched. """ @@ -300,10 +300,10 @@ def merge_claude_settings( # relies on the original ``existing_settings`` remaining intact for the # post-merge "did anything change?" equality check. merged: dict[str, Any] = copy.deepcopy(existing_settings) - vendor_hooks = copy.deepcopy(vendor_settings.get("hooks", {})) + shipped_hooks = copy.deepcopy(shipped_settings.get("hooks", {})) existing_hooks = merged.get("hooks", {}) - for event, vendor_matchers in vendor_hooks.items(): + for event, shipped_matchers in shipped_hooks.items(): existing_matchers = existing_hooks.get(event, []) # Build a set of (matcher, command) pairs already present to dedupe. seen: set[tuple[str, str]] = set() @@ -313,7 +313,7 @@ def merge_claude_settings( seen.add((matcher, h.get("command", ""))) combined = list(existing_matchers) - for vgroup in vendor_matchers: + for vgroup in shipped_matchers: matcher = vgroup.get("matcher", "") new_hooks = [ h @@ -331,9 +331,9 @@ def merge_claude_settings( if existing_hooks: merged["hooks"] = existing_hooks - # Any other top-level keys from the vendor that the user does not have get copied. + # Any other top-level keys we ship that the user does not have get copied. # Note: we never write mcpServers to this file — that belongs in .mcp.json. - for k, v in vendor_settings.items(): + for k, v in shipped_settings.items(): if k == "hooks": continue if k not in merged: @@ -428,12 +428,12 @@ def _merge_or_write_claude_settings( only appends our hook matchers (deduplicating on ``(matcher, command)``). """ rel = _display_relpath(dst) - vendor = _build_claude_settings(python_exe, jupyter_enabled=jupyter_enabled) + shipped = _build_claude_settings(python_exe, jupyter_enabled=jupyter_enabled) if not dst.exists(): if not dry_run: dst.parent.mkdir(parents=True, exist_ok=True) - atomic_write(dst, json.dumps(vendor, indent=2) + "\n") + atomic_write(dst, json.dumps(shipped, indent=2) + "\n") return InstallAction("copied", rel) try: @@ -445,7 +445,7 @@ def _merge_or_write_claude_settings( f"existing {dst.name} is not valid JSON ({exc}); leaving untouched", ) - merged = merge_claude_settings(vendor, existing) + merged = merge_claude_settings(shipped, existing) if merged == existing: return InstallAction("skipped_identical", rel) @@ -631,15 +631,15 @@ def _jupyter_mcp_entries() -> dict[str, Any]: # --------------------------------------------------------------------------- -def block_merge_markdown(existing: str, vendor: str) -> str: - """Append (or refresh) a vendor-managed block inside ``existing``. +def block_merge_markdown(existing: str, shipped: str) -> str: + """Append (or refresh) a package-managed block inside ``existing``. If the begin/end markers are already present, replace whatever is between - them. Otherwise, append the vendor content wrapped in markers. + them. Otherwise, append the shipped content wrapped in markers. """ begin = _BEGIN_MARKER end = _END_MARKER - block = f"{begin}\n{vendor.rstrip()}\n{end}\n" + block = f"{begin}\n{shipped.rstrip()}\n{end}\n" if begin in existing and end in existing: before, rest = existing.split(begin, 1) @@ -653,15 +653,15 @@ def block_merge_markdown(existing: str, vendor: str) -> str: def _merge_or_copy_markdown(src: Path, dst: Path, *, dry_run: bool = False) -> InstallAction: """Copy or block-merge a Markdown file based on whether target exists.""" rel = _display_relpath(dst) - vendor_text = src.read_text(encoding="utf-8") + shipped_text = src.read_text(encoding="utf-8") if not dst.exists(): if not dry_run: - atomic_write(dst, vendor_text) + atomic_write(dst, shipped_text) return InstallAction("copied", rel) existing_text = dst.read_text(encoding="utf-8") - merged = block_merge_markdown(existing_text, vendor_text) + merged = block_merge_markdown(existing_text, shipped_text) if merged == existing_text: return InstallAction("skipped_identical", rel) @@ -676,17 +676,17 @@ def _merge_or_copy_markdown(src: Path, dst: Path, *, dry_run: bool = False) -> I def _install_skills(root: Path, *, force: bool, dry_run: bool = False) -> list[InstallAction]: - """Copy the vendored research-methodology skills into ``<root>/.claude/skills/``. + """Copy the bundled research-methodology skills into ``<root>/.claude/skills/``. - Each ``vendor/limina/skills/<name>/`` directory is copied to + Each ``scaffold/skills/<name>/`` directory is copied to ``<root>/.claude/skills/<name>/`` and emits one ``installed_skill`` action. File-level conflicts (existing skill files differing from - vendor) are handled per the same rules as ``_copy_tree``. + the shipped copy) are handled per the same rules as ``_copy_tree``. """ actions: list[InstallAction] = [] dst_skills = root / ".claude" / "skills" - skills_src = VENDOR_ROOT / "skills" + skills_src = SCAFFOLD_ROOT / "skills" if skills_src.is_dir(): for skill_dir in sorted(p for p in skills_src.iterdir() if p.is_dir()): dst = dst_skills / skill_dir.name @@ -697,7 +697,7 @@ def _install_skills(root: Path, *, force: bool, dry_run: bool = False) -> list[I InstallAction( "installed_skill", _display_relpath(dst), - f"copied vendor/limina/skills/{skill_dir.name} -> {_display_relpath(dst)}", + f"copied scaffold/skills/{skill_dir.name} -> {_display_relpath(dst)}", ) ) @@ -776,7 +776,7 @@ def install_scaffold( allow_self_install: bool = False, with_jupyter: bool = False, ) -> list[InstallAction]: - """Install the vendored research harness into ``repo_root``. + """Install the bundled research-harness scaffold into ``repo_root``. Parameters ---------- @@ -816,7 +816,7 @@ def install_scaffold( dev repo on purpose). with_jupyter : bool, optional If ``True``, also write the ``jupyter`` MCP server entry into - ``.mcp.json``, vendor ``docs/setup/jupyter-mcp.md`` into the + ``.mcp.json``, copy ``docs/setup/jupyter-mcp.md`` into the consumer repo, and set ``jupyter_enabled: true`` in the install marker. The marker bit is sticky — once set, subsequent installs preserve it even if ``with_jupyter=False``. The ``.mcp.json`` @@ -882,8 +882,8 @@ def install_scaffold( actions: list[InstallAction] = [] - # Short-circuit if already installed at the same vendor sha. - vendor_sha = compute_vendor_sha() + # Short-circuit if already installed at the same scaffold sha. + scaffold_sha = compute_scaffold_sha() existing_marker = read_installed_marker(root) # The `jupyter_enabled` marker bit is sticky: a user who once opted in # should keep getting the Jupyter PostToolUse hook on subsequent installs @@ -896,15 +896,15 @@ def install_scaffold( # Dual-read: markers written before the de-brand carry the legacy # `limina_vendor_sha` key. Fall back to it so an old marker still # short-circuits cleanly instead of forcing a spurious re-install. - marker_sha = existing_marker.get("vendor_sha") or existing_marker.get( + marker_sha = existing_marker.get("scaffold_sha") or existing_marker.get( "limina_vendor_sha" ) - if marker_sha == vendor_sha: + if marker_sha == scaffold_sha: actions.append( InstallAction( "already_installed", str(INSTALLED_MARKER_REL.as_posix()), - f"vendor sha {vendor_sha[:12]} already applied at " + f"scaffold sha {scaffold_sha[:12]} already applied at " f"{existing_marker.get('installed_at', 'unknown')}", ) ) @@ -920,7 +920,7 @@ def install_scaffold( for name in _TREES_VERBATIM: actions.extend( _copy_tree( - VENDOR_ROOT / name, + SCAFFOLD_ROOT / name, root / name, force=force, dry_run=dry_run, @@ -930,12 +930,12 @@ def install_scaffold( # 2. Copy / block-merge top-level Markdown docs. for name in _MERGE_FILES: - src = VENDOR_ROOT / name + src = SCAFFOLD_ROOT / name if not src.is_file(): continue actions.append(_merge_or_copy_markdown(src, root / name, dry_run=dry_run)) - # 2a. Vendor the Jupyter MCP setup doc to docs/setup/jupyter-mcp.md. + # 2a. Copy the Jupyter MCP setup doc to docs/setup/jupyter-mcp.md. # Unlike kb/ + templates/ (editable scaffold), this is a canonical # reference doc that ships fixes via `pip install -U`. We use the # standard tooling-file rules (overwrite under --force, skip @@ -946,7 +946,7 @@ def install_scaffold( # Copied unconditionally (not gated on --with-jupyter): the doc is # small, harmless, and lets a consumer read about the integration # before deciding to opt in. - jupyter_doc_src = VENDOR_ROOT / "docs" / "setup" / "jupyter-mcp.md" + jupyter_doc_src = SCAFFOLD_ROOT / "docs" / "setup" / "jupyter-mcp.md" if jupyter_doc_src.is_file(): actions.append( _copy_file( @@ -988,7 +988,7 @@ def install_scaffold( ) ) - # 3b. Install the vendored Claude Code skills into <repo>/.claude/skills/. + # 3b. Install the bundled Claude Code skills into <repo>/.claude/skills/. # AGENTS.md references skills like $experiment-rigor; without this step # those references are broken for every consumer repo. actions.extend(_install_skills(root, force=force, dry_run=dry_run)) @@ -1016,7 +1016,7 @@ def install_scaffold( root, version=__version__, run_store_path=run_store, - vendor_sha=vendor_sha, + scaffold_sha=scaffold_sha, jupyter_enabled=with_jupyter, ) actions.append(InstallAction("wrote_marker", _display_relpath(marker_path))) diff --git a/src/aexp/kb_validate.py b/src/aexp/kb_validate.py index 1f96750..5abcf54 100644 --- a/src/aexp/kb_validate.py +++ b/src/aexp/kb_validate.py @@ -1,6 +1,6 @@ """KB structural validator — frontmatter, aliases, wikilinks, H->E->F chain. -Ported from the vendored ``kb_validate.py`` into the package. Callable +Ported from the upstream ``kb_validate.py`` into the package. Callable in-process via :func:`validate_kb`; :func:`main` preserves a ``python -m aexp.kb_validate`` CLI for parity with the old script invocation. @@ -496,14 +496,14 @@ def validate_conditions_block(note: NoteRecord, result: ValidationResult) -> Non "F": "finding.md", "T": "thread.md", } -_VENDOR_TEMPLATES_DIR = ( - Path(__file__).resolve().parent / "vendor" / "limina" / "templates" +_SCAFFOLD_TEMPLATES_DIR = ( + Path(__file__).resolve().parent / "scaffold" / "templates" ) @functools.cache def _required_headers_for_kind(kind: str) -> tuple[str, ...]: - """Extract ordered ``## ``-level headers from the vendored template for ``kind``. + """Extract ordered ``## ``-level headers from the bundled template for ``kind``. Returns the headers an artifact of this kind is expected to contain. Excludes ``## Links`` (already comprehensively validated by @@ -514,7 +514,7 @@ def _required_headers_for_kind(kind: str) -> tuple[str, ...]: filename = _TEMPLATE_FILENAMES_FOR_HEADER_CHECK.get(kind) if filename is None: return () - template_path = _VENDOR_TEMPLATES_DIR / filename + template_path = _SCAFFOLD_TEMPLATES_DIR / filename if not template_path.is_file(): return () headers: list[str] = [] diff --git a/src/aexp/sandbox.py b/src/aexp/sandbox.py index 9e2ea80..931fa86 100644 --- a/src/aexp/sandbox.py +++ b/src/aexp/sandbox.py @@ -130,7 +130,7 @@ def setup_sandbox_notebook(name: str, *, start: Path | str | None = None) -> dic # Templates are inline strings rather than separate files. v0 trade-off: # inline = easier to ship + reason about; cost = users can't override # without monkey-patching. If override needs surface, extract to -# vendor/limina/templates/sandbox/ and add a precedence rule similar to +# scaffold/templates/sandbox/ and add a precedence rule similar to # artifacts._load_template. _SANDBOX_ROOT_README = """# notebooks/_sandbox/ diff --git a/src/aexp/vendor/limina/AGENTS.md b/src/aexp/scaffold/AGENTS.md similarity index 100% rename from src/aexp/vendor/limina/AGENTS.md rename to src/aexp/scaffold/AGENTS.md diff --git a/src/aexp/vendor/limina/CLAUDE.md b/src/aexp/scaffold/CLAUDE.md similarity index 100% rename from src/aexp/vendor/limina/CLAUDE.md rename to src/aexp/scaffold/CLAUDE.md diff --git a/src/aexp/vendor/limina/docs/setup/jupyter-mcp.md b/src/aexp/scaffold/docs/setup/jupyter-mcp.md similarity index 99% rename from src/aexp/vendor/limina/docs/setup/jupyter-mcp.md rename to src/aexp/scaffold/docs/setup/jupyter-mcp.md index 5fb1683..907ebf6 100644 --- a/src/aexp/vendor/limina/docs/setup/jupyter-mcp.md +++ b/src/aexp/scaffold/docs/setup/jupyter-mcp.md @@ -9,7 +9,7 @@ Reference: [Datalayer jupyter-mcp-server](https://github.com/datalayer/jupyter-m Their docs assume a clean pip install on a single machine; this guide is the cluster-specific extension. -> **Local overlay note.** This file is the canonical, vendor-managed +> **Local overlay note.** This file is the canonical, package-managed > version that ships with `agentic-experiments`. It will be overwritten > on `aexp install --force` to pick up upstream fixes. If you need > project-specific overlay info (your cluster hostname, your conda env diff --git a/src/aexp/vendor/limina/kb/ACTIVE.md b/src/aexp/scaffold/kb/ACTIVE.md similarity index 100% rename from src/aexp/vendor/limina/kb/ACTIVE.md rename to src/aexp/scaffold/kb/ACTIVE.md diff --git a/src/aexp/vendor/limina/kb/DASHBOARD.md b/src/aexp/scaffold/kb/DASHBOARD.md similarity index 100% rename from src/aexp/vendor/limina/kb/DASHBOARD.md rename to src/aexp/scaffold/kb/DASHBOARD.md diff --git a/src/aexp/vendor/limina/kb/lessons/README.md b/src/aexp/scaffold/kb/lessons/README.md similarity index 100% rename from src/aexp/vendor/limina/kb/lessons/README.md rename to src/aexp/scaffold/kb/lessons/README.md diff --git a/src/aexp/vendor/limina/kb/mission/CHALLENGE.md b/src/aexp/scaffold/kb/mission/CHALLENGE.md similarity index 100% rename from src/aexp/vendor/limina/kb/mission/CHALLENGE.md rename to src/aexp/scaffold/kb/mission/CHALLENGE.md diff --git a/src/aexp/vendor/limina/kb/reports/.gitkeep b/src/aexp/scaffold/kb/reports/.gitkeep similarity index 100% rename from src/aexp/vendor/limina/kb/reports/.gitkeep rename to src/aexp/scaffold/kb/reports/.gitkeep diff --git a/src/aexp/vendor/limina/kb/research/data/.gitkeep b/src/aexp/scaffold/kb/research/data/.gitkeep similarity index 100% rename from src/aexp/vendor/limina/kb/research/data/.gitkeep rename to src/aexp/scaffold/kb/research/data/.gitkeep diff --git a/src/aexp/vendor/limina/kb/research/experiments/.gitkeep b/src/aexp/scaffold/kb/research/experiments/.gitkeep similarity index 100% rename from src/aexp/vendor/limina/kb/research/experiments/.gitkeep rename to src/aexp/scaffold/kb/research/experiments/.gitkeep diff --git a/src/aexp/vendor/limina/kb/research/findings/.gitkeep b/src/aexp/scaffold/kb/research/findings/.gitkeep similarity index 100% rename from src/aexp/vendor/limina/kb/research/findings/.gitkeep rename to src/aexp/scaffold/kb/research/findings/.gitkeep diff --git a/src/aexp/vendor/limina/kb/research/hypotheses/.gitkeep b/src/aexp/scaffold/kb/research/hypotheses/.gitkeep similarity index 100% rename from src/aexp/vendor/limina/kb/research/hypotheses/.gitkeep rename to src/aexp/scaffold/kb/research/hypotheses/.gitkeep diff --git a/src/aexp/vendor/limina/kb/research/literature/.gitkeep b/src/aexp/scaffold/kb/research/literature/.gitkeep similarity index 100% rename from src/aexp/vendor/limina/kb/research/literature/.gitkeep rename to src/aexp/scaffold/kb/research/literature/.gitkeep diff --git a/src/aexp/vendor/limina/kb/research/threads/.gitkeep b/src/aexp/scaffold/kb/research/threads/.gitkeep similarity index 100% rename from src/aexp/vendor/limina/kb/research/threads/.gitkeep rename to src/aexp/scaffold/kb/research/threads/.gitkeep diff --git a/src/aexp/vendor/limina/skills/build-maintainable-software/SKILL.md b/src/aexp/scaffold/skills/build-maintainable-software/SKILL.md similarity index 100% rename from src/aexp/vendor/limina/skills/build-maintainable-software/SKILL.md rename to src/aexp/scaffold/skills/build-maintainable-software/SKILL.md diff --git a/src/aexp/vendor/limina/skills/build-maintainable-software/agents/openai.yaml b/src/aexp/scaffold/skills/build-maintainable-software/agents/openai.yaml similarity index 100% rename from src/aexp/vendor/limina/skills/build-maintainable-software/agents/openai.yaml rename to src/aexp/scaffold/skills/build-maintainable-software/agents/openai.yaml diff --git a/src/aexp/vendor/limina/skills/build-maintainable-software/evals/trigger-prompts.csv b/src/aexp/scaffold/skills/build-maintainable-software/evals/trigger-prompts.csv similarity index 100% rename from src/aexp/vendor/limina/skills/build-maintainable-software/evals/trigger-prompts.csv rename to src/aexp/scaffold/skills/build-maintainable-software/evals/trigger-prompts.csv diff --git a/src/aexp/vendor/limina/skills/build-maintainable-software/references/design-principles.md b/src/aexp/scaffold/skills/build-maintainable-software/references/design-principles.md similarity index 100% rename from src/aexp/vendor/limina/skills/build-maintainable-software/references/design-principles.md rename to src/aexp/scaffold/skills/build-maintainable-software/references/design-principles.md diff --git a/src/aexp/vendor/limina/skills/build-maintainable-software/references/review-checklist.md b/src/aexp/scaffold/skills/build-maintainable-software/references/review-checklist.md similarity index 100% rename from src/aexp/vendor/limina/skills/build-maintainable-software/references/review-checklist.md rename to src/aexp/scaffold/skills/build-maintainable-software/references/review-checklist.md diff --git a/src/aexp/vendor/limina/skills/experiment-rigor/SKILL.md b/src/aexp/scaffold/skills/experiment-rigor/SKILL.md similarity index 100% rename from src/aexp/vendor/limina/skills/experiment-rigor/SKILL.md rename to src/aexp/scaffold/skills/experiment-rigor/SKILL.md diff --git a/src/aexp/vendor/limina/skills/experiment-rigor/agents/openai.yaml b/src/aexp/scaffold/skills/experiment-rigor/agents/openai.yaml similarity index 100% rename from src/aexp/vendor/limina/skills/experiment-rigor/agents/openai.yaml rename to src/aexp/scaffold/skills/experiment-rigor/agents/openai.yaml diff --git a/src/aexp/vendor/limina/skills/experiment-rigor/evals/trigger-prompts.csv b/src/aexp/scaffold/skills/experiment-rigor/evals/trigger-prompts.csv similarity index 100% rename from src/aexp/vendor/limina/skills/experiment-rigor/evals/trigger-prompts.csv rename to src/aexp/scaffold/skills/experiment-rigor/evals/trigger-prompts.csv diff --git a/src/aexp/vendor/limina/skills/experiment-rigor/references/experiment-rubric.md b/src/aexp/scaffold/skills/experiment-rigor/references/experiment-rubric.md similarity index 100% rename from src/aexp/vendor/limina/skills/experiment-rigor/references/experiment-rubric.md rename to src/aexp/scaffold/skills/experiment-rigor/references/experiment-rubric.md diff --git a/src/aexp/vendor/limina/skills/experiment-rigor/references/hypothesis-rubric.md b/src/aexp/scaffold/skills/experiment-rigor/references/hypothesis-rubric.md similarity index 100% rename from src/aexp/vendor/limina/skills/experiment-rigor/references/hypothesis-rubric.md rename to src/aexp/scaffold/skills/experiment-rigor/references/hypothesis-rubric.md diff --git a/src/aexp/vendor/limina/skills/experiment-rigor/references/metrics-storage.md b/src/aexp/scaffold/skills/experiment-rigor/references/metrics-storage.md similarity index 100% rename from src/aexp/vendor/limina/skills/experiment-rigor/references/metrics-storage.md rename to src/aexp/scaffold/skills/experiment-rigor/references/metrics-storage.md diff --git a/src/aexp/vendor/limina/skills/exploratory-sota-research/SKILL.md b/src/aexp/scaffold/skills/exploratory-sota-research/SKILL.md similarity index 100% rename from src/aexp/vendor/limina/skills/exploratory-sota-research/SKILL.md rename to src/aexp/scaffold/skills/exploratory-sota-research/SKILL.md diff --git a/src/aexp/vendor/limina/skills/exploratory-sota-research/agents/openai.yaml b/src/aexp/scaffold/skills/exploratory-sota-research/agents/openai.yaml similarity index 100% rename from src/aexp/vendor/limina/skills/exploratory-sota-research/agents/openai.yaml rename to src/aexp/scaffold/skills/exploratory-sota-research/agents/openai.yaml diff --git a/src/aexp/vendor/limina/skills/exploratory-sota-research/evals/trigger-prompts.csv b/src/aexp/scaffold/skills/exploratory-sota-research/evals/trigger-prompts.csv similarity index 100% rename from src/aexp/vendor/limina/skills/exploratory-sota-research/evals/trigger-prompts.csv rename to src/aexp/scaffold/skills/exploratory-sota-research/evals/trigger-prompts.csv diff --git a/src/aexp/vendor/limina/skills/exploratory-sota-research/references/output-template.md b/src/aexp/scaffold/skills/exploratory-sota-research/references/output-template.md similarity index 100% rename from src/aexp/vendor/limina/skills/exploratory-sota-research/references/output-template.md rename to src/aexp/scaffold/skills/exploratory-sota-research/references/output-template.md diff --git a/src/aexp/vendor/limina/skills/exploratory-sota-research/references/source-selection.md b/src/aexp/scaffold/skills/exploratory-sota-research/references/source-selection.md similarity index 100% rename from src/aexp/vendor/limina/skills/exploratory-sota-research/references/source-selection.md rename to src/aexp/scaffold/skills/exploratory-sota-research/references/source-selection.md diff --git a/src/aexp/vendor/limina/skills/exploratory-sota-research/references/worked-example-information-retrieval.md b/src/aexp/scaffold/skills/exploratory-sota-research/references/worked-example-information-retrieval.md similarity index 100% rename from src/aexp/vendor/limina/skills/exploratory-sota-research/references/worked-example-information-retrieval.md rename to src/aexp/scaffold/skills/exploratory-sota-research/references/worked-example-information-retrieval.md diff --git a/src/aexp/vendor/limina/skills/research-devil-advocate/SKILL.md b/src/aexp/scaffold/skills/research-devil-advocate/SKILL.md similarity index 100% rename from src/aexp/vendor/limina/skills/research-devil-advocate/SKILL.md rename to src/aexp/scaffold/skills/research-devil-advocate/SKILL.md diff --git a/src/aexp/vendor/limina/skills/research-devil-advocate/agents/openai.yaml b/src/aexp/scaffold/skills/research-devil-advocate/agents/openai.yaml similarity index 100% rename from src/aexp/vendor/limina/skills/research-devil-advocate/agents/openai.yaml rename to src/aexp/scaffold/skills/research-devil-advocate/agents/openai.yaml diff --git a/src/aexp/vendor/limina/skills/research-devil-advocate/evals/trigger-prompts.csv b/src/aexp/scaffold/skills/research-devil-advocate/evals/trigger-prompts.csv similarity index 100% rename from src/aexp/vendor/limina/skills/research-devil-advocate/evals/trigger-prompts.csv rename to src/aexp/scaffold/skills/research-devil-advocate/evals/trigger-prompts.csv diff --git a/src/aexp/vendor/limina/skills/research-devil-advocate/references/review-rubric.md b/src/aexp/scaffold/skills/research-devil-advocate/references/review-rubric.md similarity index 100% rename from src/aexp/vendor/limina/skills/research-devil-advocate/references/review-rubric.md rename to src/aexp/scaffold/skills/research-devil-advocate/references/review-rubric.md diff --git a/src/aexp/vendor/limina/templates/active.md b/src/aexp/scaffold/templates/active.md similarity index 100% rename from src/aexp/vendor/limina/templates/active.md rename to src/aexp/scaffold/templates/active.md diff --git a/src/aexp/vendor/limina/templates/challenge-review.md b/src/aexp/scaffold/templates/challenge-review.md similarity index 100% rename from src/aexp/vendor/limina/templates/challenge-review.md rename to src/aexp/scaffold/templates/challenge-review.md diff --git a/src/aexp/vendor/limina/templates/experiment.md b/src/aexp/scaffold/templates/experiment.md similarity index 100% rename from src/aexp/vendor/limina/templates/experiment.md rename to src/aexp/scaffold/templates/experiment.md diff --git a/src/aexp/vendor/limina/templates/finding.md b/src/aexp/scaffold/templates/finding.md similarity index 100% rename from src/aexp/vendor/limina/templates/finding.md rename to src/aexp/scaffold/templates/finding.md diff --git a/src/aexp/vendor/limina/templates/hypothesis.md b/src/aexp/scaffold/templates/hypothesis.md similarity index 100% rename from src/aexp/vendor/limina/templates/hypothesis.md rename to src/aexp/scaffold/templates/hypothesis.md diff --git a/src/aexp/vendor/limina/templates/literature.md b/src/aexp/scaffold/templates/literature.md similarity index 100% rename from src/aexp/vendor/limina/templates/literature.md rename to src/aexp/scaffold/templates/literature.md diff --git a/src/aexp/vendor/limina/templates/report.md b/src/aexp/scaffold/templates/report.md similarity index 100% rename from src/aexp/vendor/limina/templates/report.md rename to src/aexp/scaffold/templates/report.md diff --git a/src/aexp/vendor/limina/templates/strategic-review.md b/src/aexp/scaffold/templates/strategic-review.md similarity index 100% rename from src/aexp/vendor/limina/templates/strategic-review.md rename to src/aexp/scaffold/templates/strategic-review.md diff --git a/src/aexp/vendor/limina/templates/thread.md b/src/aexp/scaffold/templates/thread.md similarity index 100% rename from src/aexp/vendor/limina/templates/thread.md rename to src/aexp/scaffold/templates/thread.md diff --git a/src/aexp/schema.py b/src/aexp/schema.py index 9173ac6..164cc94 100644 --- a/src/aexp/schema.py +++ b/src/aexp/schema.py @@ -27,7 +27,7 @@ # --------------------------------------------------------------------------- ArtifactKind = Literal["H", "E", "F", "L", "CR", "SR", "T"] -"""The seven research artifact kinds validated by vendored ``kb_validate.py``. +"""The seven research artifact kinds validated by the bundled ``kb_validate.py``. ``H``=Hypothesis, ``E``=Experiment, ``F``=Finding, ``L``=Literature, ``CR``=Challenge Review, ``SR``=Strategic Review, diff --git a/src/aexp/utils/atomic.py b/src/aexp/utils/atomic.py index 3e73a54..b33f2fa 100644 --- a/src/aexp/utils/atomic.py +++ b/src/aexp/utils/atomic.py @@ -73,8 +73,8 @@ def atomic_write( Text encoding (ignored for bytes). Default ``"utf-8"``. newline : str or None, optional Newline policy (ignored for bytes). Default ``"\\n"`` to force LF - on Windows as well — matters because vendored hook scripts and - kb_validate expect Unix line endings. + on Windows as well — matters because the bundled scaffold files + and kb_validate expect Unix line endings. Returns ------- diff --git a/src/aexp/utils/paths.py b/src/aexp/utils/paths.py index 3c1f3df..1c46dde 100644 --- a/src/aexp/utils/paths.py +++ b/src/aexp/utils/paths.py @@ -25,7 +25,7 @@ class InstalledMarker(TypedDict, total=False): version: str installed_at: str run_store_path: str - vendor_sha: str + scaffold_sha: str python_exe: str # absolute path to the Python that ran install_scaffold conda_env_name: str # CONDA_DEFAULT_ENV at install time, or "" for venv/system Python jupyter_enabled: bool # True if any prior install used --with-jupyter; sticky once set @@ -92,7 +92,7 @@ def write_installed_marker( *, version: str, run_store_path: str, - vendor_sha: str, + scaffold_sha: str, installed_at: str | None = None, python_exe: str | None = None, conda_env_name: str | None = None, @@ -108,8 +108,8 @@ def write_installed_marker( agentic-experiments package version. run_store_path : str Path (relative to ``repo_root``) of the signac project. - vendor_sha : str - Fingerprint of the vendored research-harness snapshot used at install time. + scaffold_sha : str + Fingerprint of the bundled research-harness scaffold used at install time. installed_at : str or None ISO-8601 UTC timestamp. Defaults to ``now`` in UTC. python_exe : str or None @@ -147,7 +147,7 @@ def write_installed_marker( "version": version, "installed_at": installed_at, "run_store_path": run_store_path, - "vendor_sha": vendor_sha, + "scaffold_sha": scaffold_sha, "python_exe": python_exe, "conda_env_name": conda_env_name, } diff --git a/src/aexp/validate.py b/src/aexp/validate.py index 4771e9c..2d7308a 100644 --- a/src/aexp/validate.py +++ b/src/aexp/validate.py @@ -91,7 +91,7 @@ def _run_kb_validate(repo_root: Path) -> list[Issue]: case — callers decide whether that's worth flagging separately). No subprocess, no env manipulation, no telemetry concerns — all three - were required back when this shelled out to the vendored + were required back when this shelled out to the bundled ``scripts/kb_validate.py``. That script is now importable as ``aexp.kb_validate``. """ diff --git a/src/aexp/vendor/limina/VENDORED_FROM.txt b/src/aexp/vendor/limina/VENDORED_FROM.txt deleted file mode 100644 index 79aed24..0000000 --- a/src/aexp/vendor/limina/VENDORED_FROM.txt +++ /dev/null @@ -1,16 +0,0 @@ -Vendored snapshot of "limina" (https://github.com/KadenMc/limina), v0.1.0, -forked into agentic-experiments on 2026-04-20. One-time fork; not resynced. - -The H->E->F artifact model, the kb/ layout, the templates/, and the -methodology skills under this directory originate from that project and -have since been adapted as agentic-experiments' own research scaffold. - -Adaptations applied during vendoring: -- The 4 shell hooks were replaced with cross-platform Python ports; they - now live in the aexp package (aexp.hooks.*) and are wired up by - `aexp install`. -- The upstream .claude/settings.json was folded into the settings merge - that `aexp install` performs. -- The upstream setup.sh and requirements.txt were dropped: project setup - is `aexp install`, and dependencies live in agentic-experiments' - pyproject.toml. diff --git a/src/aexp/vendor/limina/VERSION b/src/aexp/vendor/limina/VERSION deleted file mode 100644 index 6e8bf73..0000000 --- a/src/aexp/vendor/limina/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.1.0 diff --git a/tests/conftest.py b/tests/conftest.py index afd8d0a..76b6bf6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,26 +8,26 @@ import pytest PACKAGE_ROOT = Path(__file__).resolve().parents[1] -VENDOR_ROOT = PACKAGE_ROOT / "src" / "aexp" / "vendor" / "limina" +SCAFFOLD_ROOT = PACKAGE_ROOT / "src" / "aexp" / "scaffold" @pytest.fixture -def vendored_tree() -> Path: - """Absolute path to the vendored research-harness snapshot in this repo.""" - assert VENDOR_ROOT.is_dir(), f"vendored research harness missing at {VENDOR_ROOT}" - return VENDOR_ROOT +def scaffold_tree() -> Path: + """Absolute path to the bundled research-harness scaffold in this repo.""" + assert SCAFFOLD_ROOT.is_dir(), f"research-harness scaffold missing at {SCAFFOLD_ROOT}" + return SCAFFOLD_ROOT @pytest.fixture def scaffold_project(tmp_path: Path) -> Path: - """Copy the vendored research-harness snapshot into a tmp dir. + """Copy the bundled research-harness scaffold into a tmp dir. Gives each test an isolated ``PROJECT_ROOT`` — the ported hooks derive their root from ``Path(__file__).resolve().parents[2]``, so running a copied hook sets ``PROJECT_ROOT`` to this tmp dir. """ dest = tmp_path / "scaffold_project" - shutil.copytree(VENDOR_ROOT, dest) + shutil.copytree(SCAFFOLD_ROOT, dest) return dest diff --git a/tests/test_artifacts.py b/tests/test_artifacts.py index d7f1709..3f173b4 100644 --- a/tests/test_artifacts.py +++ b/tests/test_artifacts.py @@ -239,7 +239,7 @@ def test_full_chain_validates_clean(installed_repo: Path) -> None: # --------------------------------------------------------------------------- -# Single source of truth: artifact creation reads vendored templates only +# Single source of truth: artifact creation reads the bundled templates only # --------------------------------------------------------------------------- @@ -250,7 +250,7 @@ def test_new_hypothesis_ignores_local_template_override( When a consumer's local ``templates/<kind>.md`` is stale (or customised), the artifact-creation API must still read from the - vendored template — same source the validator uses. Otherwise + bundled template — same source the validator uses. Otherwise creation produces a skeleton that immediately fails validation. The 2026-04-24 electricrag report described exactly this failure: @@ -259,7 +259,7 @@ def test_new_hypothesis_ignores_local_template_override( expected the new one. """ # Stuff the local hypothesis template with bogus content that has - # none of the headers the vendored template ships. + # none of the headers the bundled template ships. local = installed_repo / "templates" / "hypothesis.md" local.write_text( '---\nid: "{ARTIFACT_ID}"\naliases: ["{ARTIFACT_ID}"]\n' @@ -274,7 +274,7 @@ def test_new_hypothesis_ignores_local_template_override( rendered = ( installed_repo / "kb" / "research" / "hypotheses" / "H001-t.md" ).read_text(encoding="utf-8") - # Vendored template's headers must be present. + # The bundled template's headers must be present. assert "## Statement" in rendered assert "## Test Plan" in rendered assert "## Conclusion" in rendered diff --git a/tests/test_e2e_smoke.py b/tests/test_e2e_smoke.py index cafae46..1d99872 100644 --- a/tests/test_e2e_smoke.py +++ b/tests/test_e2e_smoke.py @@ -3,7 +3,7 @@ Covers the plan §12 checklist: 1. ``aex install`` in a bare git repo produces the expected tree. -2. Create an artifact via the vendored ``kb_new_artifact.py`` — KB remains valid. +2. Create an artifact via the bundled ``kb_new_artifact.py`` — KB remains valid. 3. ``aex new-run`` creates a signac job; ``aex list-runs`` finds it. 4. ``aex bind-tracker --backend noop`` writes JSONL under the job workspace. 5. Create several runs; ``aex list-batches`` rolls them up; ``aex show-batch`` filters. diff --git a/tests/test_hooks_python.py b/tests/test_hooks_python.py index 1d293cf..0ff0901 100644 --- a/tests/test_hooks_python.py +++ b/tests/test_hooks_python.py @@ -1,4 +1,4 @@ -"""Tests for the aexp hooks (ported from the vendored harness scripts). +"""Tests for the aexp hooks (ported from the upstream harness scripts). Covers: @@ -281,7 +281,7 @@ def test_kb_write_guard_blocks_invalid_md( def test_stop_validate_passes_on_clean_kb( scaffold_project: Path, python_exe: str ) -> None: - """The vendored shipped kb/ template validates cleanly out of the box.""" + """The bundled kb/ template validates cleanly out of the box.""" r = _run_hook(scaffold_project, "stop_validate", None, python_exe, timeout=30) assert r.returncode == 0, (r.returncode, r.stdout, r.stderr) diff --git a/tests/test_install.py b/tests/test_install.py index 8fa9edd..03d3cdb 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -11,7 +11,7 @@ InstallAction, InstallRefused, block_merge_markdown, - compute_vendor_sha, + compute_scaffold_sha, install_scaffold, is_scaffold_installed, merge_claude_settings, @@ -45,13 +45,13 @@ def fresh_git_repo(tmp_path: Path) -> Path: # --------------------------------------------------------------------------- -# compute_vendor_sha +# compute_scaffold_sha # --------------------------------------------------------------------------- -def test_vendor_sha_is_deterministic() -> None: - s1 = compute_vendor_sha() - s2 = compute_vendor_sha() +def test_scaffold_sha_is_deterministic() -> None: + s1 = compute_scaffold_sha() + s2 = compute_scaffold_sha() assert s1 == s2 assert len(s1) == 64 # SHA-256 hex digest @@ -62,7 +62,7 @@ def test_vendor_sha_is_deterministic() -> None: def test_merge_claude_settings_into_empty_user() -> None: - vendor = { + shipped = { "hooks": { "SessionStart": [ { @@ -72,7 +72,7 @@ def test_merge_claude_settings_into_empty_user() -> None: ] } } - merged = merge_claude_settings(vendor, {}) + merged = merge_claude_settings(shipped, {}) assert merged["hooks"]["SessionStart"][0]["hooks"][0]["command"] == "python x.py" @@ -82,9 +82,9 @@ def test_merge_claude_settings_dedupes_identical_hook() -> None: "hooks": [{"type": "command", "command": "python x.py", "timeout": 5}], } existing = {"hooks": {"SessionStart": [entry]}} - vendor = {"hooks": {"SessionStart": [entry]}} + shipped = {"hooks": {"SessionStart": [entry]}} - merged = merge_claude_settings(vendor, existing) + merged = merge_claude_settings(shipped, existing) assert len(merged["hooks"]["SessionStart"]) == 1 @@ -93,14 +93,14 @@ def test_merge_claude_settings_preserves_user_hooks_and_appends_ours() -> None: "matcher": "Read", "hooks": [{"type": "command", "command": "user.sh", "timeout": 5}], } - vendor_hook = { + shipped_hook = { "matcher": "Write|Edit", "hooks": [{"type": "command", "command": "python guard.py", "timeout": 5}], } existing = {"hooks": {"PostToolUse": [user_hook]}, "permissions": {"allow": []}} - vendor = {"hooks": {"PostToolUse": [vendor_hook]}} + shipped = {"hooks": {"PostToolUse": [shipped_hook]}} - merged = merge_claude_settings(vendor, existing) + merged = merge_claude_settings(shipped, existing) matchers = [group["matcher"] for group in merged["hooks"]["PostToolUse"]] assert matchers == ["Read", "Write|Edit"] # User's permissions key untouched @@ -109,8 +109,8 @@ def test_merge_claude_settings_preserves_user_hooks_and_appends_ours() -> None: def test_merge_claude_settings_preserves_user_top_level_keys() -> None: existing = {"theme": "dark", "other": {"nested": True}} - vendor = {"hooks": {"Stop": [{"matcher": "", "hooks": []}]}} - merged = merge_claude_settings(vendor, existing) + shipped = {"hooks": {"Stop": [{"matcher": "", "hooks": []}]}} + merged = merge_claude_settings(shipped, existing) assert merged["theme"] == "dark" assert merged["other"] == {"nested": True} @@ -122,22 +122,22 @@ def test_merge_claude_settings_preserves_user_top_level_keys() -> None: def test_block_merge_appends_when_markers_absent() -> None: existing = "# User doc\n\nuser content\n" - merged = block_merge_markdown(existing, "vendor content") + merged = block_merge_markdown(existing, "shipped content") assert "user content" in merged assert "<!-- agentic-experiments:begin -->" in merged - assert "vendor content" in merged + assert "shipped content" in merged assert "<!-- agentic-experiments:end -->" in merged def test_block_merge_replaces_existing_block() -> None: existing = ( "# User doc\n\n" - "<!-- agentic-experiments:begin -->\nold vendor\n<!-- agentic-experiments:end -->\n" + "<!-- agentic-experiments:begin -->\nold block\n<!-- agentic-experiments:end -->\n" "\nmore user content\n" ) - merged = block_merge_markdown(existing, "new vendor") - assert "old vendor" not in merged - assert "new vendor" in merged + merged = block_merge_markdown(existing, "new block") + assert "old block" not in merged + assert "new block" in merged assert "more user content" in merged @@ -222,7 +222,7 @@ def test_install_writes_valid_marker(fresh_git_repo: Path) -> None: assert marker is not None assert marker["version"] assert marker["run_store_path"] == ".runs" - assert len(marker["vendor_sha"]) == 64 + assert len(marker["scaffold_sha"]) == 64 # Cross-platform invocation fields written by default. assert "python_exe" in marker assert Path(marker["python_exe"]).exists() @@ -234,7 +234,7 @@ def test_install_writes_valid_marker(fresh_git_repo: Path) -> None: def test_install_is_idempotent(fresh_git_repo: Path) -> None: install_scaffold(fresh_git_repo) # Second run: every action should be either already_installed (short-circuit) - # or a no-op kind. Because the marker matches the vendor sha, we short-circuit. + # or a no-op kind. Because the marker matches the scaffold sha, we short-circuit. actions = install_scaffold(fresh_git_repo) assert any(a.kind == "already_installed" for a in actions) # Critically: nothing got copied again @@ -383,7 +383,7 @@ def test_install_json_merges_existing_claude_settings(fresh_git_repo: Path) -> N post_tool = merged["hooks"]["PostToolUse"] commands = [h["command"] for group in post_tool for h in group["hooks"]] assert "user-bash.sh" in commands - # Vendor's hook got appended + # The shipped hook got appended assert any("aexp.hooks.kb_write_guard" in c for c in commands) # User's unrelated key survived assert merged["theme"] == "dark" @@ -436,7 +436,7 @@ def test_install_action_kinds_are_expected(fresh_git_repo: Path) -> None: def test_install_copies_skills_to_claude_skills(fresh_git_repo: Path) -> None: - """All vendored research skills must land under <repo>/.claude/skills/. + """All bundled research skills must land under <repo>/.claude/skills/. Without these, the AGENTS.md references like $experiment-rigor are broken on every consumer repo. @@ -444,7 +444,7 @@ def test_install_copies_skills_to_claude_skills(fresh_git_repo: Path) -> None: install_scaffold(fresh_git_repo) skills_root = fresh_git_repo / ".claude" / "skills" assert skills_root.is_dir() - # Research-methodology skills (from vendor/limina/skills/) + # Research-methodology skills (from scaffold/skills/) expected = { "experiment-rigor", "exploratory-sota-research", @@ -810,13 +810,13 @@ def test_install_jupyter_marker_is_sticky_true(fresh_git_repo: Path) -> None: assert marker.get("jupyter_enabled") is True -def test_install_with_jupyter_vendors_setup_doc(fresh_git_repo: Path) -> None: - """docs/setup/jupyter-mcp.md is copied verbatim from vendor when --with-jupyter.""" +def test_install_with_jupyter_copies_setup_doc(fresh_git_repo: Path) -> None: + """docs/setup/jupyter-mcp.md is copied verbatim from the scaffold when --with-jupyter.""" install_scaffold(fresh_git_repo, with_jupyter=True, dev=True) setup_doc = fresh_git_repo / "docs" / "setup" / "jupyter-mcp.md" assert setup_doc.is_file() body = setup_doc.read_text(encoding="utf-8") - # A few load-bearing strings from the vendored doc. + # A few load-bearing strings from the bundled doc. assert "Adapting this guide to your cluster" in body assert "Investigation log" in body @@ -1006,7 +1006,7 @@ def test_repo_root_gitattributes_forces_lf_for_text() -> None: assert ga.is_file(), f"missing .gitattributes at {ga}" body = ga.read_text(encoding="utf-8") # Each of these extensions ships in the package data (slash commands, - # vendored docs, scaffold JSON). If any drift off, the install symptom + # bundled docs, scaffold JSON). If any drift off, the install symptom # comes back. for ext in (".md", ".json", ".py", ".toml"): # Match either `*<ext> text eol=lf` or equivalent. diff --git a/tests/test_utils.py b/tests/test_utils.py index 97ed4a6..856ce1b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -143,13 +143,13 @@ def test_write_then_read_installed_marker(tmp_path: Path) -> None: tmp_path, version="0.1.0", run_store_path=".runs", - vendor_sha="deadbeef", + scaffold_sha="deadbeef", ) marker = read_installed_marker(tmp_path) assert marker is not None assert marker["version"] == "0.1.0" assert marker["run_store_path"] == ".runs" - assert marker["vendor_sha"] == "deadbeef" + assert marker["scaffold_sha"] == "deadbeef" assert "installed_at" in marker # Cross-platform invocation fields captured by default. assert "python_exe" in marker @@ -162,7 +162,7 @@ def test_write_installed_marker_honors_explicit_python_exe(tmp_path: Path) -> No tmp_path, version="0.1.0", run_store_path=".runs", - vendor_sha="x", + scaffold_sha="x", python_exe="/custom/python", conda_env_name="custom-env", ) @@ -177,7 +177,7 @@ def test_resolve_invocation_uses_conda_run_when_env_name_present(tmp_path: Path) tmp_path, version="0.1.0", run_store_path=".runs", - vendor_sha="x", + scaffold_sha="x", python_exe="/opt/miniforge3/envs/agentic-exp/bin/python", conda_env_name="agentic-exp", ) @@ -191,7 +191,7 @@ def test_resolve_invocation_falls_back_to_python_exe_when_venv(tmp_path: Path) - tmp_path, version="0.1.0", run_store_path=".runs", - vendor_sha="x", + scaffold_sha="x", python_exe="/home/u/.venv/bin/python", conda_env_name="", # venv, not conda ) @@ -259,7 +259,7 @@ def test_resolve_run_store_path_uses_marker(tmp_path: Path) -> None: tmp_path, version="0.1.0", run_store_path="custom/runs", - vendor_sha="x", + scaffold_sha="x", ) assert resolve_run_store_path(tmp_path) == (tmp_path / "custom" / "runs").resolve() @@ -271,7 +271,7 @@ def test_resolve_run_store_path_defaults(tmp_path: Path) -> None: def test_installed_marker_is_valid_json_with_trailing_newline(tmp_path: Path) -> None: write_installed_marker( - tmp_path, version="0.1.0", run_store_path=".runs", vendor_sha="x" + tmp_path, version="0.1.0", run_store_path=".runs", scaffold_sha="x" ) raw = (tmp_path / INSTALLED_MARKER_REL).read_text(encoding="utf-8") assert raw.endswith("\n") From de804824c1f0e3fb065c1e062da4d4a927cf314b Mon Sep 17 00:00:00 2001 From: Kaden McKeen <mckeenkaden@gmail.com> Date: Thu, 21 May 2026 10:57:59 -0400 Subject: [PATCH 07/10] refactor(debrand): drop the aexp.limina_io deprecation shim The de-brand kept `aexp/limina_io.py` as a re-export shim for one release. There are no external consumers to protect and no API stability commitment yet, so the shim is just dead weight -- remove it. `aexp.limina_io` is gone; import from `aexp.kb_io`. Nothing in the package imported the shim (all internal imports moved to `aexp.kb_io` during the identifier rename); the CHANGELOG note is updated to drop the shim mention. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- CHANGELOG.md | 6 ++---- src/aexp/limina_io.py | 19 ------------------- 2 files changed, 2 insertions(+), 23 deletions(-) delete mode 100644 src/aexp/limina_io.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 442a55d..e2a43c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,14 +36,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (the upstream project the harness was adapted from) is no longer surfaced as a named centerpiece, and the `vendor/` directory framing is gone — the harness reads as plain `aexp`. **Breaking** public-API - renames (old names removed; one shim — see below): + renames (old names removed): - `install_limina()` → `install_scaffold()` - `is_limina_installed()` → `is_scaffold_installed()` - `compute_vendor_sha()` → `compute_scaffold_sha()` - `LiminaArtifactRef` → `ArtifactRef` - - module `aexp.limina_io` → `aexp.kb_io`. `aexp.limina_io` is kept as - a deprecation shim (re-exports `aexp.kb_io`, emits a - `DeprecationWarning`) for one release. + - module `aexp.limina_io` → `aexp.kb_io` Persisted keys are renamed with a **read-side fallback**, so existing signac projects and install markers keep resolving with no migration: diff --git a/src/aexp/limina_io.py b/src/aexp/limina_io.py deleted file mode 100644 index 3ecc004..0000000 --- a/src/aexp/limina_io.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Deprecated alias for :mod:`aexp.kb_io`. - -``aexp.limina_io`` was renamed to :mod:`aexp.kb_io` during the limina -de-brand. This shim re-exports the new module so existing imports keep -working; importing it emits a :class:`DeprecationWarning`. It will be -removed in a future release -- migrate to ``from aexp.kb_io import ...``. -""" -from __future__ import annotations - -import warnings as _warnings - -from aexp.kb_io import * # noqa: F401,F403 (back-compat re-export) - -_warnings.warn( - "aexp.limina_io has been renamed to aexp.kb_io; " - "update imports to `from aexp.kb_io import ...`.", - DeprecationWarning, - stacklevel=2, -) From 81bc1bce8a6b77a892daa023ffb7368c5ff3c17f Mon Sep 17 00:00:00 2001 From: Kaden McKeen <mckeenkaden@gmail.com> Date: Thu, 21 May 2026 11:16:58 -0400 Subject: [PATCH 08/10] docs(readme): drop feature-count tallies "22 tools", "22 verbs", "21 verbs", "22 total", and the "four skills" counts read as feature-flexing rather than informing -- drop them. The enumerations that follow already convey the surface. The structural "three surfaces / three concerns" framing stays: it describes the architecture (and the point of it is "one canonical API"), not a tally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e6bcbc3..3a414c8 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ The missing layer is not another tracker. It's a **grammar** — a structure the | Layer | What lives here | |---|---| -| **Research grammar** | `kb/` artifact graph — Hypothesis → Experiment → Finding plus Literature / Challenge Review / Strategic Review. Claude Code hooks enforce the H→E→F chain at write time. Four research-methodology skills (`experiment-rigor`, `exploratory-sota-research`, `research-devil-advocate`, `build-maintainable-software`) install into `.claude/skills/` | +| **Research grammar** | `kb/` artifact graph — Hypothesis → Experiment → Finding plus Literature / Challenge Review / Strategic Review. Claude Code hooks enforce the H→E→F chain at write time. Research-methodology skills (`experiment-rigor`, `exploratory-sota-research`, `research-devil-advocate`, `build-maintainable-software`) install into `.claude/skills/` | | **Local run state** ([signac](https://signac.readthedocs.io)) | `.runs/.signac/` plus one `.runs/workspace/<job_id>/` directory per run. `job.sp` carries identity params; `job.doc` carries the artifact link, tracker IDs, status, and summary metrics | | **Observability** (**W&B**, optional `[wandb]` extra) | Remote runs grouped by a deterministic slug derived from `(hypothesis_id, experiment_id, condition)`. Offline-by-default on HPC — `aexp sync-offline` walks the run store and syncs every pending run in one call from a login node | @@ -104,7 +104,7 @@ The design bet: agents already know how to run experiments. What they need is a |---|---| | **H→E→F artifact graph** | Every run descends from an Experiment, which descends from a Hypothesis. Findings cite runs with strong references (either specific job IDs or batch selectors). | | **Hook-enforced discipline** | SessionStart, PreToolUse, PostToolUse, and Stop hooks inject active context, block chain violations, and validate KB integrity at turn end. Hooks ship inside the installed package and upgrade via `pip install -U`. | -| **Research methodology skills** | Four SKILL.md files install into `.claude/skills/` — experiment rigor, exploratory SOTA research, devil's advocate review, and build-maintainable-software. Trigger with `$experiment-rigor` etc. | +| **Research methodology skills** | SKILL.md files install into `.claude/skills/` — experiment rigor, exploratory SOTA research, devil's advocate review, and build-maintainable-software. Trigger with `$experiment-rigor` etc. | ### Run state + observability @@ -120,9 +120,9 @@ The design bet: agents already know how to run experiments. What they need is a | | | |---|---| -| **MCP server** | FastMCP with 22 tools covering artifact creation (H/E/F/T), run lifecycle, batch queries, queue management (incl. `queue_stop` for live-job interruption), tracker binding, and validation. Runs via `uvx --from agentic-experiments[mcp] aexp-mcp-server` — no absolute paths, no per-machine config, `.mcp.json` committable to git. | -| **Slash commands** | Artifact creation: `/aexp-new-hypothesis`, `/aexp-new-experiment`, `/aexp-new-run`. Threads (forward-looking research concerns broader than a hypothesis): `/aexp-new-thread`, `/aexp-list-threads`, `/aexp-show-thread`, `/aexp-close-thread`. Finding creation (pick by what the finding cites): `/aexp-finding-from-run`, `/aexp-finding-from-batch`, `/aexp-finding-placeholder`. Read / inspect: `/aexp-show-run`, `/aexp-show-batch`, `/aexp-list-runs`, `/aexp-status`, `/aexp-validate`. Queue: `/aexp-queue-add`, `/aexp-queue-list`, `/aexp-queue-materialize`, `/aexp-queue-stop`. Notebook lifecycle (when `--with-jupyter` is configured): `/aexp-jupyter-iterate` (test loop), `/aexp-promote-nb` (promote working cells into a tracked-run script). Sandbox scaffolding: `/aexp-new-sandbox` (create an exploratory notebook subdir under `notebooks/_sandbox/`). 22 total. | -| **CLI** | 22 verbs covering install, artifact creation (H/E/F/T + thread lifecycle), run lifecycle, batch queries, tracker binding, validation, offline sync, optional `jupyter-setup`, the `queue` subcommand group (add/list/remove/stop/clear/materialize/run) + `run-queued`, and sandbox scaffolding (`new-sandbox`). See `aexp --help` for the full list. Python API is a one-line `from aexp import ...`. | +| **MCP server** | FastMCP covering artifact creation (H/E/F/T), run lifecycle, batch queries, queue management (incl. `queue_stop` for live-job interruption), tracker binding, and validation. Runs via `uvx --from agentic-experiments[mcp] aexp-mcp-server` — no absolute paths, no per-machine config, `.mcp.json` committable to git. | +| **Slash commands** | Artifact creation: `/aexp-new-hypothesis`, `/aexp-new-experiment`, `/aexp-new-run`. Threads (forward-looking research concerns broader than a hypothesis): `/aexp-new-thread`, `/aexp-list-threads`, `/aexp-show-thread`, `/aexp-close-thread`. Finding creation (pick by what the finding cites): `/aexp-finding-from-run`, `/aexp-finding-from-batch`, `/aexp-finding-placeholder`. Read / inspect: `/aexp-show-run`, `/aexp-show-batch`, `/aexp-list-runs`, `/aexp-status`, `/aexp-validate`. Queue: `/aexp-queue-add`, `/aexp-queue-list`, `/aexp-queue-materialize`, `/aexp-queue-stop`. Notebook lifecycle (when `--with-jupyter` is configured): `/aexp-jupyter-iterate` (test loop), `/aexp-promote-nb` (promote working cells into a tracked-run script). Sandbox scaffolding: `/aexp-new-sandbox` (create an exploratory notebook subdir under `notebooks/_sandbox/`). | +| **CLI** | Verbs covering install, artifact creation (H/E/F/T + thread lifecycle), run lifecycle, batch queries, tracker binding, validation, offline sync, optional `jupyter-setup`, the `queue` subcommand group (add/list/remove/stop/clear/materialize/run) + `run-queued`, and sandbox scaffolding (`new-sandbox`). See `aexp --help` for the full list. Python API is a one-line `from aexp import ...`. | | **Typed JSON contracts** | Pydantic models (`RunLink`, `BatchSelector`, `Issue`, …) back the schema; MCP tools and CLI return the same shapes. | | **Jupyter MCP integration** (optional, `[jupyter]` extra) | `aexp install --with-jupyter` adds the `jupyter` MCP server to `.mcp.json` so Claude can read/edit/execute cells in a remote JupyterLab through an existing SSH tunnel — no agent SSH required. The target Jupyter is set per-session at runtime via `connect_to_jupyter`, so one entry retargets to any node. `aexp jupyter-setup` applies the verified Jupyter Server extension state on the cluster (disable Datalayer experiments that conflict with the mainstream stack). After install, see `docs/setup/jupyter-mcp.md` for cluster-side recipe + investigation log. The `/aexp-jupyter-iterate` slash command guides the read → propose → execute loop. | @@ -146,8 +146,8 @@ graph TB end subgraph "aexp (Python package)" - MCP[MCP Server<br/>FastMCP, 22 tools] - CLI[CLI — typer<br/>21 verbs] + MCP[MCP Server<br/>FastMCP] + CLI[CLI — typer] API[Python API<br/>aexp.*] end @@ -198,7 +198,7 @@ aexp install aexp --help ``` -> **Heads up — `aexp install` will modify your repo.** It creates `.mcp.json`, **merges into** any existing `.claude/settings.json` (hooks + permissions are additive; yours are preserved), adds `.claude/skills/` with four research-methodology skills, copies a `kb/` scaffold plus `templates/` into the repo root, initializes `.runs/` as a signac project, and records the interpreter path in `.aexp/installed.json`. It prints the plan and asks for confirmation before writing — pass `--yes` to skip the prompt or `--dry-run` to preview only. **No Python code you didn't write lands in your repo**: hook scripts and validator logic live inside the installed `aexp` package and upgrade via `pip install -U`. +> **Heads up — `aexp install` will modify your repo.** It creates `.mcp.json`, **merges into** any existing `.claude/settings.json` (hooks + permissions are additive; yours are preserved), adds `.claude/skills/` with the research-methodology skills, copies a `kb/` scaffold plus `templates/` into the repo root, initializes `.runs/` as a signac project, and records the interpreter path in `.aexp/installed.json`. It prints the plan and asks for confirmation before writing — pass `--yes` to skip the prompt or `--dry-run` to preview only. **No Python code you didn't write lands in your repo**: hook scripts and validator logic live inside the installed `aexp` package and upgrade via `pip install -U`. See [docs/quickstart.md](docs/quickstart.md) for a full worked example — hypothesis → experiment → runs → finding. From 844e361118b6e58708b90d3187499019aada7e34 Mon Sep 17 00:00:00 2001 From: Kaden McKeen <mckeenkaden@gmail.com> Date: Thu, 21 May 2026 11:43:48 -0400 Subject: [PATCH 09/10] docs(readme): drop the stale version line + roadmap from Status The `## Status` section opened with `Pre-release (v0.2.x)` (the version drifts from pyproject) and carried a `v0.3 backlog` roadmap (drifts as work ships) -- duplicate sources of truth that go stale. Drop both. The platform/CI note, the PyPI-surface note, and the closing line stay; the `status-beta` badge already conveys maturity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 3a414c8..cd65850 100644 --- a/README.md +++ b/README.md @@ -280,11 +280,8 @@ docs/ # concepts, quickstart, cli, mcp, mapping, tracker-adapt ## Status -**Pre-release (v0.2.x).** Actively developed by one person and the agents they direct; used in the author's own ML research workflow. The API surface is not yet stable — see [CHANGELOG.md](CHANGELOG.md) for what has shipped. - - **Developed and primarily tested on Windows 11 / Python 3.12.** Supports Python 3.11+. CI runs the full suite on Ubuntu + Windows × Py 3.11/3.12/3.13. macOS hasn't been exercised — issues welcome. - **MCP server is the only PyPI-gated surface** — the CLI and Python API run from a local checkout without any PyPI round-trip. -- **v0.3 backlog:** `aexp index` dashboard, MLflow / Aim / DVC tracker adapters, OpenTelemetry extra. (Artifact-creation CLI verbs, the three-mode wandb surface, the queue + runner-materialization layer, threads as a new artifact kind, and template/validator strictness all shipped in 0.2.0 — see CHANGELOG for the full breakdown.) If you run ML experiments with Claude Code and find yourself wanting a harness that holds your agent to scientific discipline, this is built for you. Feedback, bug reports, and PRs all welcome. From c873fb1354ce182926ecc75b262fb5e1807cb842 Mon Sep 17 00:00:00 2001 From: Kaden McKeen <mckeenkaden@gmail.com> Date: Thu, 21 May 2026 11:43:48 -0400 Subject: [PATCH 10/10] chore(release): 0.5.0 Cut 0.5.0. Since 0.4.0 this bundles the single-server Jupyter MCP integration (drop jupyter-compute) + the jupyter-mcp-server pin, and the limina de-brand / de-vendor: breaking public-API renames, persisted keys renamed with a dual-read fallback. See CHANGELOG `[0.5.0]`. Minor bump -- pre-1.0, so breaking changes signal via the minor slot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- CHANGELOG.md | 2 ++ pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a43c0..e49a3b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.0] - 2026-05-21 + ### Changed - **The Jupyter MCP integration is now a single MCP server.** `aexp diff --git a/pyproject.toml b/pyproject.toml index 5f506f9..e1173bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agentic-experiments" -version = "0.4.0" +version = "0.5.0" description = "Git-first, hypothesis-forcing experiment tracking for agent-driven ML research. Bundles a research harness for the H->E->F artifact model, uses signac for local execution/run state, and bridges to W&B for remote observability." authors = [ {name = "Kaden McKeen", email = "mckeenkaden@gmail.com"}