@linchpinagency/skills 0.1.0
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/README.md +337 -0
- package/bin/install.mjs +231 -0
- package/package.json +44 -0
- package/skills/browser-automation/SKILL.md +93 -0
- package/skills/commit-and-release/SKILL.md +135 -0
- package/skills/dependency-updates/SKILL.md +102 -0
- package/skills/design-previews/SKILL.md +118 -0
- package/skills/engagement-types/SKILL.md +108 -0
- package/skills/investigate/SKILL.md +95 -0
- package/skills/project-context/SKILL.md +89 -0
- package/skills/quality-gates/SKILL.md +94 -0
- package/skills/quality-gates/references/toolchain.md +104 -0
- package/skills/safety-hooks/SKILL.md +121 -0
- package/skills/safety-hooks/scripts/check-destructive.sh +80 -0
- package/skills/safety-hooks/scripts/check-edit-boundary.sh +65 -0
- package/skills/support-triage/SKILL.md +103 -0
- package/skills/task-tracking/SKILL.md +243 -0
- package/skills/web-qa/SKILL.md +108 -0
- package/skills/web-qa/references/qa-checklist.md +98 -0
- package/skills/wordpress-blocks/SKILL.md +110 -0
- package/skills/wordpress-blocks/references/block-grammar.md +94 -0
- package/skills/wordpress-blocks/references/core-blocks.md +123 -0
- package/skills/wordpress-blocks/references/patterns-and-parts.md +70 -0
- package/skills/wordpress-blocks/references/recipes/faq.md +56 -0
- package/skills/wordpress-blocks/references/recipes/hero.md +74 -0
- package/skills/wordpress-blocks/references/recipes/pricing-table.md +77 -0
- package/skills/wordpress-blocks/references/tool-contract.md +167 -0
- package/skills/wordpress-blocks/references/validation.md +38 -0
- package/skills/wp-audit/SKILL.md +115 -0
- package/skills/wp-block-conventions/SKILL.md +134 -0
- package/skills/wp-block-conventions/references/block-anatomy.md +175 -0
- package/skills/wp-implementation-choice/SKILL.md +88 -0
- package/skills/wp-local-setup/SKILL.md +262 -0
- package/skills/wp-pressable/SKILL.md +172 -0
- package/skills/wp-studio-cli/SKILL.md +165 -0
- package/skills/write-a-linchpin-skill/SKILL.md +195 -0
- package/skills/write-a-linchpin-skill/references/template.md +83 -0
- package/upstream.json +20 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: project-context
|
|
3
|
+
description: Orient before acting on a Linchpin project — identify the repo and branch, the local environment (Studio, wp-env, LocalWP), the host (Pressable or Cloudflare), the ClickUp space, and the release model, from .linchpin.json, composer.json, package.json, and the git remote. Use when starting work on an unfamiliar repo, before running commands that assume an environment, when a skill's Preflight needs the project's shape, or when something behaves differently than expected. Not for running the checks themselves.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Project context
|
|
8
|
+
|
|
9
|
+
Our projects are not uniform: WordPress plugin repos, `wp-content`-shaped site repos, and
|
|
10
|
+
Cloudflare Workers services all live under the same conventions but expose different tools,
|
|
11
|
+
environments, and hosts. **Guessing wrong is the most common way an agent wastes a session** —
|
|
12
|
+
running `wp-env` on a Studio project, or looking for `phpcs.xml.dist` in a Workers repo.
|
|
13
|
+
|
|
14
|
+
This is the one place that answers "what am I working in?". Other skills reference it from
|
|
15
|
+
their Preflight instead of each re-deriving it.
|
|
16
|
+
|
|
17
|
+
## When to use
|
|
18
|
+
|
|
19
|
+
- Starting work on a repo you haven't touched this session.
|
|
20
|
+
- Before any command that assumes an environment, host, or toolchain.
|
|
21
|
+
- A skill's Preflight needs the project's shape.
|
|
22
|
+
- Something behaves unexpectedly and the environment is a suspect.
|
|
23
|
+
|
|
24
|
+
**Not this skill:** running lint or tests — [`quality-gates`](../quality-gates/SKILL.md).
|
|
25
|
+
Operating the local site — [`wp-studio-cli`](../wp-studio-cli/SKILL.md). Operating the
|
|
26
|
+
server — [`wp-pressable`](../wp-pressable/SKILL.md).
|
|
27
|
+
|
|
28
|
+
## Owns
|
|
29
|
+
|
|
30
|
+
Canonical for: what to read to identify a project, and what each signal means. Every other
|
|
31
|
+
skill's Preflight should link here rather than restate the detection table.
|
|
32
|
+
|
|
33
|
+
## The orientation pass
|
|
34
|
+
|
|
35
|
+
Cheap, read-only, and worth doing once per session rather than per command:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
git rev-parse --show-toplevel && git branch --show-current && git remote get-url origin
|
|
39
|
+
ls .linchpin.json composer.json package.json phpcs.xml.dist .wp-env.json wrangler.toml 2>/dev/null
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then read what exists:
|
|
43
|
+
|
|
44
|
+
| Signal | What it tells you |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `.linchpin.json` | House metadata — plugin slug, declared local environments and their paths, preferred agent |
|
|
47
|
+
| Repo root has `themes/` + `plugins/` and no core | A **site repo** — the repo *is* `wp-content` ([`wp-local-setup`](../wp-local-setup/SKILL.md)) |
|
|
48
|
+
| A single plugin bootstrap file + `includes/` | A **plugin/product repo** (Mantle, linchpin-blocks) |
|
|
49
|
+
| `wrangler.toml` | A **Cloudflare Workers** service — no PHP toolchain at all |
|
|
50
|
+
| Site registered in Studio | Local env is **Studio** — the default ([`wp-studio-cli`](../wp-studio-cli/SKILL.md)) |
|
|
51
|
+
| `.wp-env.json` / `.linchpin.json` environments | Legacy wp-env or LocalWP; predates the Studio switch — confirm before using |
|
|
52
|
+
| `composer.json` scripts, `phpcs.xml.dist`, `phpstan.neon` | Which PHP gates exist ([`quality-gates`](../quality-gates/SKILL.md)) |
|
|
53
|
+
| Nested `blocks/package.json`, `themes/*/package.json` | Builds run **in that workspace**, not the root |
|
|
54
|
+
| `release-please-config.json` | Versions and `CHANGELOG.md` are machine-owned ([`commit-and-release`](../commit-and-release/SKILL.md)) |
|
|
55
|
+
| `commitlint.config.js` | This repo's allowed commit types — they differ between repos |
|
|
56
|
+
| Deploy workflows referencing Pressable | Hosted on Pressable ([`wp-pressable`](../wp-pressable/SKILL.md)) |
|
|
57
|
+
| Git remote name | Infers the ClickUp space ([`task-tracking`](../task-tracking/SKILL.md)) |
|
|
58
|
+
|
|
59
|
+
## What to report
|
|
60
|
+
|
|
61
|
+
State the shape in one or two lines before doing the work, so the user can correct a wrong
|
|
62
|
+
assumption before it costs anything:
|
|
63
|
+
|
|
64
|
+
> `linchpin.com` on `issue/LINCHPIN-5210` — site repo (repo is `wp-content`), Studio local,
|
|
65
|
+
> Pressable hosted, release-please. PHP gates: phpcs + phpstan. Nested builds under
|
|
66
|
+
> `themes/linchpin` and `plugins/linchpin-functionality`.
|
|
67
|
+
|
|
68
|
+
Then carry it for the session. Re-check only when the branch changes or something surprises
|
|
69
|
+
you — this is orientation, not a per-command ritual.
|
|
70
|
+
|
|
71
|
+
## Guardrails
|
|
72
|
+
|
|
73
|
+
- **Never assume the environment from a config file's presence alone.** A leftover
|
|
74
|
+
`.wp-env.json` in a Studio project is history, not intent — when two exist, ask.
|
|
75
|
+
- **Never infer the host from the repo name.** Read the deploy workflows.
|
|
76
|
+
- **Never run an environment-changing command** (starting containers, creating sites) as part
|
|
77
|
+
of orientation. This pass is read-only.
|
|
78
|
+
- **Never carry stale context across a branch switch** — base branch, gates, and task key can
|
|
79
|
+
all change.
|
|
80
|
+
- If a signal is missing and the answer matters, say what's missing rather than picking the
|
|
81
|
+
most likely option silently.
|
|
82
|
+
|
|
83
|
+
## Done
|
|
84
|
+
|
|
85
|
+
- [ ] Repo root, branch, and remote identified.
|
|
86
|
+
- [ ] Project shape known: site repo, plugin/product repo, or Workers service.
|
|
87
|
+
- [ ] Local environment identified, and ambiguity resolved with the user rather than guessed.
|
|
88
|
+
- [ ] Host and release model known before anything is shipped or deployed.
|
|
89
|
+
- [ ] The shape was stated back to the user in a line or two.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: quality-gates
|
|
3
|
+
description: Run a Linchpin project's own lint, coding-standards, static-analysis, and test gates before committing or opening a PR — detecting the toolchain from composer.json, package.json, phpcs.xml.dist and lint-staged rather than assuming it. Use when preparing to commit, when asked "is this ready to commit/ship", when CI lint or PHPCS is failing, when a pre-commit hook blocks you, or when a repo is missing the standard lint scripts. Not for writing the commit message — use `commit-and-release`.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Quality gates
|
|
8
|
+
|
|
9
|
+
Every Linchpin repo already declares how it wants to be checked — in `composer.json`
|
|
10
|
+
scripts, `package.json` scripts, `phpcs.xml.dist`, `lint-staged.config.js`. Your job is to
|
|
11
|
+
**find those declarations and run them**, not to invent commands. Getting this right is
|
|
12
|
+
what makes the difference between a clean PR and a red CI run.
|
|
13
|
+
|
|
14
|
+
The gates are the same ones CI runs, so passing here means passing there.
|
|
15
|
+
|
|
16
|
+
## When to use
|
|
17
|
+
|
|
18
|
+
- About to commit or open a PR.
|
|
19
|
+
- The user asks whether a change is ready to ship.
|
|
20
|
+
- CI lint / PHPCS / PHPStan failed and you need to reproduce and fix it locally.
|
|
21
|
+
- A husky pre-commit hook is blocking a commit.
|
|
22
|
+
- A repo is missing the house lint scripts and should get them.
|
|
23
|
+
|
|
24
|
+
**Not this skill:** the commit message, branch, or release — [`commit-and-release`](../commit-and-release/SKILL.md).
|
|
25
|
+
Upgrading packages — [`dependency-updates`](../dependency-updates/SKILL.md).
|
|
26
|
+
|
|
27
|
+
## Owns
|
|
28
|
+
|
|
29
|
+
Canonical for: which gates exist, how to detect them, the order they run in, and how to fix
|
|
30
|
+
failures without suppressing them. Skills that end in a commit defer here for verification.
|
|
31
|
+
|
|
32
|
+
## Preflight — detect, never assume
|
|
33
|
+
|
|
34
|
+
Read these before running anything. Our repos genuinely differ: PHP-heavy plugin repos,
|
|
35
|
+
`wp-content`-shaped site repos with nested builds, and JS/TS Workers projects all live under
|
|
36
|
+
the same conventions but expose different gates.
|
|
37
|
+
|
|
38
|
+
| Look for | Tells you | If missing |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `composer.json` → `scripts.lint` | The canonical PHP gate: `composer run lint` | Run the individual tools below that do exist |
|
|
41
|
+
| `phpcs.xml.dist` or `phpcs.xml` | Coding standards are configured → PHPCS applies | **Skip PHPCS and say so** — never invent a standard |
|
|
42
|
+
| `phpstan.neon(.dist)` | Static analysis applies | Skip; don't add it unprompted |
|
|
43
|
+
| `.php-cs-fixer.dist.php` | PHP-CS-Fixer applies (`composer run fixer`) | Skip |
|
|
44
|
+
| `package.json` → `scripts` | JS/CSS gates (`lint:js`, `lint:css`, `format`, `lint:check`) | Fall back to `eslint`/`prettier` only if configured |
|
|
45
|
+
| `lint-staged.config.js` + `.husky/` | Pre-commit is already wired — **mirror those exact commands** | Run the scripts directly |
|
|
46
|
+
| Nested `package.json` (e.g. `themes/*`, `plugins/*`, `src/`) | Gates run **in that workspace**, not the root | Root only |
|
|
47
|
+
| `.linchpin.json` | Project metadata and local environments | Not every repo has one |
|
|
48
|
+
|
|
49
|
+
Full command matrix: [`references/toolchain.md`](references/toolchain.md).
|
|
50
|
+
|
|
51
|
+
## Procedure
|
|
52
|
+
|
|
53
|
+
1. **Scope to what changed.** Staged: `git diff --name-only --cached --diff-filter=ACMR`.
|
|
54
|
+
Whole branch: diff against the merge base with `main`. Split into PHP, JS/TS, CSS/SCSS,
|
|
55
|
+
and config buckets. → You can name every changed file and which gate covers it.
|
|
56
|
+
2. **PHP gate** (any `.php` changed). Prefer `composer run lint`. If absent, run only the
|
|
57
|
+
tools whose config exists: `composer run phpcs`, `composer run phpstan`,
|
|
58
|
+
`composer run fixer:test`. → Each command exits 0, or you have the exact violations.
|
|
59
|
+
3. **JS / CSS gate** (any `.js`/`.ts`/`.css`/`.scss` changed). Prefer `npm run lint:check`
|
|
60
|
+
when defined; otherwise `npm run lint:js` and `npm run lint:css`. Run them in the
|
|
61
|
+
workspace that owns the file. → Exit 0 or a concrete rule violation list.
|
|
62
|
+
4. **Tests** when the change touches covered code: `composer run phpunit`, `npm run test:unit`.
|
|
63
|
+
E2E (`npm run test:e2e`) only when asked or when the change is UI-facing — it needs a
|
|
64
|
+
running environment. → Green, or a named failing test.
|
|
65
|
+
5. **Fix, don't silence.** Auto-fixers first (`composer run phpcbf`, `composer run fixer`,
|
|
66
|
+
`npm run format`), then re-run the gate; hand-fix what remains. → Gate passes with the
|
|
67
|
+
fix in the code, not in the config.
|
|
68
|
+
6. **Report gaps, then hand off.** State which gates ran, which were skipped and why. If the
|
|
69
|
+
repo lacks a house script, propose it (see `references/toolchain.md`) and add it **only
|
|
70
|
+
with approval**. → Then go to [`commit-and-release`](../commit-and-release/SKILL.md).
|
|
71
|
+
|
|
72
|
+
## Guardrails
|
|
73
|
+
|
|
74
|
+
- **Never** commit with `--no-verify`. The hook is the gate; if it blocks you, fix the code.
|
|
75
|
+
- **Never** silence a violation to make a gate pass — no widening `phpcs.xml.dist` excludes,
|
|
76
|
+
no new `phpcs:ignore` / `phpcs:disable` / `eslint-disable` / `@phpstan-ignore` without a
|
|
77
|
+
stated reason the user accepted, no editing a PHPStan baseline to hide a new error.
|
|
78
|
+
- **Never** reformat files the change didn't touch. A formatting-only diff across the repo
|
|
79
|
+
buries the actual change and blows up review.
|
|
80
|
+
- **Never** run `composer update` or `npm update` to fix a lint failure — that's a
|
|
81
|
+
dependency change ([`dependency-updates`](../dependency-updates/SKILL.md)), not a fix.
|
|
82
|
+
- **Never** commit `vendor/`, `node_modules/`, or build output unless the repo already
|
|
83
|
+
tracks it — check `.gitignore` and `.distignore` first.
|
|
84
|
+
- If a tool can't run (not installed, no config, requires Docker that isn't up), **say so
|
|
85
|
+
explicitly**. A silently skipped gate reads as a passing gate.
|
|
86
|
+
|
|
87
|
+
## Done
|
|
88
|
+
|
|
89
|
+
- [ ] Every changed file is covered by a gate that ran, or is explicitly reported as uncovered.
|
|
90
|
+
- [ ] PHP gate passed (or is correctly not applicable — no `phpcs.xml.dist`, no PHP changed).
|
|
91
|
+
- [ ] JS/CSS gate passed in the owning workspace (or correctly not applicable).
|
|
92
|
+
- [ ] Tests run for touched, covered code.
|
|
93
|
+
- [ ] No suppressions, config widenings, or `--no-verify` were used to get green.
|
|
94
|
+
- [ ] Skipped gates and missing house scripts are named in the report.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Toolchain reference
|
|
2
|
+
|
|
3
|
+
The commands Linchpin projects actually expose, and what to do when they're missing.
|
|
4
|
+
**Run what the repo declares.** This file is a lookup table, not a script to run top to
|
|
5
|
+
bottom.
|
|
6
|
+
|
|
7
|
+
## PHP
|
|
8
|
+
|
|
9
|
+
| Intent | Command | Requires |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Everything | `composer run lint` | `scripts.lint` in `composer.json` |
|
|
12
|
+
| Syntax only | `composer run php-lint` | `php-parallel-lint` |
|
|
13
|
+
| Coding standards | `composer run phpcs` (or `phpcs --standard=phpcs.xml.dist`) | `phpcs.xml.dist` |
|
|
14
|
+
| Auto-fix standards | `composer run phpcbf` | same |
|
|
15
|
+
| Formatting check | `composer run fixer:test` | `.php-cs-fixer.dist.php` |
|
|
16
|
+
| Auto-fix formatting | `composer run fixer` | same |
|
|
17
|
+
| Static analysis | `composer run phpstan` | `phpstan.neon(.dist)` |
|
|
18
|
+
| Unit tests | `composer run phpunit` | `phpunit.xml(.dist)` |
|
|
19
|
+
| Lint + tests | `composer run test` | both |
|
|
20
|
+
|
|
21
|
+
The house `scripts.lint` composes `php-lint` → `phpcs` → `fixer:test` → `phpstan`, in that
|
|
22
|
+
order — cheapest failure first. Reproduce that order when running tools individually.
|
|
23
|
+
|
|
24
|
+
**PHPCS with no `phpcs.xml.dist`:** skip it and report it. Do not fall back to a global
|
|
25
|
+
standard — the result won't match CI, and a WordPress-standard run against a non-WordPress
|
|
26
|
+
codebase produces hundreds of meaningless violations.
|
|
27
|
+
|
|
28
|
+
**PHPStan baselines:** `phpstan-baseline.neon` records accepted debt. New errors go in the
|
|
29
|
+
code's fix, never in the baseline, unless the user explicitly asks to re-baseline.
|
|
30
|
+
|
|
31
|
+
## JavaScript / CSS
|
|
32
|
+
|
|
33
|
+
| Intent | Command | Notes |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| Pre-commit equivalent | `npm run lint:check` | Where defined, this is `format` + `lint:js` |
|
|
36
|
+
| JS/TS | `npm run lint:js` | `wp-scripts lint-js` on WordPress projects |
|
|
37
|
+
| CSS/SCSS | `npm run lint:css` | `wp-scripts lint-style` |
|
|
38
|
+
| Format | `npm run format` | Prettier via `wp-scripts format` |
|
|
39
|
+
| Unit tests | `npm run test:unit` | |
|
|
40
|
+
| E2E | `npm run test:e2e` | Playwright; needs a running environment |
|
|
41
|
+
| Build | `npm run build` | Verify the change compiles before pushing |
|
|
42
|
+
|
|
43
|
+
**Nested workspaces.** Site repos build per package — e.g. a theme and a functionality
|
|
44
|
+
plugin each with their own `package.json`, sometimes an inner `src/`. Root scripts often
|
|
45
|
+
delegate (`npm run build --prefix themes/<theme>`). Lint a file in the workspace that owns
|
|
46
|
+
it; running the root script may silently skip it.
|
|
47
|
+
|
|
48
|
+
**Non-WordPress projects** (Workers, Astro, API services) use `eslint.config.mjs` +
|
|
49
|
+
`.prettierrc` directly. Same rule: run the declared `package.json` script.
|
|
50
|
+
|
|
51
|
+
## Pre-commit hooks
|
|
52
|
+
|
|
53
|
+
`husky` + `lint-staged` are the house pre-commit setup (`"prepare": "husky"`).
|
|
54
|
+
|
|
55
|
+
- `lint-staged.config.js` is the source of truth for what runs on commit. When it exists,
|
|
56
|
+
**run the same commands it would run** — that's exactly what the hook will do.
|
|
57
|
+
- Config files are often excluded from linting there. Don't "fix" a config file the
|
|
58
|
+
lint-staged rules deliberately skip.
|
|
59
|
+
- A blocked commit means the gate found something real. Fix the code; never `--no-verify`.
|
|
60
|
+
|
|
61
|
+
## The house script set
|
|
62
|
+
|
|
63
|
+
When a repo is missing these, propose adding them — matching the repo's existing tooling,
|
|
64
|
+
and only with the user's approval:
|
|
65
|
+
|
|
66
|
+
```jsonc
|
|
67
|
+
// package.json
|
|
68
|
+
"scripts": {
|
|
69
|
+
"format": "wp-scripts format",
|
|
70
|
+
"lint:js": "wp-scripts lint-js",
|
|
71
|
+
"lint:css": "wp-scripts lint-style",
|
|
72
|
+
"lint:check": "npm run format && npm run lint:js",
|
|
73
|
+
"prepare": "husky"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```jsonc
|
|
78
|
+
// composer.json
|
|
79
|
+
"scripts": {
|
|
80
|
+
"lint": ["@php-lint", "@phpcs", "@fixer:test", "@phpstan"],
|
|
81
|
+
"phpcs": "phpcs --standard=phpcs.xml.dist",
|
|
82
|
+
"phpcbf": "phpcbf --standard=phpcs.xml.dist",
|
|
83
|
+
"phpstan": "phpstan analyse"
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Adding a script that references a tool the repo doesn't require is worse than having no
|
|
88
|
+
script — it fails for everyone. Check `require-dev` first, and add the dependency in the
|
|
89
|
+
same change or not at all.
|
|
90
|
+
|
|
91
|
+
## Local environments
|
|
92
|
+
|
|
93
|
+
**WordPress Studio is the default** for new Linchpin work. Repos still carrying wp-env or
|
|
94
|
+
LocalWP config predate that switch — those files are not evidence that the project is being
|
|
95
|
+
run that way today. Detect, and when both exist, ask which the user is on:
|
|
96
|
+
|
|
97
|
+
| Signal | Environment | Start |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| Site registered in Studio | **WordPress Studio** (default) | [`wp-studio-cli`](../../wp-studio-cli/SKILL.md) — MCP `site_start`, else `studio site start` |
|
|
100
|
+
| `.wp-env.json` | wp-env (Docker) — legacy | `npx @wordpress/env start` |
|
|
101
|
+
| `.linchpin.json` → `wordpress.environments` | LocalWP or a declared path — legacy | Already running; use the path |
|
|
102
|
+
|
|
103
|
+
Unit tests and lint don't need any of this. Only reach for an environment when running E2E
|
|
104
|
+
or a check that boots WordPress.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: safety-hooks
|
|
3
|
+
description: Turn on enforced guardrails for risky work — a PreToolUse hook that makes destructive commands (wp db drop, search-replace without --dry-run, DROP TABLE, git push --force, rm -rf) require confirmation, and an optional edit boundary that blocks writes outside one directory. Use when touching production or a client database, debugging a live site, working in an unfamiliar repo, or when asked for careful mode, safety mode, or to lock down edits. Claude Code only.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Safety hooks
|
|
8
|
+
|
|
9
|
+
Every other skill in this library *asks* an agent not to do something dangerous. This one
|
|
10
|
+
**enforces it**, by registering a `PreToolUse` hook that inspects each command before it runs
|
|
11
|
+
and makes the destructive ones require confirmation.
|
|
12
|
+
|
|
13
|
+
That matters most where our work is riskiest: client databases, production WordPress, and
|
|
14
|
+
`wp search-replace` — a single command that rewrites a live database in place.
|
|
15
|
+
|
|
16
|
+
## When to use
|
|
17
|
+
|
|
18
|
+
- About to touch production or staging, or any client database.
|
|
19
|
+
- Debugging a live site, or working in a repo you don't know well.
|
|
20
|
+
- Handing a session to someone less familiar with the project.
|
|
21
|
+
- Asked for "careful mode", "safety mode", or "only edit this folder".
|
|
22
|
+
|
|
23
|
+
**Not this skill:** the prose rules about what's safe on a server —
|
|
24
|
+
[`wp-pressable`](../wp-pressable/SKILL.md). Pre-commit checks —
|
|
25
|
+
[`quality-gates`](../quality-gates/SKILL.md).
|
|
26
|
+
|
|
27
|
+
## Owns
|
|
28
|
+
|
|
29
|
+
Canonical for: the enforced guardrail mechanism, the destructive-command list, and the edit
|
|
30
|
+
boundary. Other skills state judgment rules; this one blocks actions.
|
|
31
|
+
|
|
32
|
+
## Portability exception
|
|
33
|
+
|
|
34
|
+
This is the **one deliberately Claude-Code-only skill** in the library. Hooks are a Claude
|
|
35
|
+
Code feature; Copilot, Codex, and Cursor ignore the `hooks` frontmatter and the skill
|
|
36
|
+
degrades to documentation. That trade is worth making because the protection is real, and
|
|
37
|
+
because nothing else here depends on it — see
|
|
38
|
+
[`write-a-linchpin-skill`](../write-a-linchpin-skill/SKILL.md).
|
|
39
|
+
|
|
40
|
+
## Turning it on
|
|
41
|
+
|
|
42
|
+
**For the session** — invoking this skill registers the hooks below; they last until the
|
|
43
|
+
conversation ends.
|
|
44
|
+
|
|
45
|
+
**Permanently** — copy the same entries into `~/.claude/settings.json` (global) or the
|
|
46
|
+
project's `.claude/settings.json`:
|
|
47
|
+
|
|
48
|
+
```jsonc
|
|
49
|
+
{
|
|
50
|
+
"hooks": {
|
|
51
|
+
"PreToolUse": [
|
|
52
|
+
{
|
|
53
|
+
"matcher": "Bash",
|
|
54
|
+
"hooks": [{
|
|
55
|
+
"type": "command",
|
|
56
|
+
"command": "bash -c 'f=.claude/skills/safety-hooks/scripts/check-destructive.sh; [ -f \"$f\" ] || f=$HOME/.claude/skills/safety-hooks/scripts/check-destructive.sh; bash \"$f\"'"
|
|
57
|
+
}]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"matcher": "Edit|Write",
|
|
61
|
+
"hooks": [{
|
|
62
|
+
"type": "command",
|
|
63
|
+
"command": "bash -c 'f=.claude/skills/safety-hooks/scripts/check-edit-boundary.sh; [ -f \"$f\" ] || f=$HOME/.claude/skills/safety-hooks/scripts/check-edit-boundary.sh; bash \"$f\"'"
|
|
64
|
+
}]
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Setting an edit boundary
|
|
72
|
+
|
|
73
|
+
The boundary is opt-in and off by default. To scope edits to one directory — useful while
|
|
74
|
+
debugging, so unrelated code can't be "helpfully" fixed:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
mkdir -p "${LINCHPIN_SAFETY_DIR:-$HOME/.claude/.linchpin-safety}"
|
|
78
|
+
cd <directory-to-restrict-to> && pwd > "${LINCHPIN_SAFETY_DIR:-$HOME/.claude/.linchpin-safety}/edit-boundary.txt"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
To clear it:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
rm -f "${LINCHPIN_SAFETY_DIR:-$HOME/.claude/.linchpin-safety}/edit-boundary.txt"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Ask the user which directory rather than guessing, and confirm the resolved absolute path
|
|
88
|
+
back to them.
|
|
89
|
+
|
|
90
|
+
## What triggers a confirmation
|
|
91
|
+
|
|
92
|
+
| Area | Patterns |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| WordPress | `wp db drop`/`reset`/`import`, `wp search-replace` without `--dry-run`, `wp site empty`, `wp post/user/term/comment delete`, `wp plugin uninstall`, changing `home`/`siteurl` |
|
|
95
|
+
| Databases | `DROP TABLE`/`DATABASE`, `TRUNCATE`, `DELETE FROM` with no `WHERE` |
|
|
96
|
+
| Git | `push --force`, `reset --hard`, `checkout .`/`restore .`, `clean -fd`, `branch -D` |
|
|
97
|
+
| Filesystem & infra | `rm -rf`, `kubectl delete`, `docker rm -f`, `docker system prune`, `ssh` running `wp`/`mysql` remotely |
|
|
98
|
+
|
|
99
|
+
Allowed without prompting: removing `node_modules`, `vendor`, `dist`, `build`, `.next`,
|
|
100
|
+
`.turbo`, `.cache`, `coverage`, `__pycache__`, and any `search-replace` carrying `--dry-run`.
|
|
101
|
+
|
|
102
|
+
## Guardrails
|
|
103
|
+
|
|
104
|
+
- **This is a speed bump, not a sandbox.** It fails *open* — if `python3` is missing or
|
|
105
|
+
parsing fails, commands proceed. Bash can still write anywhere via `sed` or redirects.
|
|
106
|
+
Never describe it to a client or teammate as a security control.
|
|
107
|
+
- **Never disable the hook to get a command through.** Confirm the command deliberately, or
|
|
108
|
+
change the command.
|
|
109
|
+
- **Never widen the safe-exception list to silence a prompt** you found annoying — that list
|
|
110
|
+
is why the prompts stay meaningful.
|
|
111
|
+
- **Never set an edit boundary without telling the user** what it is; a blocked edit is
|
|
112
|
+
confusing when the reason is invisible.
|
|
113
|
+
- A confirmation prompt is a moment to re-read the command, especially the environment it
|
|
114
|
+
points at. Approving reflexively is the failure this exists to prevent.
|
|
115
|
+
|
|
116
|
+
## Done
|
|
117
|
+
|
|
118
|
+
- [ ] Hooks registered — session-scoped, or written into settings for permanence.
|
|
119
|
+
- [ ] The user knows which protections are active and how to clear them.
|
|
120
|
+
- [ ] Edit boundary, if set, was chosen by the user and echoed back as an absolute path.
|
|
121
|
+
- [ ] No hook was bypassed or weakened to let a specific command through.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse hook for Bash: warn before destructive commands.
|
|
3
|
+
#
|
|
4
|
+
# Reads the tool-call JSON on stdin and, when the command matches a destructive
|
|
5
|
+
# pattern, returns permissionDecision "ask" so the user confirms before it runs.
|
|
6
|
+
# Anything else produces no output, which lets the command proceed normally.
|
|
7
|
+
#
|
|
8
|
+
# Fails OPEN by design: this is a safety net, not a security boundary. If parsing
|
|
9
|
+
# breaks or python3 is missing, work continues rather than the session wedging.
|
|
10
|
+
# Bash can always modify files another way (sed, redirects), so treat this as a
|
|
11
|
+
# speed bump on autopilot, not a sandbox.
|
|
12
|
+
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
|
|
15
|
+
INPUT=$(cat 2>/dev/null || true)
|
|
16
|
+
[ -z "$INPUT" ] && exit 0
|
|
17
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
18
|
+
|
|
19
|
+
CMD=$(printf '%s' "$INPUT" | python3 -c '
|
|
20
|
+
import json, sys
|
|
21
|
+
try:
|
|
22
|
+
data = json.load(sys.stdin)
|
|
23
|
+
except Exception:
|
|
24
|
+
sys.exit(0)
|
|
25
|
+
print((data.get("tool_input") or {}).get("command", ""))
|
|
26
|
+
' 2>/dev/null || true)
|
|
27
|
+
|
|
28
|
+
[ -z "$CMD" ] && exit 0
|
|
29
|
+
|
|
30
|
+
# Build artifacts and dependency directories are safe to blow away.
|
|
31
|
+
if printf '%s' "$CMD" | grep -qE 'rm +(-[a-zA-Z]+ +)*(node_modules|vendor|dist|build|build-tools|\.next|\.turbo|\.cache|coverage|__pycache__)/?( |$)'; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
# An explicit dry run is the safe form of search-replace.
|
|
35
|
+
if printf '%s' "$CMD" | grep -q 'search-replace' && printf '%s' "$CMD" | grep -q -- '--dry-run'; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
REASON=""
|
|
40
|
+
match() { printf '%s' "$CMD" | grep -qiE "$1"; }
|
|
41
|
+
|
|
42
|
+
# --- WordPress / WP-CLI ------------------------------------------------------
|
|
43
|
+
if match 'wp +db +(drop|reset)'; then REASON="\`wp db drop/reset\` destroys the entire database."
|
|
44
|
+
elif match 'wp +db +import'; then REASON="\`wp db import\` overwrites the current database."
|
|
45
|
+
elif match 'wp +search-replace'; then REASON="\`wp search-replace\` rewrites the database in place. Run it with --dry-run first."
|
|
46
|
+
elif match 'wp +site +empty'; then REASON="\`wp site empty\` deletes all site content."
|
|
47
|
+
elif match 'wp +(post|user|term|comment) +delete'; then REASON="This deletes content permanently, especially with --force."
|
|
48
|
+
elif match 'wp +plugin +uninstall'; then REASON="\`wp plugin uninstall\` removes the plugin and its data."
|
|
49
|
+
elif match 'wp +option +(delete|update) +(home|siteurl)'; then REASON="Changing home/siteurl can take the site offline."
|
|
50
|
+
# --- Databases ---------------------------------------------------------------
|
|
51
|
+
elif match 'drop +(table|database)'; then REASON="DROP removes the table or database and its data."
|
|
52
|
+
elif match 'truncate +(table +)?[a-z_]'; then REASON="TRUNCATE empties the table irreversibly."
|
|
53
|
+
elif match 'delete +from +[a-z_]+ *(;|$)'; then REASON="DELETE FROM with no WHERE clause removes every row."
|
|
54
|
+
# --- Git ---------------------------------------------------------------------
|
|
55
|
+
elif match 'git +push +.*(--force|-f)( |$)'; then REASON="Force-push rewrites remote history and can destroy others' commits. Prefer --force-with-lease."
|
|
56
|
+
elif match 'git +reset +--hard'; then REASON="\`git reset --hard\` discards uncommitted work permanently."
|
|
57
|
+
elif match 'git +(checkout|restore) +\.( |$)';then REASON="This discards all uncommitted changes in the working tree."
|
|
58
|
+
elif match 'git +clean +-[a-z]*[fd]'; then REASON="\`git clean\` deletes untracked files, including ones never committed."
|
|
59
|
+
elif match 'git +branch +-D'; then REASON="Force-deleting a branch can lose unmerged commits."
|
|
60
|
+
# --- Filesystem / infra ------------------------------------------------------
|
|
61
|
+
elif match 'rm +(-[a-zA-Z]+ +)*-?[a-zA-Z]*r[a-zA-Z]*f|rm +-rf|rm +-fr'; then REASON="Recursive force delete."
|
|
62
|
+
elif match 'kubectl +delete'; then REASON="This removes live Kubernetes resources."
|
|
63
|
+
elif match 'docker +(rm +-f|system +prune)'; then REASON="This removes containers, images, or volumes."
|
|
64
|
+
elif match 'ssh .*(wp |mysql )'; then REASON="This runs a data-affecting command on a remote server."
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
[ -z "$REASON" ] && exit 0
|
|
68
|
+
|
|
69
|
+
python3 -c '
|
|
70
|
+
import json, sys
|
|
71
|
+
reason = sys.argv[1]
|
|
72
|
+
print(json.dumps({
|
|
73
|
+
"hookSpecificOutput": {
|
|
74
|
+
"hookEventName": "PreToolUse",
|
|
75
|
+
"permissionDecision": "ask",
|
|
76
|
+
"permissionDecisionReason": "Destructive command: " + reason + " Confirm you intend to run this, and that it is pointed at the environment you think it is."
|
|
77
|
+
}
|
|
78
|
+
}))
|
|
79
|
+
' "$REASON"
|
|
80
|
+
exit 0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PreToolUse hook for Edit/Write: block edits outside an allowed directory.
|
|
3
|
+
#
|
|
4
|
+
# Reads the tool-call JSON on stdin and denies the operation when the target file
|
|
5
|
+
# is outside the boundary recorded in the state file. With no state file, every
|
|
6
|
+
# edit is allowed — the boundary is opt-in per session.
|
|
7
|
+
#
|
|
8
|
+
# State file: $LINCHPIN_SAFETY_DIR/edit-boundary.txt (default ~/.claude/.linchpin-safety/)
|
|
9
|
+
#
|
|
10
|
+
# Fails OPEN: if parsing breaks, the edit proceeds. This prevents accidental edits
|
|
11
|
+
# to unrelated code; it is not a security boundary, since Bash can still write
|
|
12
|
+
# anywhere.
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
STATE_DIR="${LINCHPIN_SAFETY_DIR:-$HOME/.claude/.linchpin-safety}"
|
|
17
|
+
BOUNDARY_FILE="$STATE_DIR/edit-boundary.txt"
|
|
18
|
+
|
|
19
|
+
[ -f "$BOUNDARY_FILE" ] || exit 0
|
|
20
|
+
BOUNDARY=$(cat "$BOUNDARY_FILE" 2>/dev/null || true)
|
|
21
|
+
[ -z "$BOUNDARY" ] && exit 0
|
|
22
|
+
# Trailing slash keeps /src from matching /src-old.
|
|
23
|
+
BOUNDARY="${BOUNDARY%/}/"
|
|
24
|
+
|
|
25
|
+
INPUT=$(cat 2>/dev/null || true)
|
|
26
|
+
[ -z "$INPUT" ] && exit 0
|
|
27
|
+
command -v python3 >/dev/null 2>&1 || exit 0
|
|
28
|
+
|
|
29
|
+
FILE_PATH=$(printf '%s' "$INPUT" | python3 -c '
|
|
30
|
+
import json, sys
|
|
31
|
+
try:
|
|
32
|
+
data = json.load(sys.stdin)
|
|
33
|
+
except Exception:
|
|
34
|
+
sys.exit(0)
|
|
35
|
+
print((data.get("tool_input") or {}).get("file_path", ""))
|
|
36
|
+
' 2>/dev/null || true)
|
|
37
|
+
|
|
38
|
+
[ -z "$FILE_PATH" ] && exit 0
|
|
39
|
+
|
|
40
|
+
# Resolve relative paths against the working directory so the comparison is fair.
|
|
41
|
+
case "$FILE_PATH" in
|
|
42
|
+
/*) ABS="$FILE_PATH" ;;
|
|
43
|
+
*) ABS="$PWD/$FILE_PATH" ;;
|
|
44
|
+
esac
|
|
45
|
+
|
|
46
|
+
case "$ABS" in
|
|
47
|
+
"$BOUNDARY"*) exit 0 ;;
|
|
48
|
+
esac
|
|
49
|
+
|
|
50
|
+
python3 -c '
|
|
51
|
+
import json, sys
|
|
52
|
+
path, boundary = sys.argv[1], sys.argv[2]
|
|
53
|
+
print(json.dumps({
|
|
54
|
+
"hookSpecificOutput": {
|
|
55
|
+
"hookEventName": "PreToolUse",
|
|
56
|
+
"permissionDecision": "deny",
|
|
57
|
+
"permissionDecisionReason": (
|
|
58
|
+
"Edit boundary active: edits are restricted to " + boundary +
|
|
59
|
+
" but this targets " + path +
|
|
60
|
+
". Run /safety-hooks to change or clear the boundary if this edit is intended."
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
}))
|
|
64
|
+
' "$ABS" "$BOUNDARY"
|
|
65
|
+
exit 0
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: support-triage
|
|
3
|
+
description: Run a client support request end to end — clarify what was actually asked, reproduce it, judge urgency and whether it fits the support agreement, fix and verify it, then close the loop with the requester in their language. Use when handling a support ticket, when a client reports something broken, when triaging a support queue, or when a request might be out of scope. Baseline workflow, refined with the team over time.
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Support triage
|
|
8
|
+
|
|
9
|
+
Support work is judged on two things clients actually notice: **was it understood, and were
|
|
10
|
+
they told what happened.** The technical fix is usually the easy part.
|
|
11
|
+
|
|
12
|
+
The two failure modes to design against: fixing the wrong thing because the report was taken
|
|
13
|
+
literally, and doing three hours of unscoped work because nobody stopped to ask whether it
|
|
14
|
+
was covered.
|
|
15
|
+
|
|
16
|
+
## When to use
|
|
17
|
+
|
|
18
|
+
- A client support request needs handling.
|
|
19
|
+
- Something broke on a live client site.
|
|
20
|
+
- Triaging a queue of open support requests.
|
|
21
|
+
- A request looks like it might exceed the support agreement.
|
|
22
|
+
|
|
23
|
+
**Not this skill:** deciding what kind of engagement the work belongs to —
|
|
24
|
+
[`engagement-types`](../engagement-types/SKILL.md). Root-causing a stubborn bug —
|
|
25
|
+
[`investigate`](../investigate/SKILL.md). Server operations —
|
|
26
|
+
[`wp-pressable`](../wp-pressable/SKILL.md).
|
|
27
|
+
|
|
28
|
+
## Owns
|
|
29
|
+
|
|
30
|
+
Canonical for: support intake, urgency assessment, the in-scope decision, and closing the
|
|
31
|
+
loop with the requester.
|
|
32
|
+
|
|
33
|
+
## Procedure
|
|
34
|
+
|
|
35
|
+
1. **Read the request for the underlying need.** Clients describe symptoms and often propose
|
|
36
|
+
solutions ("can you make the button bigger" may mean "nobody is finding the form"). Restate
|
|
37
|
+
what you think they want and what outcome would count as fixed.
|
|
38
|
+
→ You can state the actual problem, not just the requested change.
|
|
39
|
+
2. **Identify the site and environment.** On multi-site accounts this is required, never
|
|
40
|
+
inferred ([`engagement-types`](../engagement-types/SKILL.md)). Confirm production URL
|
|
41
|
+
versus staging. → The exact target is named in the task.
|
|
42
|
+
3. **Reproduce it** before theorizing — same page, same role, same browser where it matters.
|
|
43
|
+
If it doesn't reproduce, that's information: ask for a screenshot, the URL, the browser,
|
|
44
|
+
or the account used. → Confirmed reproduction, or a specific question back to the client.
|
|
45
|
+
4. **Assess urgency honestly**, from user impact rather than from the tone of the request:
|
|
46
|
+
|
|
47
|
+
| Level | Means | Response |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| **Site down / data at risk** | Nobody can use it, or something is leaking | Drop other work, tell the client you're on it now |
|
|
50
|
+
| **Blocking** | A core task is impossible, no workaround | Same or next business day |
|
|
51
|
+
| **Degraded** | Works with a workaround, or affects some users | Scheduled into the queue |
|
|
52
|
+
| **Request** | Change, addition, or question | Queued and estimated |
|
|
53
|
+
|
|
54
|
+
→ A stated level the client would recognize as fair.
|
|
55
|
+
5. **Decide in-scope or not — before doing the work.** If it fits the agreement, proceed. If
|
|
56
|
+
it's really a small project, say so *now*, with a rough estimate, and let the client decide.
|
|
57
|
+
→ Either work has started, or a scoping question is with the client.
|
|
58
|
+
6. **Fix it properly.** Code changes go through the repo and the deploy pipeline; content and
|
|
59
|
+
template overrides are server-side database work
|
|
60
|
+
([`wp-pressable`](../wp-pressable/SKILL.md)). Never patch files directly on a server —
|
|
61
|
+
the next deploy erases it and the bug returns looking like a regression.
|
|
62
|
+
→ Fix is in the right layer and will survive a deploy.
|
|
63
|
+
7. **Verify on the environment the client saw it on**, flushing caches first. Screenshot the
|
|
64
|
+
result. → Evidence the specific reported case now works.
|
|
65
|
+
8. **Close the loop in the client's language.** What was wrong, what you did, anything they
|
|
66
|
+
should know or do. No jargon, no commit hashes. Then update the task
|
|
67
|
+
([`task-tracking`](../task-tracking/SKILL.md)) with the technical detail for the team.
|
|
68
|
+
→ The client knows it's resolved; the task carries the internal record.
|
|
69
|
+
|
|
70
|
+
## Recurring symptoms
|
|
71
|
+
|
|
72
|
+
| Report | Check first |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| "The site is down" | Is it actually down, or slow, or DNS/SSL? Confirm before escalating |
|
|
75
|
+
| "My change didn't save" or "isn't showing" | Caching first — object, page, browser — then the edit itself |
|
|
76
|
+
| "It broke and we didn't change anything" | Recent deploy, plugin auto-update, or an expiring credential or certificate |
|
|
77
|
+
| "It looks wrong" | Get a screenshot and their browser; often browser-specific or a stale cache |
|
|
78
|
+
| "Emails aren't arriving" | Deliverability and the sending service before the form code |
|
|
79
|
+
| "Can you just quickly…" | Scope check — quick to describe is not quick to build |
|
|
80
|
+
|
|
81
|
+
## Guardrails
|
|
82
|
+
|
|
83
|
+
- **Never work outside the agreement without approval.** Doing unbilled work "to be nice"
|
|
84
|
+
sets an expectation the whole team inherits.
|
|
85
|
+
- **Never make an undiscussed change while you're in there.** Unrequested improvements on a
|
|
86
|
+
client site are unreviewed changes.
|
|
87
|
+
- **Never edit files directly on the server** — it's overwritten by the next deploy.
|
|
88
|
+
- **Never change production data without a backup** and explicit confirmation
|
|
89
|
+
([`safety-hooks`](../safety-hooks/SKILL.md) will prompt on the dangerous commands).
|
|
90
|
+
- **Never guess at what a client meant** on anything ambiguous and destructive — ask.
|
|
91
|
+
- **Never report it fixed without verifying on their environment**, cache cleared.
|
|
92
|
+
- **Never leave a request silent.** Even "I'm looking at it, here's what I know" is a
|
|
93
|
+
response; silence is the complaint clients actually make.
|
|
94
|
+
|
|
95
|
+
## Done
|
|
96
|
+
|
|
97
|
+
- [ ] Underlying need restated and confirmed, not just the literal request.
|
|
98
|
+
- [ ] Site and environment identified explicitly.
|
|
99
|
+
- [ ] Reproduced, or a specific clarifying question sent.
|
|
100
|
+
- [ ] Urgency assessed on impact, and scope decided before the work began.
|
|
101
|
+
- [ ] Fix in the correct layer, surviving the next deploy.
|
|
102
|
+
- [ ] Verified on the client's environment with caches cleared, with evidence.
|
|
103
|
+
- [ ] Client told what happened in plain language; task updated with the technical record.
|