opencode-skills-collection 4.1.7 → 4.1.9

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.

Potentially problematic release.


This version of opencode-skills-collection might be problematic. Click here for more details.

Files changed (32) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +7 -1
  2. package/bundled-skills/court/SKILL.md +141 -0
  3. package/bundled-skills/court/references/jurors.json +14 -0
  4. package/bundled-skills/court/scripts/court.py +348 -0
  5. package/bundled-skills/darkmoon-pentest/SKILL.md +123 -0
  6. package/bundled-skills/lognorm/SKILL.md +86 -0
  7. package/bundled-skills/lognorm/references/playbooks.md +76 -0
  8. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +6 -6
  9. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +1 -1
  10. package/bundled-skills/parallel-search-mcp/SKILL.md +12 -0
  11. package/bundled-skills/scarf-single-cell/SKILL.md +326 -0
  12. package/bundled-skills/scarf-single-cell/references/LICENSE.md +29 -0
  13. package/bundled-skills/scarf-single-cell/references/clustering-and-embedding.md +270 -0
  14. package/bundled-skills/scarf-single-cell/references/data-access.md +269 -0
  15. package/bundled-skills/scarf-single-cell/references/features-and-graphs.md +279 -0
  16. package/bundled-skills/scarf-single-cell/references/gene-blacklists.md +182 -0
  17. package/bundled-skills/scarf-single-cell/references/integration-and-comparisons.md +295 -0
  18. package/bundled-skills/scarf-single-cell/references/markers-and-annotation.md +280 -0
  19. package/bundled-skills/scarf-single-cell/references/performance-and-export.md +291 -0
  20. package/bundled-skills/scarf-single-cell/references/pipeline-runs-and-artifacts.md +273 -0
  21. package/bundled-skills/scarf-single-cell/references/plotting.md +260 -0
  22. package/bundled-skills/scarf-single-cell/references/quality-control.md +290 -0
  23. package/bundled-skills/session-handoff/SKILL.md +112 -0
  24. package/bundled-skills/shark/SKILL.md +134 -0
  25. package/bundled-skills/shark/references/investors.json +12 -0
  26. package/bundled-skills/shark/scripts/shark.py +302 -0
  27. package/bundled-skills/telegram/assets/boilerplate/nodejs/package-lock.json +6 -6
  28. package/bundled-skills/telegram/assets/boilerplate/nodejs/package.json +1 -1
  29. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/package-lock.json +6 -6
  30. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/package.json +1 -1
  31. package/package.json +1 -1
  32. package/skills_index.json +341 -0
@@ -0,0 +1,260 @@
1
+ # Plotting
2
+
3
+ Draw, inspect and save Scarf figures headlessly, from a completed pipeline run or from explicit
4
+ artifact refs. Docs: <https://scarf.readthedocs.io/en/latest/tutorials/plotting.html>,
5
+ <https://scarf.readthedocs.io/en/latest/reference/api/plotting.html>, <https://scarf.readthedocs.io/en/latest/reference/api/datastore.html>.
6
+
7
+ ## When to use
8
+
9
+ - Visual checks after QC, graph, clustering, doublet and marker steps.
10
+ - Figures for a report or handoff. Agents run headless: always save to files and close.
11
+
12
+ ## Key concepts
13
+
14
+ - `ds.plots.<name>(...)` is `scarf.plotting.<name>(ds, ...)` with the store bound. Accessor methods:
15
+ `embedding`, `embedding_raster`, `distribution`, `dotplot`, `matrixplot`, `composition`,
16
+ `marker_heatmap`, `cluster_connectivity`, `cluster_tree`, `modality_weights`,
17
+ `pseudotime_heatmap`, `mapping_score`, `mapping_evidence`, `mapping_confusion`,
18
+ `mapping_calibration`, `run_recipe`. Standalone only in `scarf.plotting`: `graph_qc` (sparse
19
+ graph), `elbow` and `highly_variable_features` (arrays), `qc` (DataFrame), plus
20
+ `compose_results`, `label_panels`, `theme_context`.
21
+ - Every plot returns a `PlotResult`: `figure`, `axes` (dict keyed by panel), `tables` (dict of
22
+ DataFrames with the plotted values), `legends`, `scales`, `provenance`, `owns_figure`, and
23
+ `save()`, `save_provenance()`, `show()`, `close()`. Plots never add metadata columns; on a
24
+ writable store `cluster_tree` caches `dendrogram` and `coalesced_tree` artifacts.
25
+ - `show=True` is the default. In a script it draws nothing on Agg and closes a Scarf-owned figure.
26
+ Pass `show=False`, save, then `close()`.
27
+ - Run mode, embedding and embedding_raster only: `run=run`, `layout="umap"` (default), and
28
+ `color_by` set to one string from `run.cells.columns`. No genes, live columns, lists, `CellField`,
29
+ `facet_by`, `subset_by`, `normalization` or `Highlight(by=...)`.
30
+ - Ref mode, everything else: `layout=run["umap"]`, `color_by=` an `ArtifactRef`, a live metadata
31
+ column, a gene name, `FeatureRef`, `CellField`, or a list of these (one panel each). Cells come
32
+ from the layout artifact's selection; live columns are aligned to it.
33
+ - Helpers in `scarf.plotting`: `CellField(key, kind="auto"|"categorical"|"continuous", label=)`,
34
+ `FeatureRef(value, assay=None, by="name"|"id"|"index", label=, reduction=)`,
35
+ `NormalizationSpec(source="assay"|"raw", transform="none"|"log1p")`,
36
+ `ColorScale(cmap, vmin, vmax, vcenter, quantiles, missing_color, scope, scale)`,
37
+ `CategoricalScale(order, palette, labels, missing_color, missing_label, palette_name)`,
38
+ `Highlight(by, groups, indices, color, dim_alpha, ...)`, `DensityOverlay`, `SizeScale`,
39
+ `StudyDesign`.
40
+ - Plots are display. `max_points` thins only the overlaid points in `distribution`; violins use
41
+ every cell and `provenance.notes` gains `"subsampled_display"`. Take numbers from
42
+ `result.tables` or artifacts, never from pixels or UMAP distances.
43
+
44
+ ## Recipes
45
+
46
+ `cell_type`, `sample_id` and `B cell` below are placeholders for your own annotation and design
47
+ columns. When author labels are held out, plot only labels you assigned.
48
+
49
+ ### Save headlessly
50
+
51
+ Run scripts as `MPLBACKEND=Agg python script.py`, or call `matplotlib.use("Agg")` before
52
+ importing pyplot. `save` infers the format (png, pdf, svg, tif) from the suffix, keeps the exact
53
+ inch size, and uses a white background. The exact size can clip a legend outside the axes
54
+ (`legend_loc="right"`, many categories): save those with `exact_size=False`, a tight crop.
55
+
56
+ ```python
57
+ import matplotlib.pyplot as plt
58
+ import scarf
59
+ import scarf.plotting as splt
60
+
61
+ scarf.configure_output(level="WARNING", progress=False)
62
+ ds = scarf.DataStore("analysis.zarr", nthreads=4, min_features_per_cell=-1)
63
+ run = ds.pipeline.open(label="baseline")
64
+
65
+ result = ds.plots.embedding(run=run, layout="umap", color_by="clusters", show=False)
66
+ result.save("figures/umap_clusters.png", dpi=150, provenance_sidecar=True) # + .png.json
67
+ result.figure.savefig("figures/umap_clusters_tight.png", dpi=150, bbox_inches="tight")
68
+ result.close()
69
+ assert plt.get_fignums() == [] # nothing left open
70
+ ```
71
+
72
+ ### Plot frozen run fields
73
+
74
+ ```python
75
+ for field in ("clusters", "leiden_1.0", "doublet_score", "cell_cycle_phase"):
76
+ r = ds.plots.embedding(run=run, color_by=field, show=False)
77
+ r.save(f"figures/run_{field}.png", dpi=120)
78
+ r.close()
79
+ r = ds.plots.embedding_raster(run=run, color_by="doublet_score", show=False)
80
+ r.close()
81
+ ```
82
+
83
+ ### Color by cluster artifacts, metadata and genes
84
+
85
+ Gene panels read counts and normalize on the fly; several genes in one call share one matrix
86
+ read (seconds on remote counts).
87
+
88
+ ```python
89
+ umap = run["umap"]
90
+ r = ds.plots.embedding(
91
+ layout=umap,
92
+ color_by=[run["clusters"], "CD3D", "MS4A1", "LYZ"],
93
+ normalization=splt.NormalizationSpec(transform="log1p"),
94
+ color_scale=splt.ColorScale(cmap="viridis", quantiles=(0.0, 0.99)),
95
+ sort_values=True,
96
+ show=False,
97
+ )
98
+ r.axes["cluster_labels"].set_title("Leiden (selected)") # ref panels are keyed by kind
99
+ r.save("figures/umap_genes.png", dpi=150)
100
+ r.close()
101
+
102
+ r = ds.plots.embedding(
103
+ layout=umap,
104
+ color_by=splt.CellField("cell_type", kind="categorical", label="Cell type"),
105
+ categorical_scale=splt.CategoricalScale(palette_name="colorblind"), # or "default" only
106
+ legend_loc="right",
107
+ show=False,
108
+ )
109
+ r.save("figures/umap_cell_type.png", dpi=150, exact_size=False) # keeps the outside legend
110
+ r.close()
111
+ ds.plots.embedding(layout=umap, color_by=run["leiden_1.0"], show=False).close()
112
+ ds.plots.embedding(
113
+ layout=umap,
114
+ color_by=None,
115
+ default_color="#bdbdbd",
116
+ highlight=splt.Highlight(by="cell_type", groups=("B cell",)),
117
+ show=False,
118
+ ).close()
119
+ ds.plots.embedding(
120
+ layout=umap, color_by=splt.FeatureRef("NKG7", label="NKG7"), show=False
121
+ ).close()
122
+ ```
123
+
124
+ ### Distributions
125
+
126
+ `grouping` must be an `ArtifactRef` or a `CellField`; a plain string raises `TypeError`. `keys`
127
+ are metadata columns, genes, or a `cell_cycle` artifact: every other `ArtifactRef` (doublets,
128
+ membership strength, enrichment) raises `ValueError`. Insert a frozen field as a live column first.
129
+
130
+ ```python
131
+ r = ds.plots.distribution(
132
+ ["RNA_nCounts", "RNA_percentMito"],
133
+ grouping=run["clusters"],
134
+ kind="violin",
135
+ max_points=2000,
136
+ show=False,
137
+ )
138
+ r.tables["RNA_nCounts"].head() # value, group, display_value; one row per analysed cell
139
+ r.provenance.notes # ('distribution', 'violin', 'subsampled_display')
140
+ r.close()
141
+ ds.plots.distribution(
142
+ "RNA_nCounts",
143
+ grouping=splt.CellField("cell_type", kind="categorical"),
144
+ cell_selection=run["analysis_cell_selection"],
145
+ max_points=0,
146
+ show=False,
147
+ ).close()
148
+ ds.plots.distribution(run["cell_cycle"], grouping=run["clusters"], show=False).close()
149
+ ds.cells.insert("baseline_doublet_score", run.cells.fetch_all("doublet_score"), overwrite=True)
150
+ ds.plots.distribution("baseline_doublet_score", grouping=run["clusters"], show=False).close()
151
+ ```
152
+
153
+ ### Dot plots, matrix plots and marker heatmaps
154
+
155
+ Here `groups=` is the cluster `ArtifactRef`; `group_by=` names a live column instead and groups
156
+ the cells of `cell_key` (default `"I"`, not the run's cells). The aggregate table's group column is
157
+ `groups` for `groups=`, and the column's own name for `group_by=` (one column per name in a tuple).
158
+
159
+ ```python
160
+ panel = {"T": ["CD3D", "IL7R"], "B": ["MS4A1", "CD79A"], "NK": ["NKG7", "GNLY"]}
161
+ r = ds.plots.dotplot(features=panel, groups=run["clusters"], standardize="feature", show=False)
162
+ r.tables["aggregate"].head() # groups, feature, feature_group, mean, fraction, n_cells, variance
163
+ r.save("figures/dotplot.png", dpi=150)
164
+ r.close()
165
+ r = ds.plots.dotplot(features=panel, group_by="cell_type", show=False)
166
+ agg = r.tables["aggregate"] # cell_type, feature, feature_group, mean, fraction, n_cells, variance
167
+ fraction = agg.pivot(index="cell_type", columns="feature", values="fraction") # not "groups"
168
+ r.close()
169
+ ds.plots.matrixplot(
170
+ features=["CD3D", "MS4A1", "LYZ"], groups=run["clusters"], cluster_groups=True, show=False
171
+ ).close()
172
+ r = ds.plots.marker_heatmap(marker=run["markers"], topn=3, show=False)
173
+ r.tables["markers"].head() # group, rank, feature_index, score, feature
174
+ r.close()
175
+ ```
176
+
177
+ ### Graph and cluster structure
178
+
179
+ ```python
180
+ graph = run["connectivity_map"]
181
+ r = ds.plots.cluster_connectivity(
182
+ graph=graph, groups=run["clusters"], layout=run["umap"], show_cells=True, show=False
183
+ )
184
+ r.tables["edges"] # source, target, rawWeight, normalizedWeight
185
+ r.close()
186
+ ds.plots.cluster_tree(graph=graph, clusters=run["paris"], show=False).close()
187
+ splt.graph_qc(ds.load_graph(graph=graph), show=False).close()
188
+ ds.plots.composition(categories=run["clusters"], show=False).close() # one stacked bar
189
+ r = ds.plots.composition(category_by="cell_type", sample_by="sample_id", kind="stacked", show=False)
190
+ r.tables["per_sample"].head() # one stacked bar per sample
191
+ r.close()
192
+ ```
193
+
194
+ `kind="per_sample"` with a `StudyDesign` draws a strip of per-sample proportions per category
195
+ instead; see integration-and-comparisons.md.
196
+
197
+ ### Compose panels in a figure you own
198
+
199
+ With `target=` the caller owns the figure: child `close()` does nothing, so close the figure. A
200
+ ref-mode embedding with many long category labels and `legend_loc="right"` shrinks its axes to a
201
+ thumbnail; draw it on a wide figure you own instead.
202
+
203
+ ```python
204
+ fig, axes = plt.subplots(1, 2, figsize=(9, 4), layout="constrained")
205
+ ds.plots.embedding(run=run, color_by="clusters", target=axes[0], show=False)
206
+ ds.plots.embedding(run=run, color_by="doublet_score", target=axes[1], show=False)
207
+ fig.savefig("figures/panels.png", dpi=150, bbox_inches="tight")
208
+ plt.close(fig)
209
+
210
+ fig, ax = plt.subplots(figsize=(11, 6), layout="constrained") # room for long labels
211
+ ds.plots.embedding(layout=run["umap"], color_by=splt.CellField("cell_type"), legend_loc="right",
212
+ target=ax, show=False)
213
+ fig.savefig("figures/umap_cell_type_wide.png", dpi=150, bbox_inches="tight")
214
+ plt.close(fig)
215
+ ```
216
+
217
+ ## Parameters that matter
218
+
219
+ | Parameter | Default | Change when |
220
+ | --- | --- | --- |
221
+ | `show` | `True` | Always `False` in scripts; then `save` and `close`. |
222
+ | `layout` / `layout_key` | none; exactly one required (run mode: `"umap"`) | `layout_key` only for coordinates you inserted as metadata columns. |
223
+ | `normalization` | `NormalizationSpec()` (assay normalization, no transform) | `transform="log1p"` for gene panels. |
224
+ | `color_scale` | auto per feature | `quantiles=(0.0, 0.99)` to stop outliers washing out a gene. |
225
+ | `categorical_scale` | default palette | Fix `order` or `palette` (dict) to keep colors identical; `palette_name` is only `"default"` or `"colorblind"`. |
226
+ | `sort_values` | `False` | `True` draws high values last for expression panels. |
227
+ | `legend_loc` | `"auto"` | `"right"`, `"on_data"` or `"none"` to force placement. |
228
+ | `max_points` (distribution) | `10000` | `0` for no points; `None` gives no points for stacked violins. |
229
+ | `rasterize_threshold` | `50000` | Lower for smaller vector files. |
230
+ | `save(dpi=, exact_size=)` | figure dpi (300 for TIFF), `True` | `exact_size=False` for a tight crop and for legends outside the axes. |
231
+
232
+ ## Check before moving on
233
+
234
+ - Open one or two saved PNGs and confirm panels, legends and colorbars rendered.
235
+ - Legend categories equal the cluster count; `tables` row counts equal analysed cells.
236
+ - `plt.get_fignums()` is empty after a batch of figures.
237
+ - `result.provenance.n_cells` and `notes` match the intended selection and subsampling.
238
+
239
+ ## Pitfalls
240
+
241
+ - Run mode rejects anything but one frozen field: a gene or live column raises `KeyError`, a list
242
+ or `CellField` raises `TypeError`, `facet_by`/`subset_by`/`normalization` raise `ValueError`.
243
+ Switch to `layout=run["umap"]`.
244
+ - `layout="umap"` without `run=` raises `TypeError`. The pipeline writes no `RNA_UMAP*` columns,
245
+ so `layout_key="RNA_UMAP"` fails on pipeline-only stores.
246
+ - `groups` filters categories in `embedding` and `distribution` but is the cluster ref in
247
+ `dotplot`, `matrixplot` and `cluster_connectivity`. Pivoting a `group_by=` aggregate on
248
+ `"groups"` raises `KeyError: 'groups'`.
249
+ - `CategoricalScale(palette_name="tab20")` raises `ValueError`; pass `palette={label: color}`.
250
+ - The same clusters get different colors in run mode and ref mode; pass a `CategoricalScale`
251
+ when figures must match.
252
+ - A run opened from another `DataStore` object raises `ValueError` in plots.
253
+ - Unclosed `show=False` figures accumulate in long loops.
254
+
255
+ ## See also
256
+
257
+ - pipeline-runs-and-artifacts.md: which run keys and frozen fields exist.
258
+ - quality-control.md, features-and-graphs.md, clustering-and-embedding.md,
259
+ markers-and-annotation.md: the diagnostic plot for each step.
260
+ - integration-and-comparisons.md: `modality_weights`, mapping plots, `StudyDesign` composition.
@@ -0,0 +1,290 @@
1
+ # Quality control
2
+
3
+ Check what the count matrix holds, inspect per-cell QC metrics, build immutable filtered cell
4
+ selections, audit removals without author labels, and score doublets. Docs:
5
+ <https://scarf.readthedocs.io/en/latest/tutorials/quality_control.html>, <https://scarf.readthedocs.io/en/latest/reference/api/datastore.html>,
6
+ <https://scarf.readthedocs.io/en/latest/reference/api/pipeline.html>.
7
+
8
+ ## When to use
9
+
10
+ - Before the first graph on a new store, or whenever the analysed cell population changes.
11
+ - When the store comes from a public atlas, CELLxGENE or a Seurat submission: counts may be
12
+ corrected, genes removed, or cells already filtered.
13
+ - When a run kept fewer cells than expected, and after a first clustering (doublets, QC clusters).
14
+
15
+ ## Key concepts
16
+
17
+ - QC columns are written once, at the first writable open: `RNA_nCounts`, `RNA_nFeatures`,
18
+ `RNA_percentMito` (`^MT-`, any case) and `RNA_percentRibo` (`^RPS|^RPL|^MRPS|^MRPL`) on feature
19
+ `names`. For any other gene set use `run_feature_percentage` (an artifact, not a column).
20
+ - Filters never edit `I` (the live cell key): each returns a new immutable `ArtifactRef`
21
+ (`kind="cell_selection"`). Omit `cell_selection=` to snapshot `I`; pass a selection to compose.
22
+ - `auto_filter_cells` defaults to pooled MAD (`n_mads=3`, scaled MAD = 1.4826 * MAD), chosen by
23
+ column-name suffix: `nCounts`/`nFeatures` are filtered on log1p values with two-sided bounds;
24
+ `percentMito`/`percentRibo` on raw values with an upper bound only; other names raw and
25
+ two-sided. A metric with zero MAD is skipped with a warning. Bounds are exclusive.
26
+ - `parameters["resolved_bounds"]` are reported back on the raw scale: `low` and `high` are counts,
27
+ genes or percent; never apply `expm1`. Example (10x 5K PBMC docs dataset): `RNA_nCounts` low
28
+ 3377.1 and high 18389.9 mean 3,377 to 18,390 counts.
29
+ - Every bound assumes raw UMI counts from unfiltered cell calls. Corrected (for example
30
+ SCTransform) or pre-filtered matrices change what each bound means: check the matrix first.
31
+ - Pooled MAD erases real populations: platelets, erythrocytes and neutrophils fall under lower
32
+ bounds (floors near 200 to 650 genes); plasma, cycling and progenitor cells cross upper bounds;
33
+ myeloid cells carry more mito, so a pooled mito bound trims them first. Example (10x 5K PBMC
34
+ docs dataset): even on cells filtered once before, pooled 3-MAD kept 6 of 15 platelet-like cells.
35
+
36
+ ## Recipes
37
+
38
+ ### Check the matrix first
39
+
40
+ `scripts/inspect_store.py STORE.zarr` (in the maintained copy of this skill in the Scarf
41
+ repository; this catalogue copy does not include it) runs these checks with hints.
42
+ By hand, read-only; the count read is the first 300 cells only (one small read on a mount):
43
+
44
+ ```python
45
+ import numpy as np
46
+ import pandas as pd
47
+ import scarf
48
+
49
+ ds = scarf.DataStore("analysis.zarr", zarr_mode="r")
50
+ sample = ds.RNA.rawData[:300].compute()
51
+ nonzero = sample[sample != 0]
52
+ print("integer-like", bool(np.all(nonzero == np.round(nonzero))), "negative", bool((sample < 0).any()))
53
+ author = [c for c in ds.cells.columns if c.startswith(("nCount_", "nFeature_"))] # adapt names
54
+ qc = ds.cells.to_pandas_dataframe(["RNA_nCounts", "RNA_nFeatures"] + author, key="I")
55
+ print(qc.describe(percentiles=[0.01, 0.05, 0.5, 0.95, 0.99]).round(1))
56
+ print("share within 5% of min:", {c: round(float((qc[c] <= qc[c].min() * 1.05).mean()), 4) for c in qc})
57
+ spread = {c: np.subtract(*np.percentile(np.log1p(qc[c]), [95, 5])) for c in ("RNA_nCounts", "RNA_nFeatures")}
58
+ print("spread ratio nFeatures/nCounts:", round(spread["RNA_nFeatures"] / spread["RNA_nCounts"], 2))
59
+ if author:
60
+ print(qc.corr(method="spearman").loc[["RNA_nCounts", "RNA_nFeatures"], author].round(3))
61
+ ```
62
+
63
+ | Signal | Reading |
64
+ | --- | --- |
65
+ | Nonzero values not integers, or negatives | Normalized, scaled or log values, not counts. Re-import a raw layer (`data-access.md`). |
66
+ | Spread ratio above 1: genes vary more than totals | Depth-equalized counts, for example SCTransform. Raw UMIs are usually below 1. |
67
+ | Store `nFeatures` matches author `nFeature_SCT` (Spearman about 1) | SCT-corrected counts. Integer values do not prove raw UMIs. |
68
+ | Store totals below author raw `nCount_RNA`, Spearman well below 1 | Corrected counts, removed genes (for example rRNA) or another layer. |
69
+ | 0.5% or more of cells within 5% of the minimum; a minimum or maximum just inside a round number (1,000 counts, 200 genes, 20% mito) | A hard cutoff before import: cells were already filtered. |
70
+ | Fewer active than total cells | An earlier filter edited `I`; active ranges are truncated. |
71
+
72
+ Example (10x 5K PBMC docs dataset): integer `uint32` counts and a spread ratio of 0.78 (raw UMIs),
73
+ but `I` keeps 3,948 of 5,025 cells, and active cells sit just inside 1,000 to 15,000 counts, 500 to
74
+ 4,000 genes and 15% mito: an earlier filter edited `I`.
75
+
76
+ With corrected or pre-filtered counts, QC bounds lose their meaning: depth bounds on SCT totals
77
+ measure the correction, and refiltering filtered cells mostly trims real populations. Skip
78
+ `nCounts` bounds; use flag-only QC (a status column, nothing removed) or a gentle, audited filter
79
+ (per-sample 5-MAD on `RNA_nFeatures` and `RNA_percentMito`). The report states what the matrix
80
+ holds, any visible prior bounds, and why QC was gentle. Count models need raw UMIs.
81
+
82
+ ### Distributions, default filter and candidate policies
83
+
84
+ Distributions are metadata-only; group them by sample or capture, never by held-out labels. Then
85
+ build several policies, including gentle ones that keep high-RNA and low-complexity populations.
86
+
87
+ ```python
88
+ from scarf.plotting import CellField
89
+
90
+ ds = scarf.DataStore("analysis.zarr", min_features_per_cell=-1) # writable reopen
91
+ qc_cols = [c for c in ("RNA_nCounts", "RNA_nFeatures", "RNA_percentMito", "RNA_percentRibo")
92
+ if c in ds.cells.columns]
93
+ cells = ds.snapshot_cell_selection("I") # ArtifactRef(kind="cell_selection")
94
+ ds.plots.distribution(keys=qc_cols, cell_selection=cells, kind="violin", max_points=2000)
95
+ ds.plots.distribution(keys=["RNA_nCounts", "RNA_nFeatures"], grouping=CellField("sample_id"),
96
+ cell_selection=cells, max_points=0) # "sample_id" is your column name
97
+
98
+ def mask(ref):
99
+ return np.asarray(ds.load_artifact(ref)["values"][:], dtype=bool)
100
+
101
+ auto = ds.auto_filter_cells(cell_selection=cells) # pooled MAD over the default columns
102
+ params = ds.inspect_artifact(auto).parameters
103
+ print(pd.DataFrame(params["resolved_bounds"]["all"]).T[["low", "high", "skip_reason"]]) # raw scale
104
+ mito = ds.auto_filter_cells(["RNA_percentMito"], cell_selection=cells) # upper MAD bound only
105
+ floors = ds.filter_cells(["RNA_nFeatures", "RNA_nCounts"], lows=[200, 500], highs=[None, None],
106
+ cell_selection=mito) # composes with the mito selection
107
+ loose = ds.auto_filter_cells(cell_selection=cells, n_mads=5)
108
+ manual = ds.filter_cells(["RNA_nFeatures", "RNA_percentMito"], lows=[200, None], highs=[None, 20],
109
+ cell_selection=cells)
110
+ candidates = {"mad3": auto, "mad5": loose, "mito+floors": floors, "manual": manual}
111
+ print({name: int(mask(ref).sum()) for name, ref in candidates.items()})
112
+ ```
113
+
114
+ ### Audit what was removed, without labels
115
+
116
+ Do not read author labels. Run an unfiltered scaffold clustering, name its clusters by markers,
117
+ and report each candidate's retention per scaffold cluster and per marker-positive group. Costs
118
+ one pipeline run plus one small count pass (the panel export).
119
+
120
+ ```python
121
+ scaffold = ds.pipeline.run(label="qc_scaffold", filtering=False, cell_cycle=False, paris=False)
122
+ in_run = mask(scaffold["analysis_cell_selection"])
123
+ clusters = np.asarray(scaffold.cells.fetch("clusters"))
124
+ kept = pd.DataFrame({name: mask(ref)[in_run] for name, ref in candidates.items()})
125
+ top = ds.get_markers(marker=scaffold["markers"]).groupby("group_id", sort=False).head(5)
126
+ markers = top.groupby("group_id")["feature_name"].agg(", ".join)
127
+ by_cluster = kept.groupby(clusters).mean().round(2).assign(n=kept.groupby(clusters).size())
128
+ by_cluster["markers"] = markers.reindex(by_cluster.index.astype(str)).to_numpy()
129
+ print(by_cluster.to_string())
130
+
131
+ panel = ["PPBP", "PF4", "JCHAIN", "MZB1", "HBB", "CSF3R", "FCGR3B", "MKI67", "TOP2A", "CD34",
132
+ "CD3E", "MS4A1", "LYZ", "CD14", "MALAT1"] # human blood; adapt genes and rules to tissue
133
+ names = set(ds.RNA.feats.fetch_all("names")) # mouse symbols differ (Ppbp, Hbb-bs); map Ensembl IDs
134
+ print("panel genes not in this store:", [g for g in panel if g not in names])
135
+ panel = [g for g in panel if g in names] # an absent name raises KeyError in to_anndata
136
+ ad = ds.to_anndata(feature_names=panel) # raw counts of active cells, rows named by cell ids
137
+ counts = pd.DataFrame(ad.X.toarray(), index=ad.obs_names, columns=panel)
138
+ counts = counts.reindex(scaffold.cells.fetch("ids"))
139
+ rules = {"platelet PPBP+PF4+": {"PPBP": 1, "PF4": 1}, # minimum raw count of every gene
140
+ "plasma MZB1>=3 JCHAIN>=3": {"MZB1": 3, "JCHAIN": 3},
141
+ "erythroid HBB>=10": {"HBB": 10},
142
+ "neutrophil CSF3R+FCGR3B+": {"CSF3R": 1, "FCGR3B": 1},
143
+ "cycling MKI67+TOP2A+": {"MKI67": 1, "TOP2A": 1},
144
+ "progenitor CD34+": {"CD34": 1}}
145
+ groups = {g: (counts[list(r)] >= pd.Series(r)).all(axis=1)
146
+ for g, r in rules.items() if set(r) <= set(panel)} # a group missing a gene is skipped
147
+ by_group = pd.DataFrame({g: kept[m.to_numpy()].mean() for g, m in groups.items()}).T
148
+ print(by_group.assign(n=[int(m.sum()) for m in groups.values()]).round(2))
149
+
150
+ debris = pd.DataFrame({"nFeatures": ds.cells.fetch_all("RNA_nFeatures")[in_run],
151
+ "percentMito": ds.cells.fetch_all("RNA_percentMito")[in_run],
152
+ "marker_positive": pd.DataFrame(groups, index=counts.index)
153
+ .any(axis=1).to_numpy()})
154
+ if "MALAT1" in panel:
155
+ debris["MALAT1"] = counts["MALAT1"].to_numpy() > 0
156
+ for name in candidates: # rows: False = removed, True = kept
157
+ print(name, debris.groupby(kept[name].to_numpy()).agg(["median", "mean"]).round(2), sep="\n")
158
+ ```
159
+
160
+ - A scaffold cluster or marker group losing more than about a third of its cells is biology being
161
+ removed: pick a gentler policy or exempt that population explicitly.
162
+ - Debris shows low `nFeatures`, no `MALAT1` (a nuclear lncRNA), high mito and no lineage marker.
163
+ Platelets and erythrocytes also lack `MALAT1` (no nucleus) but carry `PPBP`/`PF4` or `HBB`.
164
+ - Marker gates are starting points on raw counts. Ambient RNA (`JCHAIN`, `HBB`, `PPBP`) is
165
+ detected at low levels in many cells, hence two markers or a higher count per gate.
166
+
167
+ Optional post-hoc audit, only after your own labels are frozen or when labels are not held out:
168
+
169
+ ```python
170
+ labels = ds.cells.fetch_all("author_cell_type")[in_run] # post hoc only; your column name
171
+ print(kept.groupby(labels).mean().round(2))
172
+ ```
173
+
174
+ ### Per-sample (capture-aware) MAD
175
+
176
+ Bounds are estimated within each sample of a real sample or capture column. Groups below
177
+ `min_cells_per_sample` are kept unfiltered with a warning. Missing labels among active cells raise.
178
+ For hashtag data pass `sample_artifact=NamedCellArtifact("hto", hto_ref)` (`kind="hto_identity"`).
179
+
180
+ ```python
181
+ per_sample = ds.auto_filter_cells(cell_selection=cells, sample_column="sample_id", n_mads=3.0,
182
+ min_cells_per_sample=20) # "sample_id" is your column name
183
+ bounds = ds.inspect_artifact(per_sample).parameters["resolved_bounds"] # {sample: {metric: {...}}}
184
+ for side in ("low", "high"): # raw scale; a low near zero means no floor in that sample
185
+ print(side, pd.DataFrame({s: {m: b[side] for m, b in d.items()} for s, d in bounds.items()}).T)
186
+ ```
187
+
188
+ ### Pipeline filtering and feeding a custom selection
189
+
190
+ `filtering=True` (default) is pooled MAD on the four default columns, stored as its own artifact
191
+ (`run["analysis_cell_selection"]`; compare masks, not refs). Other accepted values:
192
+ `False`; `{"method": "mad", "n_mads": 5, "sample_column": "sample_id", "min_cells_per_sample": 20}`;
193
+ `{"method": "gaussian", "min_p": 0.01, "max_p": 0.99}`; and
194
+ `{"method": "manual", "attrs": [...], "lows": [...], "highs": [...], "keep_bounds": False}`.
195
+ Unknown keys raise early. To run on a granular selection, insert it as a boolean column and
196
+ disable pipeline filtering:
197
+
198
+ ```python
199
+ run_manual = ds.pipeline.run(label="qc_manual", filtering={
200
+ "method": "manual", "attrs": ["RNA_nFeatures", "RNA_percentMito"], "lows": [200, None],
201
+ "highs": [None, 20]}, snapshot_columns=("RNA_nCounts", "RNA_percentMito"))
202
+ ds.cells.insert("qc_pass", mask(floors), overwrite=True)
203
+ run_custom = ds.pipeline.run(label="qc_custom", cell_key="qc_pass", filtering=False)
204
+ assert np.array_equal(mask(run_custom["analysis_cell_selection"]), mask(floors))
205
+ ```
206
+
207
+ ### Custom feature-percentage metric
208
+
209
+ ```python
210
+ from scarf.metadata.selection import NamedCellArtifact
211
+
212
+ names = ds.RNA.feats.fetch_all("names").astype(str)
213
+ hb = ds.set_feature_selection(from_assay="RNA", mask=np.isin(names, ["HBA1", "HBA2", "HBB"]))
214
+ pct_hb = ds.run_feature_percentage(cells, hb) # kind="quality_metric"; one count pass; no column
215
+ no_rbc = ds.select_cells(pct_hb, high=5.0, cell_selection=floors) # fixed bound, composed
216
+ mad_hb = ds.auto_filter_cells(["RNA_percentMito"], cell_selection=cells,
217
+ artifact_metrics=[NamedCellArtifact("percentHb", pct_hb)])
218
+ # A mostly-zero metric (percentHb in PBMC) has zero MAD and is skipped: use a fixed bound.
219
+ ```
220
+
221
+ ### Doublet scores
222
+
223
+ The pipeline scores doublets by default (it needs a graph and a clustering). High-RNA types such as
224
+ plasma cells score high as a whole, so never threshold globally: rank cells within their cluster
225
+ and require co-expressed markers of two lineages.
226
+
227
+ ```python
228
+ scores = pd.Series(np.asarray(scaffold.cells.fetch("doublet_score")))
229
+ print(scores.groupby(clusters).median().round(2)) # compare clusters within a lineage
230
+ spread = scores.groupby(clusters).transform(lambda s: 1.4826 * (s - s.median()).abs().median())
231
+ relative = (scores - scores.groupby(clusters).transform("median")) / (spread + 1e-6)
232
+ two_lineage = (((counts.CD3E > 0) & (counts.LYZ >= 5)) | ((counts.MS4A1 > 0) & (counts.LYZ >= 5))
233
+ | ((counts.MZB1 >= 3) & (counts.CD14 > 0))).to_numpy()
234
+ suspect = (relative.to_numpy() > 3) & two_lineage
235
+ singlet = in_run.copy()
236
+ singlet[np.flatnonzero(in_run)[suspect]] = False
237
+ ds.cells.insert("qc_singlet", singlet, overwrite=True) # then cell_key="qc_singlet"
238
+ doublets = ds.run_doublet_detection(scaffold["clusters"], scaffold["connectivity_map"]) # explicit
239
+ ```
240
+
241
+ ## Parameters that matter
242
+
243
+ | Parameter | Default | Change when |
244
+ | --- | --- | --- |
245
+ | `auto_filter_cells(n_mads=)` | `3.0` | Raise (for example 5) when real populations sit in the tails. |
246
+ | `auto_filter_cells(attrs=)` | the four `RNA_*` QC columns | Drop count bounds on corrected counts or when they remove high-RNA types. |
247
+ | `sample_column=` / `sample_artifact=` | `None` (pooled) | Several samples, captures or depths. MAD only. |
248
+ | `min_cells_per_sample=` | `20` | Small samples; groups below it are kept unfiltered. |
249
+ | `method=` (`min_p`, `max_p`) | `"mad"` | `"gaussian"` only to reproduce the old policy; `min_p`/`max_p` with MAD raise. |
250
+ | `pipeline.run(filtering=False, cell_cycle=False, paris=False)` | filtering on | Scaffold run for the label-free audit. |
251
+ | `run_doublet_detection(random_seed=)` | `4444` | Keep fixed; change only to test score stability. |
252
+
253
+ ## Check before moving on
254
+
255
+ - The report states what the matrix holds: raw UMIs, corrected counts, removed genes, pre-filtered.
256
+ - Retention per sample, per scaffold cluster and per marker-positive group. A population losing
257
+ more than about a third of its cells needs a reason, not a default.
258
+ - `resolved_bounds` (raw scale) sit in distribution tails, not inside a mode. Per sample, a low near
259
+ zero means no floor there. Negative lows (Gaussian on skewed counts) mean no lower filtering.
260
+ - Removed cells look like debris, not platelets, plasma, erythroid, neutrophil, cycling or CD34+.
261
+ - After clustering, QC metrics by cluster (`ds.plots.distribution(keys=qc_cols,
262
+ grouping=run["clusters"])`): a cluster defined only by low counts or high mito is a QC suspect.
263
+ - Doublet suspects are high within their own cluster and co-express markers of two lineages.
264
+
265
+ ## Pitfalls
266
+
267
+ - Accepting pipeline defaults without the audit above.
268
+ - Applying `expm1` to `resolved_bounds` (already raw scale), or taking integer values as raw UMIs
269
+ (SCT-corrected counts are integers too). On corrected or pre-filtered counts, filter gently.
270
+ - Pooling samples with different depths under one bound; use `sample_column`.
271
+ - Per-sample MAD on a bimodal sample, for example one dominated by erythrocytes: the wide MAD can
272
+ put the lower bound near zero (no floor), or the dominant mode pushes the other population out.
273
+ Read per-sample lows and marker-group retention in that sample.
274
+ - Upper `nCounts`/`nFeatures` bounds or a global doublet-score threshold as a doublet filter: both
275
+ remove plasma and other large cells. Compare scores within a lineage and check markers.
276
+ - Calling CD34+ progenitors multiplets. Progenitors show a coherent program (`CD34`, `PRSS57`,
277
+ `SPINK2`, `SOX4`, often `GATA2`, `CYTL1`) and no second lineage, even with high counts and
278
+ doublet scores. Multiplets co-express full marker sets of two lineages (`CD3E` with `LYZ` or
279
+ `CD14`; `MZB1` with `CD14`), exceed both parents in counts and sit between their clusters.
280
+ - Expecting `I` to shrink after filtering. Pass the returned ref, or use a column with `cell_key=`.
281
+ - `run.cells` exposes `names`, run results and `snapshot_columns`, not filter columns. QC columns
282
+ need `snapshot_columns=(...)` or `ds.cells.fetch_all(col)[mask]`.
283
+
284
+ ## See also
285
+
286
+ - `data-access.md`: raw layers on import; a read-only first look at a store.
287
+ - `features-and-graphs.md`: feature selection and graph construction on the filtered selection.
288
+ - `pipeline-runs-and-artifacts.md`: run records, lineage and artifact inspection.
289
+ - `plotting.md`: distribution, dotplot and embedding options.
290
+ - `integration-and-comparisons.md`: per-sample reporting for condition comparisons.
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: session-handoff
3
+ description: "Use when context approaches capacity, before /clear or /compact, when switching tasks, or when ending a coding session: produces a structured handoff artifact for the next session."
4
+ category: productivity
5
+ risk: safe
6
+ source: community
7
+ source_repo: alapha888/session-handoff-kit
8
+ source_type: community
9
+ date_added: "2026-10-05"
10
+ author: alapha888
11
+ tags: [context-management, session-handoff, coding-agents, workflow]
12
+ tools: [claude, cursor, codex]
13
+ license: "MIT"
14
+ license_source: "https://github.com/alapha888/session-handoff-kit/blob/main/LICENSE"
15
+ ---
16
+
17
+ # Session Handoff Skill
18
+
19
+ ## Purpose
20
+ Large context windows degrade in instruction adherence, reasoning precision, and tool accuracy as token counts accumulate. Long sessions fill with superseded plans, aborted scratchpads, and repetitive error traces. Resetting to a fresh session restores performance, but discarding context blindly introduces state loss, hallucinated progress, and repeated mistakes.
21
+
22
+ The `session-handoff` skill standardizes how an agent snapshots active state. It translates ephemeral working memory into an actionable, verifiable bridge document for the next session.
23
+
24
+ ## When to Use This Skill
25
+
26
+ - Context usage approaches capacity (roughly >60%) and a reset is coming.
27
+ - Before running `/clear` or `/compact` in Claude Code, Cursor, or Codex.
28
+ - When switching tasks mid-session, or ending a coding session that another session (or agent) will continue.
29
+
30
+ ---
31
+
32
+ ## Workflow: Ending a Session (6 Steps)
33
+
34
+ Run this sequence when an agent session reaches operational limits or when halting work:
35
+
36
+ 1. **Audit Git and Workspace State**
37
+ Run `git status -s` and `git diff --stat` to establish grounded reality. Never rely on chat memory for what was modified. Identify modified, untracked, and staged files.
38
+
39
+ 2. **Capture Goal State vs. Implemented Reality**
40
+ Define the original objective in one sentence. Document precisely what has been verified as working (with passing test commands or runtime checks) versus what remains partial or unverified.
41
+
42
+ 3. **Record Decisions Made and Discarded Paths**
43
+ List architectural and design choices committed during the session, along with the concrete rationale. Explicitly record negative decisions (approaches tried and discarded) to prevent the next agent from re-attempting failed solutions.
44
+
45
+ 4. **Catalog Files Touched and Relevant Symbols**
46
+ List key file paths, functions, classes, and schema definitions modified or referenced. Include line references where relevant.
47
+
48
+ 5. **Isolate Open Loops with Owners and Checks**
49
+ List all unaddressed edge cases, failing tests, deferred tasks, and environment constraints. Every open loop must specify who resolves it (Human or Agent) and how completion is verified.
50
+
51
+ 6. **Specify the Immediate Next Action**
52
+ Draft a single, atomic, unambiguous instruction for the next agent to execute on turn one. Do not provide a broad list; specify the exact command to run or file to edit first.
53
+
54
+ ---
55
+
56
+ ## Operating Rules
57
+
58
+ - **Separate Facts from Speculation**: A test that passed is a fact; assuming code works because it compiled is speculation. Label untested assumptions explicitly as `[UNVERIFIED]`.
59
+ - **Mandate Owners and Verifications for Open Loops**: Every open loop must state an owner (`Human` or `Agent`) and a deterministic verification criteria (e.g., `pytest tests/auth/test_token.py:test_refresh_rotation passes`).
60
+ - **Ban Vague Continuations**: Never write "continue working on auth" or "finish frontend components." State the exact symbol, file, and failure mode.
61
+ - **Reference Real Workspace Identifiers**: All references to code must use exact file paths and symbol names. Do not use generalized pseudocode descriptions.
62
+
63
+ ---
64
+
65
+ ## Worked Example: Session Handoff Note
66
+
67
+ ```markdown
68
+ # Session Handoff: JWT Rotation Implementation
69
+
70
+ ### Goal & Current State
71
+ - **Target Goal**: Implement refresh token rotation and revocation in the FastAPI auth service.
72
+ - **Current State**: Refresh token database migration applied and rotation endpoint created. Token generation and database persistence work. Revocation blacklist cache is not yet wired into the dependency validator.
73
+ - **Verification**: `pytest tests/test_auth_tokens.py` passes (4/4 tests).
74
+
75
+ ### Decisions Made (With Reasons)
76
+ - **Used Redis for Token Revocation List**: Selected Redis over Postgres lookups for revoked JTI tokens to keep latency under 5ms on protected routes.
77
+ - **Rejected Stateless Refresh Tokens**: We initially evaluated sliding expiration in JWT claims without persistence, but discarded it because compromised tokens could not be revoked before natural expiry.
78
+ - **Token Format**: Chose UUIDv4 for `jti` claim to maintain compatibility with existing session storage schemas.
79
+
80
+ ### Files Changed
81
+ - `src/services/auth.py`: Added `rotate_refresh_token(old_token: str)` function and Redis invalidation call.
82
+ - `src/models/tokens.py`: Added `RefreshTokenRecord` SQLModel table for tracking token families.
83
+ - `alembic/versions/20261005_add_token_families.py`: Migration creating `token_records` table.
84
+ - `tests/test_auth_tokens.py`: Added unit tests for token family generation and rotation.
85
+
86
+ ### Open Loops
87
+ - [Agent] Wire `verify_token_not_revoked` into `src/api/deps.py` -> Verify by checking `tests/test_auth_deps.py` fails on blacklisted token.
88
+ - [Agent] Handle database rollback if Redis is unreachable during rotation -> Verify by mocking Redis connection failure in unit test.
89
+ - [Human] Set `REDIS_AUTH_URL` secret in `.env.test` for CI execution.
90
+
91
+ ### Next Action
92
+ Open `src/api/deps.py` and import `is_token_blacklisted` from `src.services.auth`. Insert the check into `get_current_user` directly after JWT decode verification.
93
+
94
+ ### Watch-outs
95
+ - Do not modify the existing `access_token` expiration time (currently 15 minutes) in `src/core/config.py`.
96
+ - Alembic migration `20261005_add_token_families.py` must run before starting the API server locally (`alembic upgrade head`).
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Anti-Patterns
102
+
103
+ - **The Chat Transcript Dump**: Pasting raw conversation logs into the handoff note. The next session's context window is immediately polluted with the very conversational noise you intended to escape.
104
+ - **The Phantom Pass**: Marking a module "completed" when it has only been written, not executed or tested.
105
+ - **The Omitted Dead-End**: Leaving out solutions that failed. The next agent will read the codebase, see the same apparent path, and re-implement the failed approach.
106
+ - **The Abstract Next Step**: Ending with "Next: Implement remaining tests." The new agent spends 3-5 unnecessary turns discovering what needs testing.
107
+
108
+ ## Limitations
109
+
110
+ - This skill produces a written handoff only; it cannot preserve live state such as running processes, uncommitted terminal output, or in-memory debugging context — record anything that matters explicitly in the note.
111
+ - Handoff quality is bounded by what the outgoing session records: decisions, dead-ends, or file changes that are not written down cannot be recovered by the next session.
112
+ - It does not replace version control or issue tracking; commit work first and reference commits or tickets in the note instead of duplicating them.