opencode-skills-collection 4.1.7 → 4.1.8
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.
- package/bundled-skills/.antigravity-install-manifest.json +7 -1
- package/bundled-skills/court/SKILL.md +141 -0
- package/bundled-skills/court/references/jurors.json +14 -0
- package/bundled-skills/court/scripts/court.py +348 -0
- package/bundled-skills/darkmoon-pentest/SKILL.md +123 -0
- package/bundled-skills/lognorm/SKILL.md +86 -0
- package/bundled-skills/lognorm/references/playbooks.md +76 -0
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +6 -6
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +1 -1
- package/bundled-skills/parallel-search-mcp/SKILL.md +12 -0
- package/bundled-skills/scarf-single-cell/SKILL.md +326 -0
- package/bundled-skills/scarf-single-cell/references/LICENSE.md +29 -0
- package/bundled-skills/scarf-single-cell/references/clustering-and-embedding.md +270 -0
- package/bundled-skills/scarf-single-cell/references/data-access.md +269 -0
- package/bundled-skills/scarf-single-cell/references/features-and-graphs.md +279 -0
- package/bundled-skills/scarf-single-cell/references/gene-blacklists.md +182 -0
- package/bundled-skills/scarf-single-cell/references/integration-and-comparisons.md +295 -0
- package/bundled-skills/scarf-single-cell/references/markers-and-annotation.md +280 -0
- package/bundled-skills/scarf-single-cell/references/performance-and-export.md +291 -0
- package/bundled-skills/scarf-single-cell/references/pipeline-runs-and-artifacts.md +273 -0
- package/bundled-skills/scarf-single-cell/references/plotting.md +260 -0
- package/bundled-skills/scarf-single-cell/references/quality-control.md +290 -0
- package/bundled-skills/session-handoff/SKILL.md +112 -0
- package/bundled-skills/shark/SKILL.md +134 -0
- package/bundled-skills/shark/references/investors.json +12 -0
- package/bundled-skills/shark/scripts/shark.py +302 -0
- package/bundled-skills/telegram/assets/boilerplate/nodejs/package-lock.json +6 -6
- package/bundled-skills/telegram/assets/boilerplate/nodejs/package.json +1 -1
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/package-lock.json +6 -6
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/package.json +1 -1
- package/package.json +1 -1
- package/skills_index.json +201 -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.
|