Skip to content

feat: task-aware generator profiles (workload-selectable, tier-aware) - #177

Merged
jaylfc merged 9 commits into
masterfrom
feat/generator-profiles
Jun 29, 2026
Merged

feat: task-aware generator profiles (workload-selectable, tier-aware)#177
jaylfc merged 9 commits into
masterfrom
feat/generator-profiles

Conversation

@jaylfc

@jaylfc jaylfc commented Jun 29, 2026

Copy link
Copy Markdown
Owner

What

Make the answer/memory generator selectable by workload instead of a single global choice. The generator win is task-dependent (gemma4:12b wins LongMemEval single-fact QA but loses LoCoMo and BEAM), so flipping the global default would trade two benchmarks for one. This adds a data-driven, tier-aware generator-profile registry with a safe default.

Design

An orthogonal taosmd/generator_profiles.py registry sits alongside the retrieval recipes. Each profile maps a workload to a generator model per hardware tier (an empty string means retrieval-only on tiny devices). resolve_generator precedence is pin > active profile (per-agent > global, default balanced) > recipe generator > retrieval-only. config.resolve_memory_model delegates to it, and apply_recipe no longer auto-seeds memory_model (so a profile is never shadowed by what looked like a user pin).

Seeds:

  • balanced (default): qwen3.5:9b at 12/8 GB, llama3.1:8b at 4 GB, retrieval-only on Pi. Mirrors the previous per-tier recipe generators exactly, so default behaviour is unchanged.
  • factual-recall (opt-in): gemma4:12b at 12 GB, llama3.1:8b at 8 and 4 GB. Wins single-fact retrieval QA; loses on conversational and long-context, so it is opt-in.

Backends are local and none only. Remote and cloud generation are deferred to a follow-up generator-backend-abstraction spec.

Evidence

The 8 GB and 4 GB factual picks (llama3.1:8b) are confirmed by the E-023 low-tier bench (F-015): on LongMemEval full-500, llama3.1:8b scored 49.2 (Qwen judge) / 54.4 (llama judge) and beat the shipped qwen3.5:9b (42.8) on the cross-family judge. qwen3:4b was recorded invalid (it leaked the self-verify scratchpad as its answer) and gemma4:e4b was weaker.

Surfacing

  • CLI: taosmd generator-profile list | show <id> | set <id> [--agent NAME]
  • Config (global) and per-agent storage
  • Controls registry: the profile appears in the dashboard read-only (consumer-scope controls are not POST-settable); the CLI is the setter. A dashboard setter widget is a possible follow-up.

Quality

Built as 8 TDD tasks, each with a spec + quality review, plus a whole-branch review. The whole-branch review caught a real regression: one of three get_memory_model read sites (memory_extractor.py) was orphaned by the auto-seed removal, which would have silently dropped LLM fact-extraction to regex on a fresh install. Fixed, with a regression test proven to fail against the buggy code. Full suite 1009 passed.

Spec: docs/superpowers/specs/2026-06-24-task-aware-generator-profiles-design.md
Plan: docs/superpowers/plans/2026-06-29-task-aware-generator-profiles.md

Summary by CodeRabbit

  • New Features

    • Added generator profiles for selecting answer models by workload, including a default profile and an opt-in factual-recall profile.
    • Added a new command-line flow to list, inspect, and set generator profiles, including per-agent selection.
    • Added support for viewing the new generator profile control in the dashboard and configuration docs.
  • Bug Fixes

    • Preserved user-selected settings when applying recipes and improved fallback behavior for model resolution.

jaylfc added 9 commits June 29, 2026 21:11
…ect dashboard-settability + 8GB docs; trim stale apply_recipe docstring

- memory_extractor.py L259-263: switch from get_memory_model to resolve_memory_model so extraction routes through the generator-profile resolver (pin > profile > tier) rather than only reading the raw config pin
- README.md + spec: remove false claim that generator_profile can be set from the dashboard Settings panel; consumer-scope controls are read-only in the dashboard and set via CLI only
- README.md: fix balanced profile 8 GB description (qwen3.5:9b on 12/8 GB, llama3.1:8b on 4 GB only)
- recipes.py apply_recipe docstring: drop stale clause about writing generator model to config (auto-seed removed in 728be1e)
- tests/test_memory_extractor_model_resolution.py: regression coverage for the resolve_memory_model code path
…test

Replace vacuous isolation test with a spy-based test that calls
process_conversation_turn directly. The spy monkeypatches
extract_facts_with_llm to capture the model kwarg and raise a sentinel,
short-circuiting before kg access. Test 1 asserts the model is NOT
"default" (balanced@gpu-12gb -> qwen3.5:9b), which fails on the pre-fix
get_memory_model() path. Test 2 verifies the "default" fallback when the
tier is absent from the balanced map.
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a task-aware generator profile system (taosmd/generator_profiles.py) with balanced and factual-recall profiles mapping hardware tiers to generator models. Profile selection is persisted globally via config.py and per-agent via agents.py, surfaced through a new taosmd generator-profile CLI subcommand, registered as a consumer control, and integrated into resolve_memory_model and memory_extractor. Recipe application no longer auto-seeds the global memory model.

Changes

Generator Profile System

Layer / File(s) Summary
GeneratorProfile dataclass and registry
taosmd/generator_profiles.py
Defines GeneratorProfile dataclass, TIER_ORDER, private _REGISTRY, and registers balanced and factual-recall profiles with per-tier model strings (empty string = retrieval-only). Adds get_profile, list_profiles, default_profile_id query helpers.
resolve_generator precedence chain
taosmd/generator_profiles.py
Implements resolve_generator() with pin → agent profile → global profile → "balanced" default precedence, then performs per-tier lookup; returns fallback or "" when tier is absent.
Config and agent profile persistence
taosmd/config.py, taosmd/agents.py
Adds generator_profile config key, get/set_generator_profile helpers, and rewires resolve_memory_model to delegate to resolve_generator. Adds get/set_agent_generator_profile methods to AgentRegistry with module-level wrappers.
Controls registration and recipe auto-seed removal
taosmd/controls.py, taosmd/recipes.py
Registers generator_profile as a consumer-scoped choice control sourced from the profile registry. Removes apply_recipe()'s code that seeded the global memory model from the recipe's generator field.
memory_extractor wiring
taosmd/memory_extractor.py
Switches process_conversation_turn from get_memory_model to resolve_memory_model for profile-aware LLM model resolution, preserving "default" fallback.
CLI generator-profile subcommand
taosmd/cli.py
Adds _generator_profile_list, _generator_profile_show, _generator_profile_set helpers and wires generator-profile list/show/set [--agent] subcommands into the parser and main() dispatch.
Tests and docs
tests/test_generator_profiles.py, tests/test_generator_resolution.py, tests/test_generator_profile_*.py, tests/test_memory_extractor_model_resolution.py, tests/test_recipes.py, tests/test_config_memory_model.py, README.md, docs/*
Adds profile registry, resolution precedence, config/agent roundtrip, CLI, controls, memory extractor regression, and recipe no-seed tests; updates existing resolve_memory_model tests for the new delegation; updates README, benchmarks, and design spec.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 A rabbit hops through tiers of GPU might,
Balanced or factual, profiles set just right,
The CLI whispers: "set factual-recall!"
No more auto-seeding — profiles handle all.
Each tier maps a model, or empty string replies,
Retrieval-only skies for Pi beneath the skies! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding task-aware, tier-aware generator profiles.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/generator-profiles

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

jaylfc added a commit that referenced this pull request Jun 29, 2026
…e-branch review, regression caught+fixed), PR #177 open for sign-off
Comment thread taosmd/cli.py
print(f"error: unknown profile {profile_id!r}", file=sys.stderr)
return 1
if agent:
agents.set_agent_generator_profile(agent, profile_id, data_dir=data_dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: _generator_profile_set does not catch AgentNotFoundError raised by agents.set_agent_generator_profile(agent, ...). Running taosmd generator-profile set <id> --agent does-not-exist will print a Python traceback to stderr instead of a clean error message and a non-zero exit. Other CLI sites that touch per-agent state (cli.py:69, 108, 636) all wrap this exception. Mirror that pattern here.

Suggested change
agents.set_agent_generator_profile(agent, profile_id, data_dir=data_dir)
if agent:
try:
agents.set_agent_generator_profile(agent, profile_id, data_dir=data_dir)
except agents.AgentNotFoundError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
print(f"agent {agent}: generator profile = {profile_id}")

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

resolved_model = extraction_model
if not resolved_model or resolved_model == "default":
resolved_model = get_memory_model() or "default"
resolved_model = resolve_memory_model() or "default"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CRITICAL: resolve_memory_model() is called here without an agent argument, so generator_profiles.resolve_generator never sees the per-agent profile and silently falls through to the global profile. The new per-agent branch (and its test_per_agent_beats_global coverage) is therefore unreachable from the production extraction path. Either pass agent_name through process_conversation_turnresolve_memory_model(agent=...), or document the limitation explicitly so users do not assume --agent works for fact extraction. This is the exact regression shape that commit f203a77 (the memory_extractor fix) was meant to guard against — but it only fixed the global profile path.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

return "balanced"


def resolve_generator(agent: str | None = None, *, fallback: str | None = None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Doc/code divergence on the resolution precedence. The PR description and config.resolve_memory_model (config.py:256-261) advertise pin > profile > recipe generator > fallback > retrieval-only, but this function never consults recipe.generator["model"]. The previous behaviour (auto-seeded by apply_recipe) is also gone (recipes.py:393-398 removed the seed). The result: a user who applies a recipe that names a generator no longer gets that generator — they get whatever the active profile says for the detected tier, with no recipe visibility. Either re-implement the recipe-generator branch in resolve_generator (e.g. recipes.get_recipe(_agents.get_agent(agent)["applied_recipe_id"]).generator.get("model", "")) or update the spec docstring + README + design doc to drop the "recipe generator" rung from the precedence list.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread taosmd/agents.py
pid = rec.get("generator_profile_id")
return pid if isinstance(pid, str) and pid.strip() else None

def set_agent_generator_profile(self, name: str, profile_id: str | None) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: set_agent_generator_profile (and the matching config.set_generator_profile at config.py:131-144) persist any non-empty string without validating it against generator_profiles.get_profile(...). A typo like "factual-recal", "Balanced", or "balanced " (trailing space — .strip() does save you here) is silently written to disk; the CLI confirms "set successfully" and the profile is then ignored at resolution time, falling back to default_profile_id(). The CLI wrapper does call gp.get_profile(profile_id) first, so the bug is only reachable from direct Python callers (tests, library users, the agent-facing API). Add a registry lookup in both setters and raise ValueError for unknown ids so the failure mode is loud.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread taosmd/controls.py
"generator_profile": Control(
id="generator_profile", label="Generator profile",
category="quality", scope="consumer", type="choice",
config_key="generator_profile",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: config_key="generator_profile" is a flat (top-level) key, while every other control in this table uses a dotted path (controls.prefer_verified, vector_memory.late_interaction, answer.self_verify, vector_memory.embed_model, controls.fusion, controls.adjacent_turns, controls.reranker). The asymmetry is harmless today (consumer-scope controls are not part of get_runtime_overrides), but it is a trap for any future code that walks Control.config_key to read/write a value, and the test in tests/test_generator_profile_control.py even uses a or "generator_profile" in str(schema) OR-clause that papers over a real schema-shape ambiguity. Either pick a dotted form (e.g. controls.generator_profile) or drop the field's config_key value to a documented placeholder and fix the test.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 5 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 2
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

CRITICAL

File Line Issue
taosmd/cli.py 176 _generator_profile_set does not catch AgentNotFoundErrortaosmd generator-profile set <id> --agent unknown will traceback instead of erroring cleanly.
taosmd/memory_extractor.py 263 resolve_memory_model() is called without an agent argument, so the per-agent profile branch is silently bypassed in the production extraction path. The test_per_agent_beats_global coverage only exercises gp.resolve_generator(agent, ...) directly.

WARNING

File Line Issue
taosmd/generator_profiles.py 89 Doc/code divergence: the PR description and resolve_memory_model docstring claim pin > profile > recipe generator > fallback, but resolve_generator never consults recipe.generator["model"]. With the auto-seed also removed in apply_recipe, the recipe's generator is dead at the resolution layer.
taosmd/agents.py 332 set_agent_generator_profile (and config.set_generator_profile) persist any non-empty string without validating it against the profile registry. Typos are silently stored and the CLI confirms success while resolution falls back to the default.

SUGGESTION

File Line Issue
taosmd/controls.py 138 config_key="generator_profile" is a flat (top-level) key while every other control in this table uses a dotted path. The corresponding test even uses an or ... in str(schema) OR-clause to paper over the schema-shape ambiguity.
Files Reviewed (19 files)
  • README.md - 0 issues
  • docs/benchmarks.md - 0 issues
  • docs/superpowers/specs/2026-06-24-task-aware-generator-profiles-design.md - 0 issues
  • taosmd/agents.py - 1 issue
  • taosmd/cli.py - 1 issue
  • taosmd/config.py - 0 issues (referenced via generator_profiles.py warning)
  • taosmd/controls.py - 1 issue
  • taosmd/generator_profiles.py - 1 issue
  • taosmd/memory_extractor.py - 1 issue
  • taosmd/recipes.py - 0 issues
  • tests/test_config_memory_model.py - 0 issues
  • tests/test_generator_profile_agent.py - 0 issues
  • tests/test_generator_profile_cli.py - 0 issues
  • tests/test_generator_profile_config.py - 0 issues
  • tests/test_generator_profile_control.py - 0 issues
  • tests/test_generator_profiles.py - 0 issues
  • tests/test_generator_resolution.py - 0 issues
  • tests/test_memory_extractor_model_resolution.py - 0 issues
  • tests/test_recipes.py - 0 issues

Fix these issues in Kilo Cloud


Reviewed by minimax-m3 · Input: 70.6K · Output: 8.3K · Cached: 719.5K

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (2)
tests/test_generator_profile_cli.py (1)

4-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the per-agent CLI branch too.

_generator_profile_set has a separate persistence path when agent is provided, but this file only protects the global path and the unknown-profile error. A small test that registers an agent, calls _generator_profile_set(..., agent="alice"), and asserts agents.get_agent_generator_profile(...) would lock down the new CLI surface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_generator_profile_cli.py` around lines 4 - 18, The current CLI
tests only cover the global generator profile path in _generator_profile_set and
miss the per-agent branch. Add a test that registers an agent, calls
cli._generator_profile_set with a valid profile and agent="alice", and then
verifies the value through agents.get_agent_generator_profile to lock down the
agent-specific persistence behavior alongside the existing unknown-profile
check.
tests/test_generator_profile_agent.py (1)

9-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the field is actually removed on clear.

The last assertion only checks the accessor contract. This would still pass if set_agent_generator_profile(..., None) wrote "" instead of deleting generator_profile_id, so it won't catch a persistence regression in the clear path.

Suggested test tightening
     agents.set_agent_generator_profile("alice", None, data_dir=tmp_path)
     assert agents.get_agent_generator_profile("alice", data_dir=tmp_path) is None
+    rec = agents.AgentRegistry(tmp_path).get_agent("alice")
+    assert "generator_profile_id" not in rec
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_generator_profile_agent.py` around lines 9 - 12, The clear-path
test for agent generator profiles is too weak because it only checks
get_agent_generator_profile, so it can miss cases where
set_agent_generator_profile(..., None) stores an empty value instead of removing
generator_profile_id. Tighten the test in test_generator_profile_agent.py by
asserting the persisted record/state for alice after clearing no longer contains
generator_profile_id, using the existing set_agent_generator_profile and
get_agent_generator_profile flow as the setup. This should verify the field is
actually deleted from storage, not just interpreted as None by the accessor.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@taosmd/agents.py`:
- Around line 332-343: The public setter set_agent_generator_profile currently
writes any non-empty string into agent records, so validate the provided profile
id before saving it. Reuse the same generator-profile lookup/validation used by
the CLI path (or call the generator-profile resolver/registry check) and reject
unknown ids instead of persisting them. Keep the clear/remove behavior for None
or blank values, and apply the same validation to the other affected assignment
path noted in the diff.

In `@taosmd/cli.py`:
- Around line 1777-1784: The generator-profile dispatch in main() is not passing
args.data_dir into the helper calls, so list/show/set use the default store
instead of the user-selected directory. Update the generator-profile branch to
thread args.data_dir through _generator_profile_list, _generator_profile_show,
and _generator_profile_set, matching their existing data_dir parameter. Keep the
profile_id and agent arguments unchanged while ensuring every generator-profile
subcommand operates on the same data directory from args.data_dir.
- Around line 169-177: The _generator_profile_set CLI path currently lets
agents.set_agent_generator_profile() raise AgentNotFoundError for an unknown
--agent, which produces a traceback instead of a clean CLI error. Update
_generator_profile_set to catch AgentNotFoundError around the agent update call,
print a normal error: message to stderr that includes the agent name, and return
a nonzero exit code while keeping the existing profile validation and success
print behavior intact.

In `@taosmd/config.py`:
- Around line 256-265: resolve_memory_model currently hides per-agent overrides
because it always calls generator_profiles.resolve_generator() without an agent
context. Update resolve_memory_model to accept and forward an agent parameter,
then make taosmd.memory_extractor.process_conversation_turn() pass the active
agent through this shim so the new per-agent precedence in resolve_generator()
can be reached on the extraction path.
- Around line 120-146: The config setter currently accepts any non-empty string,
so bad profile ids can be persisted and later bypass the documented default
behavior. Update set_generator_profile() in taosmd/config.py to reject unknown
ids at the boundary by validating against the registered generator profiles
before writing. Keep the clear=True path unchanged, and ensure
get_generator_profile()/resolve_generator() only ever see known profile ids or
None.

In `@taosmd/memory_extractor.py`:
- Around line 259-263: The memory model resolution in process_conversation_turn
is still using only the global default path, so agent-specific generator-profile
overrides are ignored. Update the resolve_memory_model call path to accept and
use agent_name (or the appropriate agent/profile context) when extraction_model
is unset or "default", and ensure the fallback logic still preserves the global
default only when no per-agent override exists. Keep the fix localized around
process_conversation_turn and resolve_memory_model so fact extraction honors
generator-profile set --agent for that agent.

In `@tests/test_generator_profile_control.py`:
- Around line 21-24: The test in test_generator_profile_in_schema is too weak
because it string-matches the schema instead of verifying the actual controls
payload. Update the assertion to inspect the structured result from
controls.controls_schema() directly and confirm generator_profile appears as a
real control entry in the returned controls list, using the existing
controls_schema symbol and control item ids rather than str(schema).
- Around line 15-18: The test for controls.validate_control is too broad because
it catches any Exception instead of the specific contract. Update
test_generator_profile_rejects_unknown to assert ValueError from
validate_control when passed the "generator_profile" control with an invalid
value like "nope", using the same function and test name to keep the expectation
precise.

In `@tests/test_memory_extractor_model_resolution.py`:
- Around line 99-103: The assertion message in the memory model resolution test
contains unnecessary f-string prefixes, which triggers lint error F541. Update
the assertion in tests/test_memory_extractor_model_resolution.py so the
multi-line message used by the assert on model != "default" is plain string text
rather than f-strings; keep the wording intact and remove the stray f prefixes
from that assertion message.

---

Nitpick comments:
In `@tests/test_generator_profile_agent.py`:
- Around line 9-12: The clear-path test for agent generator profiles is too weak
because it only checks get_agent_generator_profile, so it can miss cases where
set_agent_generator_profile(..., None) stores an empty value instead of removing
generator_profile_id. Tighten the test in test_generator_profile_agent.py by
asserting the persisted record/state for alice after clearing no longer contains
generator_profile_id, using the existing set_agent_generator_profile and
get_agent_generator_profile flow as the setup. This should verify the field is
actually deleted from storage, not just interpreted as None by the accessor.

In `@tests/test_generator_profile_cli.py`:
- Around line 4-18: The current CLI tests only cover the global generator
profile path in _generator_profile_set and miss the per-agent branch. Add a test
that registers an agent, calls cli._generator_profile_set with a valid profile
and agent="alice", and then verifies the value through
agents.get_agent_generator_profile to lock down the agent-specific persistence
behavior alongside the existing unknown-profile check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 599652e3-b121-44d3-bab9-4e18437a2301

📥 Commits

Reviewing files that changed from the base of the PR and between 1559dbf and 70b853c.

📒 Files selected for processing (19)
  • README.md
  • docs/benchmarks.md
  • docs/superpowers/specs/2026-06-24-task-aware-generator-profiles-design.md
  • taosmd/agents.py
  • taosmd/cli.py
  • taosmd/config.py
  • taosmd/controls.py
  • taosmd/generator_profiles.py
  • taosmd/memory_extractor.py
  • taosmd/recipes.py
  • tests/test_config_memory_model.py
  • tests/test_generator_profile_agent.py
  • tests/test_generator_profile_cli.py
  • tests/test_generator_profile_config.py
  • tests/test_generator_profile_control.py
  • tests/test_generator_profiles.py
  • tests/test_generator_resolution.py
  • tests/test_memory_extractor_model_resolution.py
  • tests/test_recipes.py

Comment thread taosmd/agents.py
Comment on lines +332 to +343
def set_agent_generator_profile(self, name: str, profile_id: str | None) -> dict:
"""Set or clear the per-agent generator-profile id (None/'' clears)."""
data = self._read()
for a in data["agents"]:
if a["name"] == name:
if profile_id and profile_id.strip():
a["generator_profile_id"] = profile_id.strip()
else:
a.pop("generator_profile_id", None)
self._write(data)
return dict(a)
raise AgentNotFoundError(f"agent {name!r} is not registered")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate generator-profile ids before writing agent records.

Unlike the CLI path, this public setter accepts any truthy string. A bad generator_profile_id then causes taosmd.generator_profiles.resolve_generator() to miss both the agent profile and the expected default behavior for that agent, falling through to fallback / retrieval-only.

Suggested fix
 def set_agent_generator_profile(self, name: str, profile_id: str | None) -> dict:
     """Set or clear the per-agent generator-profile id (None/'' clears)."""
+    from . import generator_profiles  # lazy: avoids agents<->profiles cycle
     data = self._read()
     for a in data["agents"]:
         if a["name"] == name:
-            if profile_id and profile_id.strip():
-                a["generator_profile_id"] = profile_id.strip()
+            if isinstance(profile_id, str) and profile_id.strip():
+                normalized = profile_id.strip()
+                if generator_profiles.get_profile(normalized) is None:
+                    raise ValueError(f"unknown generator profile {normalized!r}")
+                a["generator_profile_id"] = normalized
             else:
                 a.pop("generator_profile_id", None)
             self._write(data)
             return dict(a)

Also applies to: 620-621

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/agents.py` around lines 332 - 343, The public setter
set_agent_generator_profile currently writes any non-empty string into agent
records, so validate the provided profile id before saving it. Reuse the same
generator-profile lookup/validation used by the CLI path (or call the
generator-profile resolver/registry check) and reject unknown ids instead of
persisting them. Keep the clear/remove behavior for None or blank values, and
apply the same validation to the other affected assignment path noted in the
diff.

Comment thread taosmd/cli.py
Comment on lines +169 to +177
def _generator_profile_set(profile_id: str, agent=None, data_dir=None) -> int:
from . import generator_profiles as gp
from . import config, agents
if gp.get_profile(profile_id) is None:
print(f"error: unknown profile {profile_id!r}", file=sys.stderr)
return 1
if agent:
agents.set_agent_generator_profile(agent, profile_id, data_dir=data_dir)
print(f"agent {agent}: generator profile = {profile_id}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle unknown --agent without a traceback.

Line 176 calls agents.set_agent_generator_profile(), which raises AgentNotFoundError for an unregistered agent. Right now that escapes the CLI and prints a Python traceback instead of a normal error: message and nonzero exit.

Suggested fix
 def _generator_profile_set(profile_id: str, agent=None, data_dir=None) -> int:
     from . import generator_profiles as gp
     from . import config, agents
     if gp.get_profile(profile_id) is None:
         print(f"error: unknown profile {profile_id!r}", file=sys.stderr)
         return 1
     if agent:
-        agents.set_agent_generator_profile(agent, profile_id, data_dir=data_dir)
+        try:
+            agents.set_agent_generator_profile(agent, profile_id, data_dir=data_dir)
+        except agents.AgentNotFoundError as exc:
+            print(f"error: {exc}", file=sys.stderr)
+            return 1
         print(f"agent {agent}: generator profile = {profile_id}")
     else:
         config.set_generator_profile(profile_id, data_dir=data_dir)
         print(f"global generator profile = {profile_id}")
     return 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _generator_profile_set(profile_id: str, agent=None, data_dir=None) -> int:
from . import generator_profiles as gp
from . import config, agents
if gp.get_profile(profile_id) is None:
print(f"error: unknown profile {profile_id!r}", file=sys.stderr)
return 1
if agent:
agents.set_agent_generator_profile(agent, profile_id, data_dir=data_dir)
print(f"agent {agent}: generator profile = {profile_id}")
def _generator_profile_set(profile_id: str, agent=None, data_dir=None) -> int:
from . import generator_profiles as gp
from . import config, agents
if gp.get_profile(profile_id) is None:
print(f"error: unknown profile {profile_id!r}", file=sys.stderr)
return 1
if agent:
try:
agents.set_agent_generator_profile(agent, profile_id, data_dir=data_dir)
except agents.AgentNotFoundError as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
print(f"agent {agent}: generator profile = {profile_id}")
else:
config.set_generator_profile(profile_id, data_dir=data_dir)
print(f"global generator profile = {profile_id}")
return 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/cli.py` around lines 169 - 177, The _generator_profile_set CLI path
currently lets agents.set_agent_generator_profile() raise AgentNotFoundError for
an unknown --agent, which produces a traceback instead of a clean CLI error.
Update _generator_profile_set to catch AgentNotFoundError around the agent
update call, print a normal error: message to stderr that includes the agent
name, and return a nonzero exit code while keeping the existing profile
validation and success print behavior intact.

Comment thread taosmd/cli.py
Comment on lines +1777 to +1784
if args.cmd == "generator-profile":
if args.generator_profile_cmd == "list":
return _generator_profile_list()
if args.generator_profile_cmd == "show":
return _generator_profile_show(args.profile_id)
if args.generator_profile_cmd == "set":
return _generator_profile_set(args.profile_id, agent=args.agent)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Thread --data-dir through the generator-profile dispatch.

These helpers all accept data_dir, but main() calls them without args.data_dir. So taosmd --data-dir /tmp/x generator-profile set factual-recall still reads/writes the default store, and list/show report the wrong active profile.

Suggested fix
     if args.cmd == "generator-profile":
         if args.generator_profile_cmd == "list":
-            return _generator_profile_list()
+            return _generator_profile_list(data_dir=args.data_dir)
         if args.generator_profile_cmd == "show":
-            return _generator_profile_show(args.profile_id)
+            return _generator_profile_show(args.profile_id, data_dir=args.data_dir)
         if args.generator_profile_cmd == "set":
-            return _generator_profile_set(args.profile_id, agent=args.agent)
+            return _generator_profile_set(
+                args.profile_id,
+                agent=args.agent,
+                data_dir=args.data_dir,
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if args.cmd == "generator-profile":
if args.generator_profile_cmd == "list":
return _generator_profile_list()
if args.generator_profile_cmd == "show":
return _generator_profile_show(args.profile_id)
if args.generator_profile_cmd == "set":
return _generator_profile_set(args.profile_id, agent=args.agent)
if args.cmd == "generator-profile":
if args.generator_profile_cmd == "list":
return _generator_profile_list(data_dir=args.data_dir)
if args.generator_profile_cmd == "show":
return _generator_profile_show(args.profile_id, data_dir=args.data_dir)
if args.generator_profile_cmd == "set":
return _generator_profile_set(
args.profile_id,
agent=args.agent,
data_dir=args.data_dir,
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/cli.py` around lines 1777 - 1784, The generator-profile dispatch in
main() is not passing args.data_dir into the helper calls, so list/show/set use
the default store instead of the user-selected directory. Update the
generator-profile branch to thread args.data_dir through
_generator_profile_list, _generator_profile_show, and _generator_profile_set,
matching their existing data_dir parameter. Keep the profile_id and agent
arguments unchanged while ensuring every generator-profile subcommand operates
on the same data directory from args.data_dir.

Comment thread taosmd/config.py
Comment on lines +120 to +146
def get_generator_profile(data_dir=None) -> str | None:
"""Return the active global generator-profile id, or None if unset."""
pid = _read(data_dir).get(_GENERATOR_PROFILE_KEY)
if isinstance(pid, str) and pid.strip():
return pid
return None


def set_generator_profile(profile_id: str, clear: bool = False, data_dir=None) -> None:
"""Persist the active global generator-profile id.

Args:
profile_id: a registered profile id. Ignored when clear is True.
clear: when True, remove the setting (unset).

Raises:
ValueError: when clear is False and profile_id is not a non-empty str.
"""
data = _read(data_dir)
if clear:
data.pop(_GENERATOR_PROFILE_KEY, None)
_write(data, data_dir)
return
if not isinstance(profile_id, str) or not profile_id.strip():
raise ValueError("profile_id must be a non-empty string")
data[_GENERATOR_PROFILE_KEY] = profile_id.strip()
_write(data, data_dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject unknown profile ids at the config boundary.

cli._generator_profile_set() already rejects unknown ids, but set_generator_profile() still persists any non-empty string. Once that happens, taosmd.generator_profiles.resolve_generator() treats the bad id as a miss and skips the documented balanced default, so a typo here can silently drop generation to fallback / retrieval-only.

Suggested fix
 def get_generator_profile(data_dir=None) -> str | None:
     """Return the active global generator-profile id, or None if unset."""
     pid = _read(data_dir).get(_GENERATOR_PROFILE_KEY)
     if isinstance(pid, str) and pid.strip():
-        return pid
+        from . import generator_profiles  # lazy: avoids config<->profiles cycle
+        pid = pid.strip()
+        if generator_profiles.get_profile(pid) is not None:
+            return pid
     return None
@@
 def set_generator_profile(profile_id: str, clear: bool = False, data_dir=None) -> None:
@@
     if not isinstance(profile_id, str) or not profile_id.strip():
         raise ValueError("profile_id must be a non-empty string")
-    data[_GENERATOR_PROFILE_KEY] = profile_id.strip()
+    from . import generator_profiles  # lazy: avoids config<->profiles cycle
+    profile_id = profile_id.strip()
+    if generator_profiles.get_profile(profile_id) is None:
+        raise ValueError(f"unknown generator profile {profile_id!r}")
+    data[_GENERATOR_PROFILE_KEY] = profile_id
     _write(data, data_dir)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_generator_profile(data_dir=None) -> str | None:
"""Return the active global generator-profile id, or None if unset."""
pid = _read(data_dir).get(_GENERATOR_PROFILE_KEY)
if isinstance(pid, str) and pid.strip():
return pid
return None
def set_generator_profile(profile_id: str, clear: bool = False, data_dir=None) -> None:
"""Persist the active global generator-profile id.
Args:
profile_id: a registered profile id. Ignored when clear is True.
clear: when True, remove the setting (unset).
Raises:
ValueError: when clear is False and profile_id is not a non-empty str.
"""
data = _read(data_dir)
if clear:
data.pop(_GENERATOR_PROFILE_KEY, None)
_write(data, data_dir)
return
if not isinstance(profile_id, str) or not profile_id.strip():
raise ValueError("profile_id must be a non-empty string")
data[_GENERATOR_PROFILE_KEY] = profile_id.strip()
_write(data, data_dir)
def get_generator_profile(data_dir=None) -> str | None:
"""Return the active global generator-profile id, or None if unset."""
pid = _read(data_dir).get(_GENERATOR_PROFILE_KEY)
if isinstance(pid, str) and pid.strip():
from . import generator_profiles # lazy: avoids config<->profiles cycle
pid = pid.strip()
if generator_profiles.get_profile(pid) is not None:
return pid
return None
def set_generator_profile(profile_id: str, clear: bool = False, data_dir=None) -> None:
"""Persist the active global generator-profile id.
Args:
profile_id: a registered profile id. Ignored when clear is True.
clear: when True, remove the setting (unset).
Raises:
ValueError: when clear is False and profile_id is not a non-empty str.
"""
data = _read(data_dir)
if clear:
data.pop(_GENERATOR_PROFILE_KEY, None)
_write(data, data_dir)
return
if not isinstance(profile_id, str) or not profile_id.strip():
raise ValueError("profile_id must be a non-empty string")
from . import generator_profiles # lazy: avoids config<->profiles cycle
profile_id = profile_id.strip()
if generator_profiles.get_profile(profile_id) is None:
raise ValueError(f"unknown generator profile {profile_id!r}")
data[_GENERATOR_PROFILE_KEY] = profile_id
_write(data, data_dir)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/config.py` around lines 120 - 146, The config setter currently accepts
any non-empty string, so bad profile ids can be persisted and later bypass the
documented default behavior. Update set_generator_profile() in taosmd/config.py
to reject unknown ids at the boundary by validating against the registered
generator profiles before writing. Keep the clear=True path unchanged, and
ensure get_generator_profile()/resolve_generator() only ever see known profile
ids or None.

Comment thread taosmd/config.py
Comment on lines 256 to +265
def resolve_memory_model(fallback: str | None = None, data_dir=None) -> str | None:
"""Return the global memory model if set, else ``fallback``.
"""Resolve the active generator model: pin > profile(tier) > fallback.

Consumers call this so an unset global transparently falls back to
their existing default. Standalone installs that never set a model
keep working exactly as before.
Delegates to generator_profiles.resolve_generator (lazy import to avoid a
cycle). Returns None when resolution yields the empty (retrieval-only)
value AND no fallback was given, preserving the historical None contract.
"""
model = get_memory_model(data_dir)
return model if model is not None else fallback
from . import generator_profiles # lazy: avoids config<->profiles cycle
resolved = generator_profiles.resolve_generator(fallback=fallback, data_dir=data_dir)
return resolved or None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Per-agent overrides are unreachable through this shim.

taosmd.memory_extractor.process_conversation_turn() now resolves through resolve_memory_model(), but this helper has no agent parameter and always calls resolve_generator() globally. That makes the new per-agent precedence in taosmd.generator_profiles.resolve_generator() impossible to reach on the extraction path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/config.py` around lines 256 - 265, resolve_memory_model currently
hides per-agent overrides because it always calls
generator_profiles.resolve_generator() without an agent context. Update
resolve_memory_model to accept and forward an agent parameter, then make
taosmd.memory_extractor.process_conversation_turn() pass the active agent
through this shim so the new per-agent precedence in resolve_generator() can be
reached on the extraction path.

Comment on lines +259 to +263
from .config import resolve_memory_model # noqa: PLC0415

resolved_model = extraction_model
if not resolved_model or resolved_model == "default":
resolved_model = get_memory_model() or "default"
resolved_model = resolve_memory_model() or "default"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Per-agent generator-profile overrides are still bypassed here.

process_conversation_turn() has agent_name, but resolve_memory_model() only receives fallback/data_dir in the contract shown here. That means this path can only resolve against the global/default profile, so generator-profile set --agent ... never affects fact extraction for that agent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@taosmd/memory_extractor.py` around lines 259 - 263, The memory model
resolution in process_conversation_turn is still using only the global default
path, so agent-specific generator-profile overrides are ignored. Update the
resolve_memory_model call path to accept and use agent_name (or the appropriate
agent/profile context) when extraction_model is unset or "default", and ensure
the fallback logic still preserves the global default only when no per-agent
override exists. Keep the fix localized around process_conversation_turn and
resolve_memory_model so fact extraction honors generator-profile set --agent for
that agent.

Comment on lines +15 to +18
def test_generator_profile_rejects_unknown():
import pytest
with pytest.raises(Exception):
controls.validate_control("generator_profile", "nope")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== test file ==\n'
git ls-files 'tests/test_generator_profile_control.py' 'taosmd/controls.py' | cat

printf '\n== test excerpt ==\n'
sed -n '1,80p' tests/test_generator_profile_control.py

printf '\n== validator excerpt ==\n'
sed -n '1,220p' taosmd/controls.py

printf '\n== locate validate_control references ==\n'
rg -n "def validate_control|validate_control\(" -S .

Repository: jaylfc/taosmd

Length of output: 14768


Assert ValueError here. pytest.raises(Exception) is too broad; this validator raises ValueError for invalid choices, so the test should pin that contract.

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 17-17: Do not assert blind exception: Exception

(B017)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_generator_profile_control.py` around lines 15 - 18, The test for
controls.validate_control is too broad because it catches any Exception instead
of the specific contract. Update test_generator_profile_rejects_unknown to
assert ValueError from validate_control when passed the "generator_profile"
control with an invalid value like "nope", using the same function and test name
to keep the expectation precise.

Source: Linters/SAST tools

Comment on lines +21 to +24
def test_generator_profile_in_schema():
schema = controls.controls_schema()
ids = [c["id"] for c in schema] if isinstance(schema, list) else list(schema)
assert "generator_profile" in ids or "generator_profile" in str(schema)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Check the controls payload directly instead of string-matching it.

This currently proves only that the substring appears somewhere in the serialized schema. It does not verify that generator_profile is actually exposed as a control entry in the controls list returned by controls_schema().

Proposed fix
 def test_generator_profile_in_schema():
     schema = controls.controls_schema()
-    ids = [c["id"] for c in schema] if isinstance(schema, list) else list(schema)
-    assert "generator_profile" in ids or "generator_profile" in str(schema)
+    assert any(c["id"] == "generator_profile" for c in schema["controls"])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_generator_profile_in_schema():
schema = controls.controls_schema()
ids = [c["id"] for c in schema] if isinstance(schema, list) else list(schema)
assert "generator_profile" in ids or "generator_profile" in str(schema)
def test_generator_profile_in_schema():
schema = controls.controls_schema()
assert any(c["id"] == "generator_profile" for c in schema["controls"])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_generator_profile_control.py` around lines 21 - 24, The test in
test_generator_profile_in_schema is too weak because it string-matches the
schema instead of verifying the actual controls payload. Update the assertion to
inspect the structured result from controls.controls_schema() directly and
confirm generator_profile appears as a real control entry in the returned
controls list, using the existing controls_schema symbol and control item ids
rather than str(schema).

Comment on lines +99 to +103
assert model != "default", (
f"model resolved to sentinel 'default'; expected a profile-derived model. "
f"This indicates process_conversation_turn is still using get_memory_model() "
f"instead of resolve_memory_model()."
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the stray f prefixes in this assertion message.

Ruff is flagging Lines 100-102 with F541 because these literals don't interpolate anything, so this file will fail lint as written.

Minimal fix
     assert model != "default", (
-        f"model resolved to sentinel 'default'; expected a profile-derived model. "
-        f"This indicates process_conversation_turn is still using get_memory_model() "
-        f"instead of resolve_memory_model()."
+        "model resolved to sentinel 'default'; expected a profile-derived model. "
+        "This indicates process_conversation_turn is still using get_memory_model() "
+        "instead of resolve_memory_model()."
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert model != "default", (
f"model resolved to sentinel 'default'; expected a profile-derived model. "
f"This indicates process_conversation_turn is still using get_memory_model() "
f"instead of resolve_memory_model()."
)
assert model != "default", (
"model resolved to sentinel 'default'; expected a profile-derived model. "
"This indicates process_conversation_turn is still using get_memory_model() "
"instead of resolve_memory_model()."
)
🧰 Tools
🪛 Ruff (0.15.20)

[error] 100-100: f-string without any placeholders

Remove extraneous f prefix

(F541)


[error] 101-101: f-string without any placeholders

Remove extraneous f prefix

(F541)


[error] 102-102: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_memory_extractor_model_resolution.py` around lines 99 - 103, The
assertion message in the memory model resolution test contains unnecessary
f-string prefixes, which triggers lint error F541. Update the assertion in
tests/test_memory_extractor_model_resolution.py so the multi-line message used
by the assert on model != "default" is plain string text rather than f-strings;
keep the wording intact and remove the stray f prefixes from that assertion
message.

Source: Linters/SAST tools

@jaylfc
jaylfc merged commit feb0d01 into master Jun 29, 2026
3 checks passed
jaylfc added a commit that referenced this pull request Jun 30, 2026
…rd-settability (#178) merged; bus-auth held for #1507
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant