zmip 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
zmip-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 chansigit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
zmip-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: zmip
3
+ Version: 0.1.0
4
+ Summary: Zoom-in pipeline: per-lineage re-embedding, foreign-lineage scoring and agent refinement of an msp annotation
5
+ Author-email: chansigit <chansigit@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/chansigit/zmip
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: msp-sc>=0.2.0
15
+ Requires-Dist: claude-agent-sdk
16
+ Requires-Dist: scanpy
17
+ Requires-Dist: anndata
18
+ Requires-Dist: pandas
19
+ Requires-Dist: numpy
20
+ Requires-Dist: scipy
21
+ Requires-Dist: matplotlib
22
+ Dynamic: license-file
23
+
24
+ # zmip — zoom-in pipeline
25
+
26
+ The round after [msp](https://github.com/chansigit/msp): take msp's
27
+ `annotated.h5ad`, split it into lineages, re-embed each lineage on its own,
28
+ and let a per-lineage agent refine the annotation, clean noise and hand
29
+ misassigned cells to the lineage they belong to. Same pattern as osp/msp —
30
+ fixed computation, narrow agent decisions validated by the host, one
31
+ self-contained report per lineage plus a global one.
32
+
33
+ ```
34
+ msp annotated.h5ad ──▶ plan ──▶ per lineage: re-embed → foreign scores → agent ──▶ merge
35
+ (agent) (msp.integrate_adata) (agent) annotated_zmip.h5ad
36
+ ```
37
+
38
+ ## Install
39
+
40
+ ```bash
41
+ pip install msp-sc # msp on PyPI (import name `msp`)
42
+ pip install zmip # needs claude-agent-sdk + Claude Code CLI credentials
43
+ ```
44
+
45
+ ## Usage
46
+
47
+ ```bash
48
+ python -m zmip msp_out/annotated.h5ad --outdir zmip_out --model claude-sonnet-5 [--min-cells 800]
49
+ python -m zmip.report zmip_out # rebuild the global report only
50
+ ```
51
+
52
+ Re-running resumes: the plan is reused, lineages whose contract files
53
+ (`annotation_proposal.json`, `annotated.h5ad`, `report.html`) exist are
54
+ skipped; `--force` redoes everything. Integration knobs (`--resolutions`,
55
+ `--n-top-genes`, `--n-pcs`, `--n-neighbors`, `--harmony KEY=VALUE`) are the
56
+ same as msp's and apply to every per-lineage re-embedding.
57
+
58
+ ## Steps
59
+
60
+ ### 1. plan (`zmip.plan`)
61
+
62
+ Host writes the evidence: cells/samples per coarse label, a kNN
63
+ cross-connectivity matrix between coarse labels (share of each label's
64
+ graph edges landing on every other label), PAGA on the same graph, and the
65
+ coarse-label UMAP. The agent **must read the UMAP** and pools coarse labels
66
+ that form one connected island into one lineage — even across cell types
67
+ when data quality fuses them (T/B/myeloid as one immune island) — and keeps
68
+ separate islands separate even when related; states (proliferating,
69
+ stressed) go with the island they sit in. Host rules: every coarse label
70
+ assigned exactly once; zoom only for lineages with at least `--min-cells`
71
+ (default 800 — below that leiden cannot resolve stable substates); archived
72
+ to `zmip_plan.json`. One lineage or none above the threshold → nothing is
73
+ zoomed and the msp labels pass through.
74
+
75
+ ### 2. per lineage (`zmip.foreign`, `msp.integrate_adata`, `zmip.annotate`)
76
+
77
+ - Subset → `msp.integrate_adata`: HVG/PCA/harmony/leiden(0.3/1.0/2.0)/UMAP
78
+ recomputed on the lineage alone, with every msp artifact (QC tables,
79
+ cell-level outliers, standissect fragments, DEG at r1.0/r2.0,
80
+ `preannotation_removal.csv`) in `<lineage>/`.
81
+ - **Foreign-lineage scores**: lineage-level markers (wilcoxon on the whole
82
+ dataset at the plan's lineage level, specific genes only) → `sc.tl.score_genes`
83
+ for every other lineage → `obs["foreign_<lineage>"]`, per-cluster summaries
84
+ and UMAPs. Evidence only: close lineages share programs, so the agent
85
+ decides between doublet, ambient, misassignment and genuine biology.
86
+ - Agent on `msp_leiden_r2.0` of the subset, one Claude Code Task per
87
+ cluster, tools `cluster_context` / `check_genes` / `check_deg` /
88
+ `check_stability` / `subcluster` (reclustering allowed). Per cluster:
89
+ distinctness → identity → foreign signal → merge, and one action:
90
+ `keep` (coarse label within the lineage), `remove` (with reason), or
91
+ `reassign` to another lineage's coarse label (relabel only — the cells are
92
+ not re-embedded there this round). Host validation as in msp.annotate plus
93
+ the reassign rules. Removal is real: subset pre-annotation filtering ∪
94
+ agent-removed clusters.
95
+ - Outputs: `annotation_proposal.json`, `annotation_removed.csv`,
96
+ `annotation_reassigned.csv`, `annotated.h5ad`, `report.html` (msp's report
97
+ with the lineage's Cell Type Annotation section).
98
+
99
+ ### 3. merge (`zmip.merge`, `zmip.report`)
100
+
101
+ Fold every lineage back into the global object. `annotated_zmip.h5ad` keeps
102
+ the survivors with `zmip_lineage`, `zmip_cluster` (`<lineage>:<id>`),
103
+ `zmip_ann_coarse`, `zmip_ann_fine`, `zmip_reassigned_from`; `msp_ann_*`
104
+ stay for the audit trail. No global re-embedding here (next round's job):
105
+ the global figures use msp's UMAP. Archives `zmip_removed.csv` (every
106
+ removed cell with lineage, cluster, sources) and `zmip_reassigned.csv`.
107
+ `report.html`: plan · lineages (linked per-lineage reports) · final
108
+ annotation · removed & reassigned.
zmip-0.1.0/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # zmip — zoom-in pipeline
2
+
3
+ The round after [msp](https://github.com/chansigit/msp): take msp's
4
+ `annotated.h5ad`, split it into lineages, re-embed each lineage on its own,
5
+ and let a per-lineage agent refine the annotation, clean noise and hand
6
+ misassigned cells to the lineage they belong to. Same pattern as osp/msp —
7
+ fixed computation, narrow agent decisions validated by the host, one
8
+ self-contained report per lineage plus a global one.
9
+
10
+ ```
11
+ msp annotated.h5ad ──▶ plan ──▶ per lineage: re-embed → foreign scores → agent ──▶ merge
12
+ (agent) (msp.integrate_adata) (agent) annotated_zmip.h5ad
13
+ ```
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install msp-sc # msp on PyPI (import name `msp`)
19
+ pip install zmip # needs claude-agent-sdk + Claude Code CLI credentials
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```bash
25
+ python -m zmip msp_out/annotated.h5ad --outdir zmip_out --model claude-sonnet-5 [--min-cells 800]
26
+ python -m zmip.report zmip_out # rebuild the global report only
27
+ ```
28
+
29
+ Re-running resumes: the plan is reused, lineages whose contract files
30
+ (`annotation_proposal.json`, `annotated.h5ad`, `report.html`) exist are
31
+ skipped; `--force` redoes everything. Integration knobs (`--resolutions`,
32
+ `--n-top-genes`, `--n-pcs`, `--n-neighbors`, `--harmony KEY=VALUE`) are the
33
+ same as msp's and apply to every per-lineage re-embedding.
34
+
35
+ ## Steps
36
+
37
+ ### 1. plan (`zmip.plan`)
38
+
39
+ Host writes the evidence: cells/samples per coarse label, a kNN
40
+ cross-connectivity matrix between coarse labels (share of each label's
41
+ graph edges landing on every other label), PAGA on the same graph, and the
42
+ coarse-label UMAP. The agent **must read the UMAP** and pools coarse labels
43
+ that form one connected island into one lineage — even across cell types
44
+ when data quality fuses them (T/B/myeloid as one immune island) — and keeps
45
+ separate islands separate even when related; states (proliferating,
46
+ stressed) go with the island they sit in. Host rules: every coarse label
47
+ assigned exactly once; zoom only for lineages with at least `--min-cells`
48
+ (default 800 — below that leiden cannot resolve stable substates); archived
49
+ to `zmip_plan.json`. One lineage or none above the threshold → nothing is
50
+ zoomed and the msp labels pass through.
51
+
52
+ ### 2. per lineage (`zmip.foreign`, `msp.integrate_adata`, `zmip.annotate`)
53
+
54
+ - Subset → `msp.integrate_adata`: HVG/PCA/harmony/leiden(0.3/1.0/2.0)/UMAP
55
+ recomputed on the lineage alone, with every msp artifact (QC tables,
56
+ cell-level outliers, standissect fragments, DEG at r1.0/r2.0,
57
+ `preannotation_removal.csv`) in `<lineage>/`.
58
+ - **Foreign-lineage scores**: lineage-level markers (wilcoxon on the whole
59
+ dataset at the plan's lineage level, specific genes only) → `sc.tl.score_genes`
60
+ for every other lineage → `obs["foreign_<lineage>"]`, per-cluster summaries
61
+ and UMAPs. Evidence only: close lineages share programs, so the agent
62
+ decides between doublet, ambient, misassignment and genuine biology.
63
+ - Agent on `msp_leiden_r2.0` of the subset, one Claude Code Task per
64
+ cluster, tools `cluster_context` / `check_genes` / `check_deg` /
65
+ `check_stability` / `subcluster` (reclustering allowed). Per cluster:
66
+ distinctness → identity → foreign signal → merge, and one action:
67
+ `keep` (coarse label within the lineage), `remove` (with reason), or
68
+ `reassign` to another lineage's coarse label (relabel only — the cells are
69
+ not re-embedded there this round). Host validation as in msp.annotate plus
70
+ the reassign rules. Removal is real: subset pre-annotation filtering ∪
71
+ agent-removed clusters.
72
+ - Outputs: `annotation_proposal.json`, `annotation_removed.csv`,
73
+ `annotation_reassigned.csv`, `annotated.h5ad`, `report.html` (msp's report
74
+ with the lineage's Cell Type Annotation section).
75
+
76
+ ### 3. merge (`zmip.merge`, `zmip.report`)
77
+
78
+ Fold every lineage back into the global object. `annotated_zmip.h5ad` keeps
79
+ the survivors with `zmip_lineage`, `zmip_cluster` (`<lineage>:<id>`),
80
+ `zmip_ann_coarse`, `zmip_ann_fine`, `zmip_reassigned_from`; `msp_ann_*`
81
+ stay for the audit trail. No global re-embedding here (next round's job):
82
+ the global figures use msp's UMAP. Archives `zmip_removed.csv` (every
83
+ removed cell with lineage, cluster, sources) and `zmip_reassigned.csv`.
84
+ `report.html`: plan · lineages (linked per-lineage reports) · final
85
+ annotation · removed & reassigned.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zmip"
7
+ version = "0.1.0"
8
+ description = "Zoom-in pipeline: per-lineage re-embedding, foreign-lineage scoring and agent refinement of an msp annotation"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [{ name = "chansigit", email = "chansigit@gmail.com" }]
12
+ requires-python = ">=3.10"
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Intended Audience :: Science/Research",
16
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
17
+ ]
18
+ dependencies = [
19
+ "msp-sc>=0.2.0", # PyPI name of msp (import name `msp`)
20
+ "claude-agent-sdk",
21
+ "scanpy",
22
+ "anndata",
23
+ "pandas",
24
+ "numpy",
25
+ "scipy",
26
+ "matplotlib",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/chansigit/zmip"
31
+
32
+ [tool.setuptools]
33
+ packages = ["zmip"]
zmip-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ """zmip (zoom-in pipeline): per-lineage refinement of an msp annotation.
2
+
3
+ plan UMAP-connected lineages from the coarse labels (agent + host rules)
4
+ zoom each lineage re-embedded on its own (msp.integrate_adata), scored
5
+ for foreign-lineage signal, annotated by its own agent
6
+ (refine fine labels / remove noise / reassign / recluster)
7
+ merge fold back with real removal → annotated_zmip.h5ad + report.html
8
+
9
+ Command line:
10
+ python -m zmip annotated.h5ad --outdir zmip_out [--min-cells 800] [--model ...]
11
+ python -m zmip.report zmip_out
12
+
13
+ Depends on msp (integration core, plots, report machinery) and needs the
14
+ claude-agent-sdk for both agent steps.
15
+ """
16
+
17
+ from .foreign import lineage_markers, score_foreign
18
+ from .merge import merge_back
19
+ from .plan import DEFAULT_MIN_CELLS, plan_lineages, validate_plan
20
+ from .report import generate_report
21
+
22
+ __all__ = ["DEFAULT_MIN_CELLS", "generate_report", "lineage_markers", "merge_back", "plan_lineages",
23
+ "score_foreign", "validate_plan"]
@@ -0,0 +1,151 @@
1
+ """python -m zmip: zoom-in pass over an msp annotated.h5ad.
2
+
3
+ plan agent groups coarse labels into UMAP-connected lineages, picks
4
+ which to zoom (>= --min-cells) → zmip_plan.json
5
+ markers lineage-level marker lists for foreign-lineage scores
6
+ → lineage_markers.csv
7
+ per lineage (sequential): subset → msp.integrate_adata (re-embed) →
8
+ foreign scores → annotation agent (refine / remove / reassign /
9
+ recluster) → <lineage>/{annotation_proposal.json, annotated.h5ad,
10
+ report.html}
11
+ merge fold back, real removal → annotated_zmip.h5ad,
12
+ zmip_removed.csv, zmip_reassigned.csv, report.html
13
+
14
+ Re-running resumes: the plan is reused, lineages whose contract files exist
15
+ are skipped; --force redoes everything. One lineage (or none above the
16
+ threshold) → nothing is zoomed and annotated_zmip.h5ad carries the msp
17
+ labels unchanged.
18
+ """
19
+
20
+ import argparse
21
+ import os
22
+ import sys
23
+
24
+ import pandas as pd
25
+ import scanpy as sc
26
+
27
+ from msp.integrate import integrate_adata
28
+ from msp.plots import save_single_umap, slug
29
+
30
+ from .annotate import PREV_SUFFIX, PREVIOUS_COLS, annotate_lineage
31
+ from .foreign import lineage_markers, score_foreign
32
+ from .merge import merge_back
33
+ from .plan import DEFAULT_MIN_CELLS, plan_lineages
34
+ from .report import generate_report
35
+ from msp.report import write_report_context
36
+
37
+ parser = argparse.ArgumentParser(prog="zmip", description=__doc__,
38
+ formatter_class=argparse.RawDescriptionHelpFormatter)
39
+ parser.add_argument("h5ad", help="msp annotated.h5ad (survivors with msp_ann_coarse/msp_ann_fine)")
40
+ parser.add_argument("--outdir", required=True)
41
+ parser.add_argument("--coarse-col", default="msp_ann_coarse")
42
+ parser.add_argument("--fine-col", default="msp_ann_fine")
43
+ parser.add_argument("--batch-col", default=None, help="defaults to uns['msp']['batch_col']")
44
+ parser.add_argument("--species", default=None, help="defaults to uns['msp']['species']")
45
+ parser.add_argument("--min-cells", type=int, default=DEFAULT_MIN_CELLS,
46
+ help=f"smallest lineage that gets zoomed (default {DEFAULT_MIN_CELLS})")
47
+ parser.add_argument("--resolutions", type=float, nargs="+", default=[0.3, 1.0, 2.0])
48
+ parser.add_argument("--n-top-genes", type=int, default=2000)
49
+ parser.add_argument("--n-pcs", type=int, default=50)
50
+ parser.add_argument("--n-neighbors", type=int, default=15)
51
+ parser.add_argument("--harmony", action="append", default=[], metavar="KEY=VALUE",
52
+ help="harmonypy override for the per-lineage re-embedding, repeatable")
53
+ parser.add_argument("--language", default="English")
54
+ parser.add_argument("--model", default=None)
55
+ parser.add_argument("--effort", default=None, choices=["low", "medium", "high", "xhigh", "max"])
56
+ parser.add_argument("--max-turns", type=int, default=200)
57
+ parser.add_argument("--report-context", default=None, metavar="TEXT",
58
+ help='where this run sits, for report titles (e.g. "round 2 · fu2022-meniscus")')
59
+ parser.add_argument("--force", action="store_true")
60
+ args = parser.parse_args()
61
+
62
+
63
+ def _kv(items):
64
+ def conv(v):
65
+ for cast in (int, float):
66
+ try:
67
+ return cast(v)
68
+ except ValueError:
69
+ pass
70
+ return v
71
+ out = {}
72
+ for it in items:
73
+ if "=" not in it:
74
+ sys.exit(f"--harmony expects KEY=VALUE, got {it!r}")
75
+ k, v = it.split("=", 1)
76
+ out[k.strip()] = [conv(x) for x in v.split(",")] if "," in v else conv(v)
77
+ return out
78
+
79
+
80
+ out = os.path.abspath(args.outdir)
81
+ os.makedirs(out, exist_ok=True)
82
+ write_report_context(out, args.report_context)
83
+ ad = sc.read_h5ad(args.h5ad)
84
+ meta = ad.uns.get("msp", {})
85
+ batch_col = args.batch_col or meta.get("batch_col")
86
+ if not batch_col:
87
+ sys.exit("no --batch-col and uns['msp']['batch_col'] absent")
88
+ species = args.species or (meta.get("species") or None)
89
+ for c in (args.coarse_col, args.fine_col):
90
+ if c not in ad.obs:
91
+ sys.exit(f"obs[{c!r}] missing — input must be msp's annotated.h5ad (or pass --coarse-col/--fine-col)")
92
+ print(f"== {ad.n_obs} cells, batch={batch_col!r}, species={species}", flush=True)
93
+
94
+ plan = plan_lineages(ad, args.coarse_col, batch_col, out, min_cells=args.min_cells, species=species,
95
+ model=args.model, effort=args.effort)
96
+ label_to_lineage = {lab: ln["name"] for ln in plan["lineages"] for lab in ln["coarse_labels"]}
97
+ ad.obs["_zmip_lineage"] = ad.obs[args.coarse_col].astype(str).map(label_to_lineage).astype("category")
98
+ for ln in plan["lineages"]:
99
+ print(f"== lineage {ln['name']}: {ln['coarse_labels']} n={ln['n_cells']} zoom={ln['zoom']}", flush=True)
100
+
101
+ markers_p = os.path.join(out, "lineage_markers.csv")
102
+ if os.path.exists(markers_p) and not args.force:
103
+ mk = pd.read_csv(markers_p)
104
+ markers = {g: mk.loc[mk["lineage"] == g, "gene"].tolist() for g in mk["lineage"].unique()}
105
+ else:
106
+ print("== lineage-level markers (for foreign-lineage scores)", flush=True)
107
+ markers = lineage_markers(ad, "_zmip_lineage", out)
108
+
109
+ all_labels = set(label_to_lineage)
110
+ zoomed = [ln for ln in plan["lineages"] if ln["zoom"]]
111
+ if not zoomed:
112
+ print("== no lineage reaches min_cells — nothing to zoom; passing msp labels through", flush=True)
113
+ results = {}
114
+ harmony_kwargs = _kv(args.harmony)
115
+ keys_for_foreign = [f"msp_leiden_r{r}" for r in args.resolutions if r in (1.0, 2.0)]
116
+
117
+ for ln in zoomed:
118
+ name, labels = ln["name"], ln["coarse_labels"]
119
+ d = os.path.join(out, slug(name))
120
+ contract = [os.path.join(d, f) for f in ("annotation_proposal.json", "annotated.h5ad", "report.html")]
121
+ if all(os.path.exists(p) for p in contract) and not args.force:
122
+ print(f"== [{name}] already done — skipping (resume)", flush=True)
123
+ results[name] = {"dir": d, "removed": pd.read_csv(os.path.join(d, "annotation_removed.csv")),
124
+ "reassigned": pd.read_csv(os.path.join(d, "annotation_reassigned.csv"))}
125
+ continue
126
+ sub = ad[ad.obs[args.coarse_col].astype(str).isin(labels).values].copy()
127
+ for c in PREVIOUS_COLS:
128
+ src = {"msp_ann_coarse": args.coarse_col, "msp_ann_fine": args.fine_col}[c]
129
+ sub.obs[c + PREV_SUFFIX] = sub.obs[src].astype(str).astype("category")
130
+ del sub.obs["_zmip_lineage"]
131
+ print(f"== [{name}] re-embedding {sub.n_obs} cells", flush=True)
132
+ integrate_adata(sub, batch_col, d, species=species, resolutions=tuple(args.resolutions),
133
+ n_top_genes=args.n_top_genes, n_pcs=args.n_pcs, n_neighbors=args.n_neighbors,
134
+ harmony_kwargs=harmony_kwargs, inputs=[args.h5ad],
135
+ meta_extra={"zmip_lineage": name, "zmip_coarse_labels": list(labels)})
136
+ figdir = os.path.join(d, "figures")
137
+ print(f"== [{name}] foreign-lineage scores", flush=True)
138
+ foreign_cols = score_foreign(sub, markers, name, keys_for_foreign, d, figdir)
139
+ for c in PREVIOUS_COLS:
140
+ col = c + PREV_SUFFIX
141
+ n = sub.obs[col].nunique()
142
+ save_single_umap(sub, col, os.path.join(figdir, f"umap_{col}.png"), repel=True,
143
+ repel_fontsize=8 if n > 15 else 11, figsize=(9, 9) if n > 15 else None)
144
+ proposal, rm, ra = annotate_lineage(sub, d, name, labels, sorted(all_labels - set(labels)), foreign_cols,
145
+ species=species, language=args.language, model=args.model,
146
+ effort=args.effort, max_turns=args.max_turns)
147
+ results[name] = {"dir": d, "removed": rm, "reassigned": ra}
148
+ del sub
149
+
150
+ merge_back(ad, plan, results, out, coarse_col=args.coarse_col, fine_col=args.fine_col)
151
+ print(f"== report: {generate_report(out)}", flush=True)