From 9740695bc44fc5a32ee71167dcd7131530230400 Mon Sep 17 00:00:00 2001 From: Ben Thomasson Date: Mon, 20 Jul 2026 13:40:51 -0400 Subject: [PATCH 1/4] Auto-create missing labels in file-issues before filing Adds _ensure_labels() that checks the target repo for required labels and creates any that are missing. Fixes #25. Also adds verification commands and reasons database usage guide to CLAUDE.md template. Co-Authored-By: Claude --- ftl_code_expert/cli.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/ftl_code_expert/cli.py b/ftl_code_expert/cli.py index 0536bfc..df4cfaa 100644 --- a/ftl_code_expert/cli.py +++ b/ftl_code_expert/cli.py @@ -2986,6 +2986,35 @@ def _build_negative_issue_body(belief: dict) -> str: return "\n".join(lines) +def _ensure_labels(platform: str, repo_slug: str, required: set[str]) -> None: + """Create any missing labels on the target repo.""" + if platform == "github": + result = subprocess.run( + ["gh", "label", "list", "--repo", repo_slug, "--json", "name", "-q", ".[].name"], + capture_output=True, text=True, + ) + existing = set(result.stdout.strip().splitlines()) if result.returncode == 0 else set() + for label in required - existing: + click.echo(f" Creating label: {label}", err=True) + subprocess.run( + ["gh", "label", "create", label, "--repo", repo_slug, + "--description", "Auto-created by code-expert file-issues"], + capture_output=True, text=True, + ) + elif platform == "gitlab": + result = subprocess.run( + ["glab", "label", "list", "--repo", repo_slug], + capture_output=True, text=True, + ) + existing = set(result.stdout.strip().splitlines()) if result.returncode == 0 else set() + for label in required - existing: + click.echo(f" Creating label: {label}", err=True) + subprocess.run( + ["glab", "label", "create", label, "--repo", repo_slug], + capture_output=True, text=True, + ) + + def _create_issue(platform: str, repo_slug: str, title: str, body: str, labels: list[str]) -> str | None: """Create an issue and return its URL, or None on failure.""" @@ -3494,6 +3523,11 @@ def file_issues(ctx, repo_slug, platform_override, labels, dry_run, skip_confirm click.echo(f" {unconfirmed} belief(s) no longer present in code", err=True) remaining = confirmed + # Ensure required labels exist + if not dry_run and remaining: + required_labels = {"reasons-gate", "reasons-negative"} | set(labels) + _ensure_labels(platform, repo_slug, required_labels) + # File issues filed = [] skipped_ids = list(existing) From bc760f7c6793153fe4a76c8540e6fb9fd65ebff4 Mon Sep 17 00:00:00 2001 From: Ben Thomasson Date: Mon, 20 Jul 2026 13:41:26 -0400 Subject: [PATCH 2/4] Add verification commands and reasons database guide to CLAUDE.md template Co-Authored-By: Claude --- ftl_code_expert/data/CLAUDE.md.template | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/ftl_code_expert/data/CLAUDE.md.template b/ftl_code_expert/data/CLAUDE.md.template index 985bdeb..c65a552 100644 --- a/ftl_code_expert/data/CLAUDE.md.template +++ b/ftl_code_expert/data/CLAUDE.md.template @@ -56,6 +56,45 @@ reasons compact code-expert update --since-last # walk commits + propose + derive + summary code-expert generate-summary # standalone morning summary +# Verification +code-expert verify # check one belief against current code +code-expert verify --all # verify all IN beliefs +code-expert verify --gated # verify beliefs blocking downstream chains +code-expert verify --negative --retract # verify negative beliefs, retract stale ones +code-expert infer-sources --all # infer source files for beliefs missing them + # Status code-expert status ``` + +## Using the Reasons Database + +When a `reasons.db` exists, **search beliefs before reading code**. The belief network contains verified claims about the codebase — architecture, patterns, bugs, invariants — that persist across sessions. + +### Answering questions about the code + +1. `reasons search ""` — find beliefs relevant to the question +2. `reasons show ` — read the full belief, its justification chain, and metadata +3. `reasons explain ` — trace why a belief is IN or OUT +4. Read the actual source code to confirm the belief still holds +5. If a belief is stale, run `code-expert verify ` to formally check it + +### Before modifying code + +1. `reasons search ""` — find beliefs about the affected code +2. Check for negative beliefs (bugs, gaps, risks) that the change might interact with +3. Check for gated beliefs — changes might unblock or break downstream reasoning chains +4. After making changes, verify affected beliefs: `code-expert verify --category ` + +### Key reasons commands + +```bash +reasons search "thread safety" # semantic search across all beliefs +reasons show # full details including dependents +reasons explain # trace justification chain (why IN or OUT) +reasons list # all nodes with status +reasons list --negative # bugs, gaps, risks +reasons list --gated # beliefs blocked by negative findings +``` + +The belief network is the project's accumulated knowledge. Searching it first avoids re-discovering what is already known and surfaces constraints that pure code reading would miss. From 28855eca1c029cec10fe7af4ab271823ed8b8c38 Mon Sep 17 00:00:00 2001 From: Ben Thomasson Date: Mon, 20 Jul 2026 13:48:24 -0400 Subject: [PATCH 3/4] Fix GitLab label parsing and GitHub pagination in _ensure_labels - GitHub: add -L 1000 to gh label list to handle repos with many labels - GitLab: use -F json output format and parse JSON instead of table output Co-Authored-By: Claude --- ftl_code_expert/cli.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ftl_code_expert/cli.py b/ftl_code_expert/cli.py index df4cfaa..8c22305 100644 --- a/ftl_code_expert/cli.py +++ b/ftl_code_expert/cli.py @@ -2990,7 +2990,8 @@ def _ensure_labels(platform: str, repo_slug: str, required: set[str]) -> None: """Create any missing labels on the target repo.""" if platform == "github": result = subprocess.run( - ["gh", "label", "list", "--repo", repo_slug, "--json", "name", "-q", ".[].name"], + ["gh", "label", "list", "--repo", repo_slug, "-L", "1000", + "--json", "name", "-q", ".[].name"], capture_output=True, text=True, ) existing = set(result.stdout.strip().splitlines()) if result.returncode == 0 else set() @@ -3003,10 +3004,18 @@ def _ensure_labels(platform: str, repo_slug: str, required: set[str]) -> None: ) elif platform == "gitlab": result = subprocess.run( - ["glab", "label", "list", "--repo", repo_slug], + ["glab", "label", "list", "--repo", repo_slug, "-F", "json"], capture_output=True, text=True, ) - existing = set(result.stdout.strip().splitlines()) if result.returncode == 0 else set() + existing: set[str] = set() + if result.returncode == 0 and result.stdout.strip(): + try: + for item in json.loads(result.stdout): + name = item.get("name", "") + if name: + existing.add(name) + except (json.JSONDecodeError, TypeError): + pass for label in required - existing: click.echo(f" Creating label: {label}", err=True) subprocess.run( From 5602ea4e3193fa4de4f6216a5556f14176d5181a Mon Sep 17 00:00:00 2001 From: Ben Thomasson Date: Mon, 20 Jul 2026 13:53:26 -0400 Subject: [PATCH 4/4] Harden _ensure_labels against edge cases - Guard GitLab JSON parsing against non-array/non-dict responses - Use --name flag for glab label create (positional args not supported) - Log errors when label creation fails on either platform Co-Authored-By: Claude --- ftl_code_expert/cli.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/ftl_code_expert/cli.py b/ftl_code_expert/cli.py index 8c22305..240abab 100644 --- a/ftl_code_expert/cli.py +++ b/ftl_code_expert/cli.py @@ -2997,11 +2997,13 @@ def _ensure_labels(platform: str, repo_slug: str, required: set[str]) -> None: existing = set(result.stdout.strip().splitlines()) if result.returncode == 0 else set() for label in required - existing: click.echo(f" Creating label: {label}", err=True) - subprocess.run( + r = subprocess.run( ["gh", "label", "create", label, "--repo", repo_slug, "--description", "Auto-created by code-expert file-issues"], capture_output=True, text=True, ) + if r.returncode != 0: + click.echo(f" Failed to create label {label}: {r.stderr.strip()}", err=True) elif platform == "gitlab": result = subprocess.run( ["glab", "label", "list", "--repo", repo_slug, "-F", "json"], @@ -3010,18 +3012,23 @@ def _ensure_labels(platform: str, repo_slug: str, required: set[str]) -> None: existing: set[str] = set() if result.returncode == 0 and result.stdout.strip(): try: - for item in json.loads(result.stdout): - name = item.get("name", "") - if name: - existing.add(name) - except (json.JSONDecodeError, TypeError): + parsed = json.loads(result.stdout) + if isinstance(parsed, list): + for item in parsed: + if isinstance(item, dict): + name = item.get("name", "") + if name: + existing.add(name) + except (json.JSONDecodeError, TypeError, AttributeError): pass for label in required - existing: click.echo(f" Creating label: {label}", err=True) - subprocess.run( - ["glab", "label", "create", label, "--repo", repo_slug], + r = subprocess.run( + ["glab", "label", "create", "--name", label, "--repo", repo_slug], capture_output=True, text=True, ) + if r.returncode != 0: + click.echo(f" Failed to create label {label}: {r.stderr.strip()}", err=True) def _create_issue(platform: str, repo_slug: str, title: str, body: str,