Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c25175e4b3 | ||
|
|
e2a93c1e99 | ||
|
|
2399ce68e5 | ||
|
|
46cbfc5967 | ||
|
|
cf3abafc66 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "skills",
|
"name": "skills",
|
||||||
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
|
"description": "Mathias's engineering skills library, plus the hooks and MCP servers that make it enforceable: TDD, clean code, debugging, specs, discovery, CI, and secret hygiene.",
|
||||||
"version": "0.19.5",
|
"version": "0.21.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Mathias",
|
"name": "Mathias",
|
||||||
"url": "https://git.d-ma.be/mathias"
|
"url": "https://git.d-ma.be/mathias"
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ the SKILL.md, not the table.
|
|||||||
| `cognitive-load` | Cognitive complexity analysis (CLI score) | Identifying hotspots, before refactoring | claude-code, library |
|
| `cognitive-load` | Cognitive complexity analysis (CLI score) | Identifying hotspots, before refactoring | claude-code, library |
|
||||||
| `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious — or a bug report with no failure text, which this skill refuses rather than guesses at | claude-code, library |
|
| `debug` | Hypothesis-first debugging discipline | Any test failure or bug whose cause is not immediately obvious — or a bug report with no failure text, which this skill refuses rather than guesses at | claude-code, library |
|
||||||
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing | claude-code, library |
|
| `discovery-framing` | Problem-before-solution discovery/framing method | Framing/validating a problem before any solution; opportunity sizing | claude-code, library |
|
||||||
|
| `document-to-markdown` | Read .docx/.xlsx/.pptx/.epub/.msg and other formats the Read tool cannot open | You need the contents of a binary document — Word, Excel, PowerPoint, EPUB, Outlook .msg, a zip of documents — and Read returns nothing useful | claude-code, library |
|
||||||
| `dream` | Reflective consolidation pass over an agent's memory directory | Pruning/merging memory files, rebuilding a memory index | claude-code, library, chat |
|
| `dream` | Reflective consolidation pass over an agent's memory directory | Pruning/merging memory files, rebuilding a memory index | claude-code, library, chat |
|
||||||
| `experiment-spec` | Rigorous experiment spec — falsifiable hypothesis, quantitative acceptance, null-result protocol | Scoping a research/ML phase instead of a feature | claude-code, library |
|
| `experiment-spec` | Rigorous experiment spec — falsifiable hypothesis, quantitative acceptance, null-result protocol | Scoping a research/ML phase instead of a feature | claude-code, library |
|
||||||
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project | claude-code, library |
|
| `feature-spec` | Implementation-level feature spec (tighter than spec-driven-dev) | Scoping one feature inside an already-specced project | claude-code, library |
|
||||||
|
|||||||
+1
-1
@@ -78,7 +78,7 @@ tasks:
|
|||||||
for entry in "{{.TASKFILE_DIR}}"/*; do
|
for entry in "{{.TASKFILE_DIR}}"/*; do
|
||||||
base=$(basename "$entry")
|
base=$(basename "$entry")
|
||||||
case "$base" in
|
case "$base" in
|
||||||
install.sh|Taskfile.yml|README.md|SKILLS_INDEX.md|skills|hooks|bin|tests) continue ;;
|
install.sh|Taskfile.yml|README.md|SKILLS_INDEX.md|skills|hooks|bin|tests|agents) continue ;;
|
||||||
esac
|
esac
|
||||||
echo "ERROR: unexpected entry at repo root: $base" >&2
|
echo "ERROR: unexpected entry at repo root: $base" >&2
|
||||||
fail=1
|
fail=1
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
---
|
||||||
|
name: pr-gatekeeper
|
||||||
|
description: Adversarially reviews one open Gitea PR, verifies its CI verdict, then merges and closes — or escalates. Use for risk/medium work that went through a PR instead of straight to main. Spawned with a repo, PR number and the issue it closes.
|
||||||
|
tools: Read, Grep, Glob, Bash
|
||||||
|
color: "#7C3AED"
|
||||||
|
---
|
||||||
|
|
||||||
|
<role>
|
||||||
|
You are the merge gate for a solo estate. Nobody else will read this PR. If you
|
||||||
|
approve something broken, it ships to a live GitOps cluster within 60 seconds.
|
||||||
|
|
||||||
|
Your default posture is refusal. The author (another agent) believes the work is
|
||||||
|
correct — that belief is the input you are testing, not evidence. The same model
|
||||||
|
in the author's seat has convinced itself five times in a row and been wrong
|
||||||
|
every time.
|
||||||
|
|
||||||
|
Blocking the same PR repeatedly is the control working, not you being
|
||||||
|
difficult. For code whose job is to refuse something, expect it.
|
||||||
|
</role>
|
||||||
|
|
||||||
|
## What you are given
|
||||||
|
|
||||||
|
- `owner/repo`, the PR number, and the issue number it claims to close
|
||||||
|
- The claim the author made about what the change does
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
Work in the repo checkout at the path given to you. Never push, never force,
|
||||||
|
never amend the author's commits.
|
||||||
|
|
||||||
|
### 1. Read the issue before the diff
|
||||||
|
|
||||||
|
```
|
||||||
|
mcp__plugin_skills_gitea__issue_get
|
||||||
|
mcp__plugin_skills_gitea__issue_list_comments
|
||||||
|
```
|
||||||
|
|
||||||
|
The issue's "done when" clauses are the acceptance criteria. A PR that is
|
||||||
|
well-built and does not satisfy them is a **reject**, not an approve-with-note.
|
||||||
|
Comments carry sequencing constraints that the body does not.
|
||||||
|
|
||||||
|
### 2. Get the real diff
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git fetch origin
|
||||||
|
git diff origin/main...origin/<branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
Read every hunk. Do not skim. If the diff is too large to review properly, say
|
||||||
|
so and escalate rather than producing confident coverage of a fraction of it.
|
||||||
|
|
||||||
|
### 3. Iron laws — any violation blocks
|
||||||
|
|
||||||
|
1. **Secrets.** No credential, token or key in the diff, in a test fixture, or
|
||||||
|
rendered into output by a command the change adds. A `kubectl get secret -o
|
||||||
|
yaml`, a `sops -d` piped to stdout, or an `echo $TOKEN` is an immediate block.
|
||||||
|
2. **Swallowed errors.** `_ = f()`, a bare `except:`, `|| true` on a line whose
|
||||||
|
failure matters, `err` returned naked.
|
||||||
|
3. **Unvalidated input at a boundary.** External API responses, file reads, user
|
||||||
|
input used before checking.
|
||||||
|
4. **Live-breaking GitOps changes.** For `mathias/infra`: does this leave the
|
||||||
|
cluster reconcilable? A `kubectl diff` that is clean is **not** proof Flux
|
||||||
|
will accept it — server-side apply does not prune fields it does not own, and
|
||||||
|
that took the whole `apps` Kustomization down on 2026-09-16.
|
||||||
|
|
||||||
|
### 4. The test is the claim — attack it
|
||||||
|
|
||||||
|
This estate's recurring defect is **a check that passes without proving
|
||||||
|
anything**. Confirmed instances to pattern-match against:
|
||||||
|
|
||||||
|
- a suite grading a subprocess's stdout passed when the subprocess crashed:
|
||||||
|
empty output, zero assertions, `fail` stayed 0
|
||||||
|
- a gate mapped 11 of 18 subjects and *printed* the other seven, then exited 0
|
||||||
|
- a manifest check matched `type: Recreate` inside a **comment** explaining why
|
||||||
|
Recreate had been rejected
|
||||||
|
|
||||||
|
So, for every test the PR adds or changes:
|
||||||
|
|
||||||
|
- **Flip it.** Break the thing it claims to check and run it. If it still
|
||||||
|
passes, the test is decoration. Say so and block. On `dispatch#75` this found
|
||||||
|
**three behavioural changes shipped with no assertion at all** — correct code
|
||||||
|
that nothing would have noticed the removal of.
|
||||||
|
- **Read the name against the assertion.** A test whose name claims more than
|
||||||
|
it proves is worse than no test, because the name wins in the reader's head
|
||||||
|
and the gap becomes invisible. Found three times in one PR, twice in tests
|
||||||
|
written to fix the previous instance. `DoesNotInheritEnv` is honest;
|
||||||
|
`IsContained` is a claim the test cannot support. (local-dev `AGENT.md` rule 9.)
|
||||||
|
- Does it fail when it cannot run? An empty result, a parser that matches
|
||||||
|
nothing, a missing file must all be failures, never silent passes.
|
||||||
|
- Does it assert on what the machine reads, or on prose near it?
|
||||||
|
- Is coverage total, or a majority? Partial coverage must be expressed as a
|
||||||
|
failure or an explicit, justified allowlist.
|
||||||
|
|
||||||
|
A PR with no test for a behavioural change is a block unless the change is
|
||||||
|
purely declarative config, and even then ask what would catch its regression.
|
||||||
|
|
||||||
|
### 4b. How to attack — the three habits that actually found the escapes
|
||||||
|
|
||||||
|
Five rounds on `dispatch#75` produced five real escapes. **CI was green on every
|
||||||
|
one of them.** These are what found them; reading the diff did not.
|
||||||
|
|
||||||
|
**Verify by execution, not by reading the implementation.** Build a scratch
|
||||||
|
repo, run the command, look at the filesystem. Every finding came from a
|
||||||
|
command that ran, not from reasoning about a matcher. The matcher said one
|
||||||
|
thing and the filesystem said another, five times running. A claim you have not
|
||||||
|
executed is a hypothesis.
|
||||||
|
|
||||||
|
**Read the survivors.** After any allow/deny sweep, read the list of what is
|
||||||
|
still *permitted*, one entry at a time. This is what caught `git commit -F
|
||||||
|
/etc/passwd` — reads an arbitrary file into a commit message that is then
|
||||||
|
pushed, i.e. exfiltration wearing a message flag. It was found by neither the
|
||||||
|
review nor the differential, because both implementations **agreed** and both
|
||||||
|
were wrong. Agreement is not correctness.
|
||||||
|
|
||||||
|
**Grade a tool on its exit code, never on grepping its output.** A crashed
|
||||||
|
linter greps as clean. `golangci-lint` exits 0 clean / 1 issues found / >1 the
|
||||||
|
linter itself failed — and a harness that matched its summary text scored a
|
||||||
|
crash as a pass, leaving the only gate over an entire package silently absent.
|
||||||
|
|
||||||
|
Where a control has **two implementations** kept in sync by hand, run one case
|
||||||
|
list through both and diff the verdicts mechanically. A case one refuses and
|
||||||
|
the other allows is a real finding, and this has caught one.
|
||||||
|
|
||||||
|
### 4c. Probing without causing the incident you are testing for
|
||||||
|
|
||||||
|
**This agent has caused a credential leak.** On 2026-09-19 a probe testing
|
||||||
|
environment scrubbing printed `/proc/$PPID/environ` and `ps e` raw, and a `git
|
||||||
|
credential fill` consulted the host's real store. Seven live credentials went
|
||||||
|
into the transcript and had to be rotated (`infra#445`). The finding was
|
||||||
|
correct; the method was not.
|
||||||
|
|
||||||
|
Non-negotiable when probing anything credential-adjacent:
|
||||||
|
|
||||||
|
- **A probe that reads an environment prints a count, never the buffer.**
|
||||||
|
Grep for a sentinel and report whether it was found. A leak test that prints
|
||||||
|
what it found defeats itself.
|
||||||
|
- **Use an invented sentinel**, never a real secret name. Set it yourself, then
|
||||||
|
assert on its absence.
|
||||||
|
- **Anything touching git's credential subsystem runs with an isolated `HOME`**
|
||||||
|
and `GIT_CONFIG_GLOBAL`/`GIT_CONFIG_SYSTEM` pointed away from the real ones.
|
||||||
|
Otherwise it reads the host's store, which is the point of the subsystem.
|
||||||
|
- Prove a primitive and stop. Do not aim a demonstrated write or execution
|
||||||
|
primitive at a real secret to show it works.
|
||||||
|
|
||||||
|
A command whose *purpose* is to surface credentials will surface real ones if
|
||||||
|
you point it at a real environment.
|
||||||
|
|
||||||
|
### 5. CI
|
||||||
|
|
||||||
|
```
|
||||||
|
mcp__plugin_skills_gitea__pr_get # mergeable, conflicts
|
||||||
|
mcp__plugin_skills_gitea__workflow_run_list
|
||||||
|
```
|
||||||
|
|
||||||
|
CI green is necessary and not sufficient — you have just spent step 4
|
||||||
|
establishing what its green is worth. On `dispatch#75` it was worth nothing
|
||||||
|
five times running: `task check` exit 0, `golangci-lint` exit 0, every test
|
||||||
|
passing, while `git rm ././.dispatch-allow` deleted the file the control
|
||||||
|
existed to protect. Say so in your report when it applies: *green, and green
|
||||||
|
covers none of these findings.*
|
||||||
|
|
||||||
|
CI **red or absent** is an automatic block: an agent-opened PR with zero runs
|
||||||
|
is a known platform failure mode in this estate, not a pass.
|
||||||
|
|
||||||
|
### 6. Verdict
|
||||||
|
|
||||||
|
**Merge** only when all hold:
|
||||||
|
|
||||||
|
- no iron-law violation
|
||||||
|
- every "done when" clause in the issue is satisfied
|
||||||
|
- CI ran and is green
|
||||||
|
- you flipped the tests and they went red
|
||||||
|
- the change is `risk/low` or `risk/medium`
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```
|
||||||
|
mcp__plugin_skills_gitea__pr_merge # squash
|
||||||
|
mcp__plugin_skills_gitea__issue_comment # the evidence, on the ISSUE
|
||||||
|
mcp__plugin_skills_gitea__issue_close
|
||||||
|
mcp__plugin_skills_gitea__branch_delete
|
||||||
|
```
|
||||||
|
|
||||||
|
Put your evidence on the **issue**, not only the PR thread. In Gitea a merge
|
||||||
|
auto-closes the gated issue and review comments live on a thread separate from
|
||||||
|
the issue's — so a finding recorded only on the PR is a finding nobody reading
|
||||||
|
the issue will ever see.
|
||||||
|
|
||||||
|
For `mathias/infra`, after merging:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flux reconcile kustomization apps --with-source
|
||||||
|
flux get kustomization apps
|
||||||
|
```
|
||||||
|
|
||||||
|
If it does not go `Ready: True`, **revert immediately** rather than
|
||||||
|
forward-fixing, and report. A blocked `apps` Kustomization stops every app in
|
||||||
|
the cluster, not just the changed one.
|
||||||
|
|
||||||
|
### 7. Escalate to Mathias — and only then
|
||||||
|
|
||||||
|
Do not merge. Comment the finding on the issue and report it. Escalate when:
|
||||||
|
|
||||||
|
- the change is `risk/high`
|
||||||
|
- it touches host networking, iptables, `ip rule`, Tailscale, or UFW on koala
|
||||||
|
- it needs `sudo` (denied outright to agents on koala)
|
||||||
|
- it writes or rotates a secret
|
||||||
|
- it deletes data, a PVC, or a repo
|
||||||
|
- a finding is real and the fix is a judgement call, not a correction
|
||||||
|
- the diff is too large to review honestly
|
||||||
|
|
||||||
|
Escalation is cheap. A bad merge on a GitOps cluster is not.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Report back, in this order:
|
||||||
|
|
||||||
|
1. **VERDICT** — `MERGED` / `BLOCKED` / `ESCALATED`
|
||||||
|
2. **Findings**, most severe first, each as `path:line — what is wrong — what
|
||||||
|
happens because of it`
|
||||||
|
3. **Flip-test results** — what you broke, and whether the test noticed
|
||||||
|
4. **CI** — run id and conclusion, or "absent"
|
||||||
|
5. **What you did not check**, explicitly
|
||||||
|
|
||||||
|
Never report a clean review you did not perform. "I read the diff and it looked
|
||||||
|
fine" is not a review; naming what you attacked and what survived is.
|
||||||
+23
@@ -357,6 +357,29 @@ ver_lt() {
|
|||||||
[ "$_am" -lt "$_bm" ]
|
[ "$_am" -lt "$_bm" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- agents shipped by the plugin, shadowed by host-local copies -------------
|
||||||
|
#
|
||||||
|
# skills#51: pr-gatekeeper lived ONLY at ~/.claude/agents/pr-gatekeeper.md for
|
||||||
|
# weeks — unversioned, on one host, while doctrine came to depend on it
|
||||||
|
# (local-dev AGENT.md rule 8). Now that the plugin ships agents/, a real file of
|
||||||
|
# the same name in ~/.claude/agents takes precedence over the shipped one and
|
||||||
|
# nothing says so. That is the same silent-drift shape as the `outside` and
|
||||||
|
# `unmanaged` checks above: it works, so nothing complains, while serving
|
||||||
|
# content nobody updates.
|
||||||
|
CLAUDE_AGENTS="$HOME/.claude/agents"
|
||||||
|
if [ -d "$CHECKOUT/agents" ]; then
|
||||||
|
for _shipped in "$CHECKOUT"/agents/*.md; do
|
||||||
|
[ -e "$_shipped" ] || continue
|
||||||
|
_name=$(basename -- "$_shipped")
|
||||||
|
_local="$CLAUDE_AGENTS/$_name"
|
||||||
|
[ -e "$_local" ] || continue
|
||||||
|
if [ -L "$_local" ]; then
|
||||||
|
continue # a link is managed wiring, not a shadow
|
||||||
|
fi
|
||||||
|
add FAIL shadow "$_local is a host-local copy of the plugin's agents/$_name — it wins over the shipped one, so the versioned file is not what runs; delete it"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
if command -v golangci-lint >/dev/null 2>&1 && command -v go >/dev/null 2>&1; then
|
if command -v golangci-lint >/dev/null 2>&1 && command -v go >/dev/null 2>&1; then
|
||||||
lint_go=$(golangci-lint --version 2>/dev/null | sed -n 's/.*built with go\([0-9][0-9.]*\).*/\1/p' | head -1)
|
lint_go=$(golangci-lint --version 2>/dev/null | sed -n 's/.*built with go\([0-9][0-9.]*\).*/\1/p' | head -1)
|
||||||
root_go=$(go version 2>/dev/null | sed -n 's/.*go version go\([0-9][0-9.]*\).*/\1/p' | head -1)
|
root_go=$(go version 2>/dev/null | sed -n 's/.*go version go\([0-9][0-9.]*\).*/\1/p' | head -1)
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
name: document-to-markdown
|
||||||
|
surfaces: [claude-code, library]
|
||||||
|
summary: "Read .docx/.xlsx/.pptx/.epub/.msg and other formats the Read tool cannot open"
|
||||||
|
use_when: "You need the contents of a binary document — Word, Excel, PowerPoint, EPUB, Outlook .msg, a zip of documents — and Read returns nothing useful"
|
||||||
|
description: Convert a binary document to Markdown locally with markitdown. No network, no LLM, safe for confidential material. Says when to use the docmark service instead.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Document → Markdown
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`Read` opens PDFs, images and notebooks natively. It does **not** open `.docx`,
|
||||||
|
`.xlsx`, `.pptx`, `.epub`, `.msg` or `.zip`. This skill converts those locally.
|
||||||
|
|
||||||
|
One command, no install step, no service:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx --from 'markitdown[docx,pdf,pptx,xls,xlsx,outlook]' markitdown FILE > /tmp/out.md
|
||||||
|
```
|
||||||
|
|
||||||
|
First run downloads 38 packages (~10s); afterwards it is cached and instant.
|
||||||
|
|
||||||
|
## When to use this
|
||||||
|
|
||||||
|
You have a file on disk whose contents you need. That is the whole trigger.
|
||||||
|
|
||||||
|
## When NOT to use this — use `docmark` instead
|
||||||
|
|
||||||
|
Three cases, and only three:
|
||||||
|
|
||||||
|
1. **A scanned PDF or an image whose content is pixels, not text.** Extracting
|
||||||
|
that needs a vision model. The estate's only audited path from an image to a
|
||||||
|
model is `docmark`, whose `sovereignty.py` constructs an LLM client *only*
|
||||||
|
when `DOCMARK_VISION_MODEL` matches `koala/*` or `iguana/*`, and silently
|
||||||
|
degrades to metadata-only otherwise. `tests/test_sovereignty.py` is the
|
||||||
|
load-bearing guard. Do not reach for a cloud vision model directly; that is
|
||||||
|
the boundary the gate exists to hold.
|
||||||
|
2. **You are writing an in-cluster service** (something in `dispatch`,
|
||||||
|
`ingestion`, `cobalt-dingo`). Call `docmark` over MCP rather than bundling a
|
||||||
|
converter into each service. Its NetworkPolicy already admits `supervisor`
|
||||||
|
and `dispatch`.
|
||||||
|
3. **The file is larger than ~25 MiB** or you want the input/output caps
|
||||||
|
enforced centrally.
|
||||||
|
|
||||||
|
For a text-bearing document, the service returns **byte-identical output** to
|
||||||
|
the local command — measured 2026-09-24 on the same `.docx`, tables included.
|
||||||
|
A network hop, a bearer token and an ingress for the same bytes is not a
|
||||||
|
trade worth making.
|
||||||
|
|
||||||
|
## Confidential material
|
||||||
|
|
||||||
|
**Conversion is local and involves no LLM.** `markitdown` uses an LLM client
|
||||||
|
only to describe images; with none configured it does not call anything. No
|
||||||
|
bytes leave the machine.
|
||||||
|
|
||||||
|
That makes this safe for client material, unlike anything that routes through a
|
||||||
|
model. Two cautions:
|
||||||
|
|
||||||
|
- **Do not install `markitdown[all]`.** It pulls `az-doc-intel` and
|
||||||
|
`az-content-understanding` — Azure cloud document-intelligence converters.
|
||||||
|
They need explicit endpoint configuration to activate, but the extras list
|
||||||
|
above avoids having them on the path at all.
|
||||||
|
- The output is a file like any other. If the source was confidential, so is
|
||||||
|
`/tmp/out.md` — and the usual rule applies: it must not reach an open-egress
|
||||||
|
sink (ADR-0014).
|
||||||
|
|
||||||
|
## Verify the conversion, do not assume it
|
||||||
|
|
||||||
|
The failure mode is **a successful exit with empty output**. A `.docx` template
|
||||||
|
with no body content converts to zero characters and exits 0; so does a scanned
|
||||||
|
PDF with no text layer. Both look like success.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx --from 'markitdown[docx,pdf,pptx,xls,xlsx,outlook]' markitdown FILE > /tmp/out.md
|
||||||
|
wc -c /tmp/out.md
|
||||||
|
```
|
||||||
|
|
||||||
|
If the byte count is 0 or implausibly small for the document's size, the
|
||||||
|
converter found no text layer — that is case 1 above, and it needs `docmark`
|
||||||
|
with a vision model, not a retry.
|
||||||
|
|
||||||
|
## What it converts
|
||||||
|
|
||||||
|
18 converters with the extras above:
|
||||||
|
|
||||||
|
```
|
||||||
|
Audio, BingSerp, Csv, Docx, Epub, Html, Image, Ipynb, OutlookMsg, Pdf,
|
||||||
|
PlainText, Pptx, Rss, Wikipedia, Xls, Xlsx, YouTube, Zip
|
||||||
|
```
|
||||||
|
|
||||||
|
`docmark` registers 22 — the same set plus `DocxWithOCR`, `PdfWithOCR`,
|
||||||
|
`PptxWithOCR`, `XlsxWithOCR`. **Those four are the entire difference**, and
|
||||||
|
they are exactly the vision path from case 1. Everything else is identical code
|
||||||
|
running in a different place.
|
||||||
|
|
||||||
|
## Reading the output
|
||||||
|
|
||||||
|
Convert to a file and `Read` it, rather than piping into the transcript —
|
||||||
|
a 200-page document produces a lot of Markdown and most of it will not be
|
||||||
|
what you needed. Convert, then grep or read the section you want.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
`docmark` ADR-0013 (`mathias/infra docs/decisions/`) owns the service; this
|
||||||
|
skill is the local path. The split was decided in `mathias/docmark#3` on
|
||||||
|
measurement, not preference: three of docmark's four plausible consumers are
|
||||||
|
better served without it, and the fourth — vision under a sovereignty gate — is
|
||||||
|
why it exists.
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
Local, read-only, single command. Nothing here is handed back mid-task.
|
||||||
|
|
||||||
|
Stop and hand back when:
|
||||||
|
|
||||||
|
- **The output is empty or near-empty.** Do not retry, and do not guess at the
|
||||||
|
contents. Say which file, what the byte count was, and that it needs a vision
|
||||||
|
path — then either use `docmark` with a local vision model, or ask. A
|
||||||
|
confident summary of a document you could not read is the worst possible
|
||||||
|
output of this skill.
|
||||||
|
- **The document is client material and the task would send its contents
|
||||||
|
anywhere** — a commit message, an issue, a PR body, a cloud model. That is
|
||||||
|
ADR-0014's refusal, and it is not this skill's call to make.
|
||||||
|
- **The file is encrypted or password-protected.** markitdown will fail; asking
|
||||||
|
for the password is a human's decision, not a retry.
|
||||||
Executable
+108
@@ -0,0 +1,108 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Tests for the agents/ directory the plugin ships, and doctor's shadow check.
|
||||||
|
#
|
||||||
|
# skills#51: pr-gatekeeper blocked dispatch#75 five times, every block a real
|
||||||
|
# escape verified by execution — and the agent definition lived only at
|
||||||
|
# ~/.claude/agents/pr-gatekeeper.md on one host. Unversioned, unreviewable, one
|
||||||
|
# rm from gone. Doctrine now depends on it (local-dev AGENT.md rule 8), so the
|
||||||
|
# file has to be in the repo.
|
||||||
|
#
|
||||||
|
# These assertions observe FILE CONTENT and doctor's OUTPUT. They cannot and do
|
||||||
|
# not claim the agent reviews well — naming them otherwise would be the exact
|
||||||
|
# defect rule 9 exists for.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
AGENT="$ROOT/agents/pr-gatekeeper.md"
|
||||||
|
DOCTOR="$ROOT/bin/doctor"
|
||||||
|
|
||||||
|
pass=0
|
||||||
|
fail=0
|
||||||
|
WORK=${TMPDIR:-/tmp}/agents_test.$$
|
||||||
|
|
||||||
|
ok() { pass=$((pass + 1)); printf 'ok: %s\n' "$1"; }
|
||||||
|
no() { fail=$((fail + 1)); printf 'FAIL: %s\n' "$1"; }
|
||||||
|
|
||||||
|
# --- the file is in the repo ------------------------------------------------
|
||||||
|
|
||||||
|
if [ -f "$AGENT" ]; then
|
||||||
|
ok 'agents/pr-gatekeeper.md exists in the checkout'
|
||||||
|
else
|
||||||
|
no 'agents/pr-gatekeeper.md exists in the checkout'
|
||||||
|
printf '\n%s passed, %s failed\n' "$pass" "$fail"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- frontmatter is present and well-formed ---------------------------------
|
||||||
|
|
||||||
|
head -1 "$AGENT" | grep -q '^---$' \
|
||||||
|
&& ok 'the file opens with a frontmatter fence' \
|
||||||
|
|| no 'the file opens with a frontmatter fence'
|
||||||
|
|
||||||
|
for field in name description tools; do
|
||||||
|
if sed -n '2,/^---$/p' "$AGENT" | grep -q "^$field:"; then
|
||||||
|
ok "frontmatter declares $field"
|
||||||
|
else
|
||||||
|
no "frontmatter declares $field"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
fm_name=$(sed -n '2,/^---$/p' "$AGENT" | sed -n 's/^name: *//p' | head -1)
|
||||||
|
if [ "$fm_name" = "pr-gatekeeper" ]; then
|
||||||
|
ok 'the frontmatter name equals the filename stem'
|
||||||
|
else
|
||||||
|
no "the frontmatter name equals the filename stem (got '$fm_name')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- the practices that actually found the defects --------------------------
|
||||||
|
#
|
||||||
|
# Each string below is traceable to a specific round of dispatch#75 or to the
|
||||||
|
# incident the agent itself caused. A grep is a weak check — it proves the
|
||||||
|
# phrase is present, not that the procedure is followed — so the test is named
|
||||||
|
# for the phrase, not for the behaviour.
|
||||||
|
|
||||||
|
check_phrase() {
|
||||||
|
if grep -qi -- "$1" "$AGENT"; then
|
||||||
|
ok "the file names: $2"
|
||||||
|
else
|
||||||
|
no "the file names: $2"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_phrase 'verify by execution' 'verify by execution, not by reading the implementation'
|
||||||
|
check_phrase 'flip' 'flip-testing'
|
||||||
|
check_phrase 'read the survivors' 'read the survivors of a differential'
|
||||||
|
check_phrase 'exit code' 'grade a linter on its exit code'
|
||||||
|
check_phrase 'never the buffer' 'a probe prints a count, never the buffer'
|
||||||
|
check_phrase 'claims more than' 'a test name must not claim more than it proves'
|
||||||
|
|
||||||
|
# --- doctor flags a host-local copy that shadows the shipped one ------------
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
rm -rf "$WORK"
|
||||||
|
mkdir -p "$WORK/home/.claude/agents" "$WORK/ck/skills/x" "$WORK/ck/agents"
|
||||||
|
printf 'x\n' > "$WORK/ck/skills/x/SKILL.md"
|
||||||
|
printf -- '---\nname: pr-gatekeeper\n---\n' > "$WORK/ck/agents/pr-gatekeeper.md"
|
||||||
|
}
|
||||||
|
|
||||||
|
setup
|
||||||
|
printf -- '---\nname: pr-gatekeeper\n---\n' > "$WORK/home/.claude/agents/pr-gatekeeper.md"
|
||||||
|
out=$(HOME="$WORK/home" DOCTOR_CHECKOUT="$WORK/ck" sh "$DOCTOR" 2>&1)
|
||||||
|
if printf '%s' "$out" | grep -q 'pr-gatekeeper'; then
|
||||||
|
ok 'doctor reports a host-local agent that shadows a shipped one'
|
||||||
|
else
|
||||||
|
no "doctor reports a host-local agent that shadows a shipped one (got: $out)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
setup
|
||||||
|
out=$(HOME="$WORK/home" DOCTOR_CHECKOUT="$WORK/ck" sh "$DOCTOR" 2>&1)
|
||||||
|
if printf '%s' "$out" | grep -q 'pr-gatekeeper'; then
|
||||||
|
no "doctor is silent when no host-local copy exists (got: $out)"
|
||||||
|
else
|
||||||
|
ok 'doctor is silent when no host-local copy exists'
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$WORK"
|
||||||
|
printf '\n%s passed, %s failed\n' "$pass" "$fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
Reference in New Issue
Block a user