@caddis/cli 0.4.0 → 0.4.2
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/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +1 -1
- package/bundles/antigravity-plugin/scripts/setup_project_ai.py +34 -5
- package/bundles/antigravity-plugin/skills/_registry.md +2 -0
- package/bundles/antigravity-plugin/skills/safe-cleanup/SKILL.md +176 -0
- package/bundles/antigravity-plugin/skills/tech-explainer/SKILL.md +280 -0
- package/bundles/antigravity-plugin/skills/tech-explainer/assets/skeleton.md +255 -0
- package/bundles/antigravity-plugin/skills/tech-explainer/evals/evals.json +50 -0
- package/bundles/antigravity-plugin/skills/tech-explainer/references/diagrams.md +237 -0
- package/bundles/antigravity-plugin/skills/tech-explainer/references/language.md +141 -0
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/_registry.md +1 -0
- package/bundles/antigravity-plugin-extras/skills/safe-cleanup/SKILL.md +176 -0
- package/bundles/codex/.codex/skills/devops/safe-cleanup/SKILL.md +176 -0
- package/bundles/codex/.codex/skills/docs/tech-explainer/SKILL.md +280 -0
- package/bundles/codex/.codex/skills/docs/tech-explainer/assets/skeleton.md +255 -0
- package/bundles/codex/.codex/skills/docs/tech-explainer/evals/evals.json +50 -0
- package/bundles/codex/.codex/skills/docs/tech-explainer/references/diagrams.md +237 -0
- package/bundles/codex/.codex/skills/docs/tech-explainer/references/language.md +141 -0
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Simplified Technical English for explainers
|
|
2
|
+
|
|
3
|
+
Read this during the STE rewrite pass (process step 6), not while drafting. Drafting in STE from the
|
|
4
|
+
first word produces stilted, thin prose — write the ideas out normally, then compress them here.
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- [Why STE, and what it is not](#why-ste-and-what-it-is-not)
|
|
9
|
+
- [The rules](#the-rules)
|
|
10
|
+
- [The rewrite table](#the-rewrite-table)
|
|
11
|
+
- [Building the vocabulary table (§2)](#building-the-vocabulary-table-2)
|
|
12
|
+
- [Numbers](#numbers)
|
|
13
|
+
- [Words to delete on sight](#words-to-delete-on-sight)
|
|
14
|
+
- [The mechanical pass](#the-mechanical-pass)
|
|
15
|
+
|
|
16
|
+
## Why STE, and what it is not
|
|
17
|
+
|
|
18
|
+
Simplified Technical English comes from aerospace maintenance manuals, where a misread sentence
|
|
19
|
+
grounds an aircraft. It constrains grammar and vocabulary so that a sentence has exactly one reading.
|
|
20
|
+
|
|
21
|
+
It is **not** writing for a beginner. Your reader may be more senior than you. They are expert in a
|
|
22
|
+
*different* area, and they are reading fast. STE removes the work of guessing which of two readings
|
|
23
|
+
you meant, so their attention goes to your argument instead of your syntax.
|
|
24
|
+
|
|
25
|
+
A side effect worth knowing: STE makes weak claims obvious. "Performance was somewhat impacted by
|
|
26
|
+
the configuration" survives normal prose. Rewritten as "the configuration made X slower by Y" it
|
|
27
|
+
either gets a number or gets deleted. **The rewrite pass is also a truth pass.**
|
|
28
|
+
|
|
29
|
+
## The rules
|
|
30
|
+
|
|
31
|
+
1. **One idea per sentence.** Target under 20 words. Split on `and`, `but`, `which`, `;`, and any
|
|
32
|
+
comma joining two independent clauses.
|
|
33
|
+
2. **One word, one meaning.** Fix each term in §2 and use only that term. Synonym variation is good
|
|
34
|
+
literary style and bad technical style — the reader cannot tell whether "engine", "runtime", and
|
|
35
|
+
"backend" are three things or one.
|
|
36
|
+
3. **One meaning, one word.** The converse: never use a defined term for anything else. If "program"
|
|
37
|
+
means the installable wrapper, do not also call a Python script a "program".
|
|
38
|
+
4. **Active voice, actor named.** "We withdrew the result" > "the result was withdrawn". Passive
|
|
39
|
+
hides who did it, and in an investigation write-up that is exactly the information at stake.
|
|
40
|
+
5. **Present tense for what is true, past tense for what was measured.** "Decode is 91.6% of the
|
|
41
|
+
time" (a property). "We measured 223 samples" (an event).
|
|
42
|
+
6. **No idiom, no metaphor, no humour, no rhetorical questions.** These are the constructs that
|
|
43
|
+
translate worst and skim worst.
|
|
44
|
+
7. **No hedging without a size.** Not "it seems likely" — instead name the uncertainty: "we measured
|
|
45
|
+
this once, with a test prompt, not a real prompt".
|
|
46
|
+
8. **Never open a paragraph with a bare pronoun** referring to the previous paragraph. Readers land
|
|
47
|
+
mid-page. Name the subject again.
|
|
48
|
+
9. **Front-load the sentence.** Subject and verb in the first six words. Put conditions after the
|
|
49
|
+
main clause: "Use TCC mode if the server runs as a Windows service" — not "If the server runs as
|
|
50
|
+
a Windows service, then in that case TCC mode should be used."
|
|
51
|
+
10. **Lists for lists.** If a sentence contains three or more parallel items, make it a bulleted list
|
|
52
|
+
or a table. Prose is for judgement, not enumeration.
|
|
53
|
+
11. **Bold the answer, not the emphasis.** Bold carries the sentence a skimming reader must not miss.
|
|
54
|
+
If half a paragraph is bold, none of it is.
|
|
55
|
+
12. **Define on first use, inline, and again in §2.** The reader will not follow a link mid-document.
|
|
56
|
+
|
|
57
|
+
## The rewrite table
|
|
58
|
+
|
|
59
|
+
| Instead of | Write |
|
|
60
|
+
| --------------------------------------------------------- | --------------------------------------------------------- |
|
|
61
|
+
| utilize, leverage | use |
|
|
62
|
+
| initiate, commence | start |
|
|
63
|
+
| terminate | stop |
|
|
64
|
+
| in order to | to |
|
|
65
|
+
| due to the fact that | because |
|
|
66
|
+
| at this point in time | now |
|
|
67
|
+
| a number of, several | the number (or "about 12") |
|
|
68
|
+
| significantly faster | 1.9 times faster |
|
|
69
|
+
| performance degradation was observed | it became slower — by X |
|
|
70
|
+
| there is a possibility that | X may happen if Y |
|
|
71
|
+
| it should be noted that | *(delete; state the thing)* |
|
|
72
|
+
| we were unable to determine | we did not measure this. To measure it: \<method\> |
|
|
73
|
+
| the aforementioned approach | *(name it)* |
|
|
74
|
+
| this is non-trivial | this takes \<time\> and needs \<thing\> |
|
|
75
|
+
| best-in-class, industry-standard, robust | *(delete; give the number or the source)* |
|
|
76
|
+
| optimize, improve | make faster / make smaller / reduce cost — say which |
|
|
77
|
+
| The result was withdrawn. | We withdrew the result. |
|
|
78
|
+
|
|
79
|
+
## Building the vocabulary table (§2)
|
|
80
|
+
|
|
81
|
+
Include a term when **any** of these is true:
|
|
82
|
+
|
|
83
|
+
- It is an acronym (`TCC`, `MoE`, `GGUF`, `SLA`).
|
|
84
|
+
- Two readers in the thread have used it to mean different things. These are the highest-value rows.
|
|
85
|
+
- It has a common meaning and a narrow meaning here ("program", "engine", "wrapper", "load").
|
|
86
|
+
- Your argument turns on the distinction between it and a neighbouring term.
|
|
87
|
+
|
|
88
|
+
Format — one meaning, in plain words, using only terms already defined:
|
|
89
|
+
|
|
90
|
+
```markdown
|
|
91
|
+
| Word | Meaning |
|
|
92
|
+
| --------------- | ----------------------------------------------------------------------------- |
|
|
93
|
+
| **Token** | A piece of a word. About 4 characters of English. A model reads and writes tokens. |
|
|
94
|
+
| **Engine** | The software that does the mathematics on the GPU. |
|
|
95
|
+
| **Wrapper** | A program that contains an engine, but does not replace it. |
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Eight to twelve rows is typical. Above about fifteen, the table stops being read — move the rare
|
|
99
|
+
terms to inline definitions at first use.
|
|
100
|
+
|
|
101
|
+
Open the section with the instruction, not just the table: *"Use these words with one meaning only."*
|
|
102
|
+
|
|
103
|
+
## Numbers
|
|
104
|
+
|
|
105
|
+
Every number in the document carries three things, either inline or in the table it sits in:
|
|
106
|
+
|
|
107
|
+
- **The unit.** "659 tokens per second", not "659".
|
|
108
|
+
- **The basis.** Measured or calculated? From what sample? On what date? "measured from `gpt-4o`",
|
|
109
|
+
"223 samples", "measured 2026-08-07".
|
|
110
|
+
- **Its precision.** Write "about 125" when the run-to-run spread is ±10. False precision ("125.4")
|
|
111
|
+
invites a challenge you cannot defend, and burns the credibility of the numbers that *are* exact.
|
|
112
|
+
|
|
113
|
+
Ratios beat percentages when the gap is large: "about 5 times too slow" lands harder and is
|
|
114
|
+
remembered better than "80% below target". Percentages are right for small differences: "0.3%
|
|
115
|
+
slower".
|
|
116
|
+
|
|
117
|
+
State the direction: "10.3% slower" not "10.3% difference".
|
|
118
|
+
|
|
119
|
+
## Words to delete on sight
|
|
120
|
+
|
|
121
|
+
`basically`, `essentially`, `simply`, `just`, `obviously`, `of course`, `clearly`, `actually`,
|
|
122
|
+
`very`, `quite`, `fairly`, `really`, `arguably`, `interestingly`, `unfortunately`, `it turns out
|
|
123
|
+
that`, `as we all know`.
|
|
124
|
+
|
|
125
|
+
Each one either adds nothing or, worse, tells the reader their question was stupid. "Simply run the
|
|
126
|
+
benchmark" is a sentence that makes a reader who then fails feel foolish, and they will not tell you.
|
|
127
|
+
|
|
128
|
+
## The mechanical pass
|
|
129
|
+
|
|
130
|
+
Work through the draft once per rule. Batching is faster and catches more than reading for "style".
|
|
131
|
+
|
|
132
|
+
1. **Sentence length.** Find every sentence over 25 words. Split or cut it.
|
|
133
|
+
2. **Passive voice.** Search `was `, `were `, `been `, `is being`. Rewrite with the actor named,
|
|
134
|
+
or confirm the actor genuinely does not matter.
|
|
135
|
+
3. **Synonyms for defined terms.** For each row of §2, search the document for its synonyms and
|
|
136
|
+
replace them.
|
|
137
|
+
4. **Bare numbers.** Search for digits. Every hit needs a unit and a basis.
|
|
138
|
+
5. **Filler.** Search the delete-on-sight list.
|
|
139
|
+
6. **Paragraph openings.** Scan the first three words of each paragraph for `This`, `It`, `They`,
|
|
140
|
+
`That`.
|
|
141
|
+
7. **Read the five-line summary cold**, as though you had read nothing else.
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
| Changelog Generator | `changelog-generator/` | Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation. |
|
|
52
52
|
| Deploy Local | `deploy-local/` | End-to-end local deployment loop for Gitea-hosted projects. Use when the user wants to commit on dev, push to remote, monitor the golden CI/build/deploy workflow, validate prod on the configured prod host, and fix lint/test/pipeline failures until deployment is healthy. |
|
|
53
53
|
| Monorepo | `monorepo/` | Monorepo management with Turborepo and pnpm workspaces. Use for Turborepo setup, turbo.json task dependencies, remote caching, pnpm workspace protocol, shared packages (ui-library, config, types), affected-only CI/CD builds, or monorepo structure (apps/ vs packages/). Covers pitfalls like circular deps, version drift, and hoisting issues. |
|
|
54
|
+
| Safe Cleanup | `safe-cleanup/` | Use BEFORE deleting any directory tree, clearing a scratchpad, removing git worktrees, or "tidying up" generated folders - especially on Windows, where a recursive delete that meets a junction or symlink destroys the link's TARGET. Also use when verifying that a Python venv or node_modules survived a cleanup, because pip check and file counts both report healthy on a half-deleted environment. |
|
|
54
55
|
| Update Readme | `update-readme/` | Detect feature commits and update README sections (Features, API, Usage) with accurate repo-aware changes, then stage only README for a docs commit. Use when users add features/routes/components and want README maintained automatically. |
|
|
55
56
|
|
|
56
57
|
### Docs
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: safe-cleanup
|
|
3
|
+
description: Use BEFORE deleting any directory tree, clearing a scratchpad, removing git worktrees, or "tidying up" generated folders - especially on Windows, where a recursive delete that meets a junction or symlink destroys the link's TARGET. Also use when verifying that a Python venv or node_modules survived a cleanup, because pip check and file counts both report healthy on a half-deleted environment.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Safe Cleanup
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Cleanup looks like the safest thing you do all day. It is the most destructive.
|
|
11
|
+
|
|
12
|
+
This skill exists because of a real incident: an agent created 31 directory
|
|
13
|
+
junctions from throwaway worktrees into live repositories, unlinked them, saw
|
|
14
|
+
"31 unlinked, 0 failed", then recursively deleted the parent. A junction it had
|
|
15
|
+
never seen survived the scan, the delete followed it, and **five repositories'
|
|
16
|
+
development environments were destroyed**. Every verification afterwards said
|
|
17
|
+
"intact", because the verification was wrong too.
|
|
18
|
+
|
|
19
|
+
Two hours of repair. None of it would have happened with the rules below.
|
|
20
|
+
|
|
21
|
+
## The one-paragraph version
|
|
22
|
+
|
|
23
|
+
On Windows, `Remove-Item -Recurse` **follows junctions and deletes their
|
|
24
|
+
targets**. Enumerating with `-ErrorAction SilentlyContinue` hides the ones you
|
|
25
|
+
cannot read. And after the damage, `pip check`, `pip list` and file counts all
|
|
26
|
+
report success, because metadata survives while package files do not. So: never
|
|
27
|
+
link into somewhere real, never recurse through a link, prefer rename over
|
|
28
|
+
delete, and verify by *using* the thing rather than counting it.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## RULE 1 - Prefer rename over delete
|
|
33
|
+
|
|
34
|
+
Rename is atomic. Recursive delete is not: it can fail half way and leave a
|
|
35
|
+
directory that looks present and is unusable.
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
Rename-Item -Path $target -NewName ".broken-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
In the incident, a venv holding `_greenlet.cp314-win_amd64.pyd` open **refused to
|
|
42
|
+
delete** but **renamed fine**. Had the delete been retried or forced, it would
|
|
43
|
+
have half-emptied the directory.
|
|
44
|
+
|
|
45
|
+
Rename first, build fresh alongside, delete the old copy later when nothing holds
|
|
46
|
+
it. Disk is cheap. An environment nobody can recover is not.
|
|
47
|
+
|
|
48
|
+
## RULE 2 - Never recurse a delete through a reparse point
|
|
49
|
+
|
|
50
|
+
Junctions and symlinks are transparent to `Remove-Item -Recurse`. Deleting a
|
|
51
|
+
folder that contains a link to `C:\live\thing` deletes **the contents of
|
|
52
|
+
`C:\live\thing`**.
|
|
53
|
+
|
|
54
|
+
Check first, and **fail if the check itself cannot complete**:
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
$links = @(Get-ChildItem $path -Recurse -Force -Directory -ErrorAction Stop |
|
|
58
|
+
Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint })
|
|
59
|
+
if ($links.Count) {
|
|
60
|
+
$links | ForEach-Object { Write-Host " $($_.FullName) -> $($_.Target)" }
|
|
61
|
+
throw "refusing: $($links.Count) link(s) inside $path"
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`-ErrorAction SilentlyContinue` on that scan is precisely what caused the
|
|
66
|
+
incident. A link inside a directory the scanner could not read was skipped in
|
|
67
|
+
silence and never appeared in the count. **A scan that cannot see everything must
|
|
68
|
+
fail, not shrug.**
|
|
69
|
+
|
|
70
|
+
To remove a link itself, unlink it - never delete through it:
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
[System.IO.Directory]::Delete($linkPath, $false) # link only, never the target
|
|
74
|
+
cmd /c rmdir "$linkPath" # same effect
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## RULE 3 - Do not create links into live directories
|
|
78
|
+
|
|
79
|
+
The safest delete is the one with nothing to follow.
|
|
80
|
+
|
|
81
|
+
If a throwaway workspace needs dependencies, **build them there**, or point the
|
|
82
|
+
tool at an absolute interpreter path. Do not junction `<scratch>\.venv` to
|
|
83
|
+
`<live-repo>\.venv` for convenience - that is a loaded gun aimed at the repo, and
|
|
84
|
+
any cleanup above the link can fire it.
|
|
85
|
+
|
|
86
|
+
If you must link, remove the link the moment you are done, and never leave one
|
|
87
|
+
inside a directory that something else will clean up.
|
|
88
|
+
|
|
89
|
+
## RULE 4 - Verify by using, not by counting
|
|
90
|
+
|
|
91
|
+
After any cleanup near an environment, prove it still works. These all LIE on a
|
|
92
|
+
half-deleted Python venv, because dist-info metadata survives while package files
|
|
93
|
+
are removed:
|
|
94
|
+
|
|
95
|
+
| Check | Why it lies |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `Test-Path .venv\Scripts\python.exe` | Proves the interpreter exists. Says nothing about packages. |
|
|
98
|
+
| file or directory counts | Metadata directories inflate the count. |
|
|
99
|
+
| `pip list` | Reads metadata. |
|
|
100
|
+
| `pip check` | Reads metadata. Reported "No broken requirements found" on a venv that could not import fastapi. |
|
|
101
|
+
| `pip install -e .` | Reads metadata, calls the package satisfied, **skips it, and reports success**. |
|
|
102
|
+
|
|
103
|
+
The honest check is whether the interpreter can import what the project declares:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import importlib.util
|
|
107
|
+
importlib.util.find_spec("fastapi") is None # -> True means genuinely missing
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For node, check that a real entry point resolves, not that `node_modules` exists.
|
|
111
|
+
|
|
112
|
+
**Map distribution names to import names.** `pyjwt` imports as `jwt`,
|
|
113
|
+
`python-ulid` as `ulid`, `python-dotenv` as `dotenv`. A naive audit produces false
|
|
114
|
+
positives and will have you rebuilding healthy environments - which is its own way
|
|
115
|
+
of causing an outage.
|
|
116
|
+
|
|
117
|
+
## RULE 5 - Stop what holds the files, or refuse
|
|
118
|
+
|
|
119
|
+
A running service turns a delete into a **partial** delete - the worst outcome.
|
|
120
|
+
|
|
121
|
+
Stop named services first. Then check for surviving processes and **refuse rather
|
|
122
|
+
than force**:
|
|
123
|
+
|
|
124
|
+
```powershell
|
|
125
|
+
$held = @(Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
|
|
126
|
+
Where-Object { $_.ExecutablePath -like "$venv\*" })
|
|
127
|
+
if ($held.Count) { throw "refusing: still held by PID $($held[0].ProcessId)" }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
In the incident's repair, this refused on one repo because a **VS Code language
|
|
131
|
+
server** had held that venv since the previous day. Not a service, not safe to
|
|
132
|
+
kill, not the script's business. That repo was fixed a different way. **Refusing
|
|
133
|
+
was correct.**
|
|
134
|
+
|
|
135
|
+
## RULE 6 - One agent at a time on a shared resource
|
|
136
|
+
|
|
137
|
+
Two agents installing into the same environment concurrently can produce exactly
|
|
138
|
+
this partial state. Three sessions were live in one repository during the
|
|
139
|
+
incident. Whoever is repairing says so; everyone else keeps out.
|
|
140
|
+
|
|
141
|
+
## RULE 7 - Verify against the source of truth, not the working copy
|
|
142
|
+
|
|
143
|
+
Checkouts go stale. During the same incident a subagent reported a file as absent
|
|
144
|
+
from a configuration, contradicting a change made hours earlier. The file was
|
|
145
|
+
present on `origin/main`; the working copy was **10 commits behind**.
|
|
146
|
+
|
|
147
|
+
Before asserting something is missing, check `git show origin/main:<path>`, not
|
|
148
|
+
the file on disk.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Before you delete anything - the checklist
|
|
153
|
+
|
|
154
|
+
1. Can I **rename** instead? Do that.
|
|
155
|
+
2. Does the tree contain **reparse points**? Scan with `-ErrorAction Stop`. Refuse if any, or if the scan fails.
|
|
156
|
+
3. Does anything **hold files open**? Stop services; refuse on surviving processes.
|
|
157
|
+
4. Did I create any **links into live directories** earlier in this session? Unlink them explicitly, by name.
|
|
158
|
+
5. After: **verify by using**, not by counting.
|
|
159
|
+
6. If something looks wrong: check `origin/main`, not the working copy.
|
|
160
|
+
|
|
161
|
+
## What good verification output looks like
|
|
162
|
+
|
|
163
|
+
Not this:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
.venv intact: True <- only proves python.exe exists
|
|
167
|
+
31 unlinked, 0 failed <- says nothing about what the scan missed
|
|
168
|
+
No broken requirements found <- reads metadata, not reality
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
This:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
service-a OK declared=24 missing=0 <- every declared dep imported
|
|
175
|
+
service-b OK declared=19 missing=0
|
|
176
|
+
```
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: safe-cleanup
|
|
3
|
+
description: Use BEFORE deleting any directory tree, clearing a scratchpad, removing git worktrees, or "tidying up" generated folders - especially on Windows, where a recursive delete that meets a junction or symlink destroys the link's TARGET. Also use when verifying that a Python venv or node_modules survived a cleanup, because pip check and file counts both report healthy on a half-deleted environment.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Safe Cleanup
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Cleanup looks like the safest thing you do all day. It is the most destructive.
|
|
11
|
+
|
|
12
|
+
This skill exists because of a real incident: an agent created 31 directory
|
|
13
|
+
junctions from throwaway worktrees into live repositories, unlinked them, saw
|
|
14
|
+
"31 unlinked, 0 failed", then recursively deleted the parent. A junction it had
|
|
15
|
+
never seen survived the scan, the delete followed it, and **five repositories'
|
|
16
|
+
development environments were destroyed**. Every verification afterwards said
|
|
17
|
+
"intact", because the verification was wrong too.
|
|
18
|
+
|
|
19
|
+
Two hours of repair. None of it would have happened with the rules below.
|
|
20
|
+
|
|
21
|
+
## The one-paragraph version
|
|
22
|
+
|
|
23
|
+
On Windows, `Remove-Item -Recurse` **follows junctions and deletes their
|
|
24
|
+
targets**. Enumerating with `-ErrorAction SilentlyContinue` hides the ones you
|
|
25
|
+
cannot read. And after the damage, `pip check`, `pip list` and file counts all
|
|
26
|
+
report success, because metadata survives while package files do not. So: never
|
|
27
|
+
link into somewhere real, never recurse through a link, prefer rename over
|
|
28
|
+
delete, and verify by *using* the thing rather than counting it.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## RULE 1 - Prefer rename over delete
|
|
33
|
+
|
|
34
|
+
Rename is atomic. Recursive delete is not: it can fail half way and leave a
|
|
35
|
+
directory that looks present and is unusable.
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
Rename-Item -Path $target -NewName ".broken-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
In the incident, a venv holding `_greenlet.cp314-win_amd64.pyd` open **refused to
|
|
42
|
+
delete** but **renamed fine**. Had the delete been retried or forced, it would
|
|
43
|
+
have half-emptied the directory.
|
|
44
|
+
|
|
45
|
+
Rename first, build fresh alongside, delete the old copy later when nothing holds
|
|
46
|
+
it. Disk is cheap. An environment nobody can recover is not.
|
|
47
|
+
|
|
48
|
+
## RULE 2 - Never recurse a delete through a reparse point
|
|
49
|
+
|
|
50
|
+
Junctions and symlinks are transparent to `Remove-Item -Recurse`. Deleting a
|
|
51
|
+
folder that contains a link to `C:\live\thing` deletes **the contents of
|
|
52
|
+
`C:\live\thing`**.
|
|
53
|
+
|
|
54
|
+
Check first, and **fail if the check itself cannot complete**:
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
$links = @(Get-ChildItem $path -Recurse -Force -Directory -ErrorAction Stop |
|
|
58
|
+
Where-Object { $_.Attributes -band [IO.FileAttributes]::ReparsePoint })
|
|
59
|
+
if ($links.Count) {
|
|
60
|
+
$links | ForEach-Object { Write-Host " $($_.FullName) -> $($_.Target)" }
|
|
61
|
+
throw "refusing: $($links.Count) link(s) inside $path"
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`-ErrorAction SilentlyContinue` on that scan is precisely what caused the
|
|
66
|
+
incident. A link inside a directory the scanner could not read was skipped in
|
|
67
|
+
silence and never appeared in the count. **A scan that cannot see everything must
|
|
68
|
+
fail, not shrug.**
|
|
69
|
+
|
|
70
|
+
To remove a link itself, unlink it - never delete through it:
|
|
71
|
+
|
|
72
|
+
```powershell
|
|
73
|
+
[System.IO.Directory]::Delete($linkPath, $false) # link only, never the target
|
|
74
|
+
cmd /c rmdir "$linkPath" # same effect
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## RULE 3 - Do not create links into live directories
|
|
78
|
+
|
|
79
|
+
The safest delete is the one with nothing to follow.
|
|
80
|
+
|
|
81
|
+
If a throwaway workspace needs dependencies, **build them there**, or point the
|
|
82
|
+
tool at an absolute interpreter path. Do not junction `<scratch>\.venv` to
|
|
83
|
+
`<live-repo>\.venv` for convenience - that is a loaded gun aimed at the repo, and
|
|
84
|
+
any cleanup above the link can fire it.
|
|
85
|
+
|
|
86
|
+
If you must link, remove the link the moment you are done, and never leave one
|
|
87
|
+
inside a directory that something else will clean up.
|
|
88
|
+
|
|
89
|
+
## RULE 4 - Verify by using, not by counting
|
|
90
|
+
|
|
91
|
+
After any cleanup near an environment, prove it still works. These all LIE on a
|
|
92
|
+
half-deleted Python venv, because dist-info metadata survives while package files
|
|
93
|
+
are removed:
|
|
94
|
+
|
|
95
|
+
| Check | Why it lies |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `Test-Path .venv\Scripts\python.exe` | Proves the interpreter exists. Says nothing about packages. |
|
|
98
|
+
| file or directory counts | Metadata directories inflate the count. |
|
|
99
|
+
| `pip list` | Reads metadata. |
|
|
100
|
+
| `pip check` | Reads metadata. Reported "No broken requirements found" on a venv that could not import fastapi. |
|
|
101
|
+
| `pip install -e .` | Reads metadata, calls the package satisfied, **skips it, and reports success**. |
|
|
102
|
+
|
|
103
|
+
The honest check is whether the interpreter can import what the project declares:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import importlib.util
|
|
107
|
+
importlib.util.find_spec("fastapi") is None # -> True means genuinely missing
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For node, check that a real entry point resolves, not that `node_modules` exists.
|
|
111
|
+
|
|
112
|
+
**Map distribution names to import names.** `pyjwt` imports as `jwt`,
|
|
113
|
+
`python-ulid` as `ulid`, `python-dotenv` as `dotenv`. A naive audit produces false
|
|
114
|
+
positives and will have you rebuilding healthy environments - which is its own way
|
|
115
|
+
of causing an outage.
|
|
116
|
+
|
|
117
|
+
## RULE 5 - Stop what holds the files, or refuse
|
|
118
|
+
|
|
119
|
+
A running service turns a delete into a **partial** delete - the worst outcome.
|
|
120
|
+
|
|
121
|
+
Stop named services first. Then check for surviving processes and **refuse rather
|
|
122
|
+
than force**:
|
|
123
|
+
|
|
124
|
+
```powershell
|
|
125
|
+
$held = @(Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
|
|
126
|
+
Where-Object { $_.ExecutablePath -like "$venv\*" })
|
|
127
|
+
if ($held.Count) { throw "refusing: still held by PID $($held[0].ProcessId)" }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
In the incident's repair, this refused on one repo because a **VS Code language
|
|
131
|
+
server** had held that venv since the previous day. Not a service, not safe to
|
|
132
|
+
kill, not the script's business. That repo was fixed a different way. **Refusing
|
|
133
|
+
was correct.**
|
|
134
|
+
|
|
135
|
+
## RULE 6 - One agent at a time on a shared resource
|
|
136
|
+
|
|
137
|
+
Two agents installing into the same environment concurrently can produce exactly
|
|
138
|
+
this partial state. Three sessions were live in one repository during the
|
|
139
|
+
incident. Whoever is repairing says so; everyone else keeps out.
|
|
140
|
+
|
|
141
|
+
## RULE 7 - Verify against the source of truth, not the working copy
|
|
142
|
+
|
|
143
|
+
Checkouts go stale. During the same incident a subagent reported a file as absent
|
|
144
|
+
from a configuration, contradicting a change made hours earlier. The file was
|
|
145
|
+
present on `origin/main`; the working copy was **10 commits behind**.
|
|
146
|
+
|
|
147
|
+
Before asserting something is missing, check `git show origin/main:<path>`, not
|
|
148
|
+
the file on disk.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Before you delete anything - the checklist
|
|
153
|
+
|
|
154
|
+
1. Can I **rename** instead? Do that.
|
|
155
|
+
2. Does the tree contain **reparse points**? Scan with `-ErrorAction Stop`. Refuse if any, or if the scan fails.
|
|
156
|
+
3. Does anything **hold files open**? Stop services; refuse on surviving processes.
|
|
157
|
+
4. Did I create any **links into live directories** earlier in this session? Unlink them explicitly, by name.
|
|
158
|
+
5. After: **verify by using**, not by counting.
|
|
159
|
+
6. If something looks wrong: check `origin/main`, not the working copy.
|
|
160
|
+
|
|
161
|
+
## What good verification output looks like
|
|
162
|
+
|
|
163
|
+
Not this:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
.venv intact: True <- only proves python.exe exists
|
|
167
|
+
31 unlinked, 0 failed <- says nothing about what the scan missed
|
|
168
|
+
No broken requirements found <- reads metadata, not reality
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
This:
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
service-a OK declared=24 missing=0 <- every declared dep imported
|
|
175
|
+
service-b OK declared=19 missing=0
|
|
176
|
+
```
|