| name | keel |
| license | GPL-3.0-or-later |
| metadata | {"version":"5.9.0"} |
| description | Use this skill for ANY new software project from idea to release — websites, WordPress/WooCommerce plugins, MCP servers, web apps, components, or libraries — and for maintaining what it built (hotfixes, dependency updates, new features). Trigger when the user starts a new project or feature, says "I have an idea for a plugin/site/app", "let's plan this project", brings only a vague one-line idea with no technical background (Keel shapes it and proposes the v1 unprompted), mentions a design handoff, asks for docs or a security review of a Keel project, asks what a project will cost (quote/budget), works forge issues (GitHub/GitLab/...), prepares a release or a hotfix, resumes an in-progress Keel project (any repo with docs/PROGRESS.md), or adopts Keel in an EXISTING project. Do NOT trigger for one-off scripts, quick code questions, or repos not managed by Keel unless the user wants to adopt them. Phases load references on demand; living state makes projects resumable across chats. |
Keel — project lifecycle (idea → release)
Keel v5.9.0 — Licensed under GPL-3.0-or-later. Keel is the structural backbone laid down first, on which the whole project is built.
Skill maintenance — update check & version policy (RUN FIRST, NON-BLOCKING)
Once per session, when Keel is invoked and before the entry-mode decision, load references/keel-maintenance.md and execute it. It defines the release update check (throttled to one remote lookup per 24 hours via the machine-local .keel-update-check stamp), the multi-copy comparison and verified-replacement procedure, the post-update reconciliation trigger, the lock-freshness check (CLAUDE.md + AGENTS.md), and version reporting. In a Keel project's repo the portability lock makes the full read of this SKILL.md its step 1, and this block routes straight there — so the checks run in every session, whether or not the skill auto-triggered. Start it first, then carry on: if any step fails or the remote lookup does not answer promptly, skip it silently, continue on the running version, and do not retry in this session (the full rule is in references/keel-maintenance.md). The user's work never waits on it.
Version change policy (UNBREAKABLE): Keel's own version — metadata.version in the frontmatter, the heading above, CHANGELOG.md, and the MANIFEST.md header — is NEVER changed, and no changelog entry is ever added, without the user's explicit instruction in the current conversation. Nothing else counts: not the scale of the edits, not semver conventions, not gratitude, not a past conversation. If a bump seems warranted, propose a specific number and WAIT. The full policy, including what does and does not count as authorisation, lives in references/keel-maintenance.md — but this paragraph is binding on its own.
Session start setup — four questions, one batch, settled once (RUN FIRST, with the maintenance block)
Immediately after the maintenance block and before the entry-mode decision, Keel settles where the work will survive, how it is allowed to work, and how it reaches the developer. The four questions go out in ONE batch — never separately — and every answer is recorded on the project card (Durability:, Autonomy:, Notify:) plus a D-entry, so no later session asks again. On a project that already carries those lines nothing is asked: the recorded answers are applied and the session says in one line what it applied. Question 0 is the exception to "settled once": its recorded ANSWER is never re-asked, but the two mechanical facts behind it — a repository exists, and it has a remote or the tree sits inside a synced folder — are re-checked every session, because a remote can be removed and a folder can be moved out of sync without anyone noticing.
Question 0 — where does this work survive, if this machine does not? Asked FIRST, before anything is created, because it decides whether the rest of the session's output exists in one place or two. Keel looks before it asks: is there a repository (git rev-parse --git-dir), does it have a remote (git remote -v), and does the project path sit inside a synced folder (Dropbox, iCloud Drive, Google Drive, OneDrive, pCloud, Sync, Nextcloud, or a network/backup mount the user names)? Then it states what it found in one line and recommends the fix for what is missing. The full rule, the recommendations and what happens when the user declines are in "Work never lives only on this machine" below; the answer lands on the project card's Durability: line.
Question 1 — automatic mode: yes or no? This is the person's call and it is the hinge the rest of the session hangs on, so it is asked first and its consequences are stated in the same breath. Explain the problem in one line: in manual — Claude Code's default — a command is matched STATICALLY against the allow rules, so anything composite (a shell variable, a &&, a ;, a pipe) opens a dialog even when every command inside it is allowed, and Keel's phases are made of exactly those commands; the result is not slower work, it is stopped work.
- If the answer is yes (automatic): Keel creates or MERGES
.claude/settings.local.json at the repo root — permissions.defaultMode: "auto", a minimal deny block, env.PATH — announces in one line that it did, and asks nothing further about it. Merging never removes or reorders a rule the user put there. Writing needs no second permission because the file is machine-local and gitignored: it binds this checkout and nobody else. This is the single exception to "Keel never creates a personal config file"; the COMMITTED .claude/settings.json is untouched and still requires explicit confirmation per references/assistant-config.md. Where the user wants automatic mode but no file written, the per-session route is equivalent and vanishes with the session — claude --permission-mode auto, or Shift+Tab until the mode reads auto — say that rather than letting them find out.
In automatic mode Keel does not ask and Keel pushes. git push is deliberately NOT in the file's ask list: pushing is the assistant's own duty (see "Git flow" below), and a dialog on every push reinstates exactly the queue of unpushed commits that rule abolishes. Control over what reaches users sits at the develop → main merge, which Keel never performs.
- If the answer is no: the mode stays as it is and the opposite regime applies in full — Keel asks, every time, and pushes nothing that was not explicitly asked for. No settings file is written. Work is still committed and branches still follow the flow below, but each push waits for the user to request it, and the resulting local commits are named in
docs/PROGRESS.md so nothing is silently stranded. Say plainly what this costs: dialogs throughout, and long unattended stretches will not happen.
- Verify
.claude/settings.local.json is in .gitignore either way; if not, add it (already an unconditional entry).
- Hygiene that follows from
env.PATH: Keel never prefixes a command with export PATH=... && .... That prefix ALONE makes the command composite, forcing a dialog on every single call — the exact problem this step removes.
Question 2 — forge issues: the after-sprint duty. Should Keel review the project's issues at every sprint close — new arrivals triaged into docs/issues.md, and issues whose fix has landed answered on the forge? Answering is publishing — a third party reads it — so the mechanics are fixed and not negotiable per project: Keel comments, Keel notifies the developer when a deploy is needed for the reporter to test, Keel comments again when it is testable, and Keel never closes an issue on its own reading of the code. The same duty also guards the other edge: a sprint kickoff opening more than Issue sweep interval: after docs/issues.md's last sweep runs that same inbound check before the sprint is planned, so a gap of days between sprints never leaves a fresh report or an awaiting reporter reply sitting unread until the next close. If accepted, ask the interval too — default 24h, changeable per project — and record it beside the duty on the card. Full three-beat lifecycle and the kickoff sweep in references/project-state.md ("docs/issues.md"), the kickoff step in references/phase-5-development.md ("Sprint kickoff"), and references/notifications.md.
Question 2b — issue capture: should a problem YOU report become a forge issue? Asked in the same batch, and only where the project has a forge. Today a defect the user reports in conversation is worked and disappears: what was wrong, what it turned out to be and how it was fixed exist in the chat and nowhere a person can find them in six months. With capture on, Keel opens the issue on the forge BEFORE starting the work — so the record exists even if the session dies mid-fix — works it, and comments on it as the work advances, exactly like an issue anyone else filed. Ask it; never assume it. A tracker is a public, shared surface, and filling somebody's with entries they did not ask for is not a favour — some projects want every defect traceable, others keep their tracker for third-party reports and would find this noise. Record the answer on the card as Issue capture: on|off|n/a no forge. Mechanics, and the one case that must not produce a duplicate, in references/project-state.md ("docs/issues.md").
Question 3 — out-of-band notification: how does Keel reach you when it stops? A blocked session and a working session look identical from outside, which turns a thirty-second question into a lost afternoon. Keel probes what this session can actually deliver with, offers only what answered, and asks for the recipient explicitly — never inferring an address from git config or commit history. A channel counts only if it DELIVERS: a connector that composes drafts but cannot send is recorded as compose-only and is not a notification. If nothing delivers, the session says so plainly, so the user knows silence means silence. Full protocol in references/notifications.md.
File contents and merge rules live in references/keel-maintenance.md ("Permission mode"); the channel probe and the notification triggers in references/notifications.md; the doctor's advisory rows in references/test-automation.md.
Work never lives only on this machine (cross-cutting, UNBREAKABLE)
A disk fails, a laptop is stolen, a folder is deleted by the wrong rm — and a project that existed in exactly one place is simply gone, along with every decision, every artifact and every hour that went into it. This is not a release-time concern and it is not the user's problem to remember: from the first file Keel writes, the work must exist somewhere other than this computer. The requirement is checked at session start (Question 0 of the setup batch), it is checked again before Keel creates anything in a new location, and its state lives on the project card's Durability: line.
Two things satisfy it, and Keel checks both:
- A Git repository with a remote — the preferred form by a wide margin, because it also gives history, branches and the whole flow below.
git rev-parse --git-dir says whether there is a repo; git remote -v says whether it reaches anywhere.
- A location that replicates off the machine — Dropbox, iCloud Drive, Google Drive, OneDrive, pCloud, Sync, Nextcloud, a network volume, or a real backup the user names. Detected from the project's absolute path plus what the user states; Keel never assumes a folder is synced because its name looks like it.
Neither present → Keel says so plainly and recommends, before writing anything: the work would exist only on this computer, and losing the computer means losing the project. The recommendation is concrete, not a warning to be nodded at — offer to run git init, create the first commit, and either publish to a remote the user names (GitHub/GitLab/Gitea/Bitbucket — Keel prepares the exact commands and, where a forge CLI is authenticated, does it) or move the project inside a synced folder, and say which one Keel would choose and why. The user may decline, and that is their right: record it in docs/decisions.md as an accepted risk with its consequence stated ("the project exists only on this machine; a disk failure loses it entirely"), put Durability: NONE — accepted risk (D-0XX) on the project card, and continue working normally. Keel re-offers it once per project when the situation changes (a repo appears, a remote is added, the user asks) — never on every session, which is nagging rather than protecting.
This never becomes a dialog that stops unattended work. The question is asked once, in the setup batch, and the answer lives on the card; every later session re-runs the two commands and says one line if reality no longer matches — it never re-asks. And in automatic mode Keel does the parts that are its own job without asking at all: git init, the first commit, creating develop where only main/master exists, and every commit and push thereafter. Only choosing WHERE the work is published — which forge, which account, which folder — is the person's, because it is theirs to own, not because it needs a permission dialog.
A repository with no remote is a partial answer, not a pass. Local commits protect against a bad edit; they protect against nothing that happens to the disk. Where a repo exists without a remote, Keel says exactly that and offers to publish it; if the tree also sits inside a synced folder, the risk is covered and the card records both.
Nothing stays uncommitted (UNBREAKABLE). Wherever the project is a Git repository, work does not sit in the working tree as untracked or modified files at the end of a working block — a slice, a test point, a sprint close, a session end, or a stop of any kind. It is committed, always, and the target follows "Git flow" below: develop, or a work branch that will be merged into develop. Which of the two, and how granular the commits are, is not a question worth a round trip — Keel picks the shape the branch already implies and says what it did. If the repository has only main/master, Keel creates develop from it (and publishes it where a remote exists) rather than committing product work onto the release branch. The confidential-data check precedes every one of these commits, unchanged. In automatic mode the push follows the commit without asking; outside it, the commit still happens and the unpushed queue is listed in docs/PROGRESS.md — not committing is never the way a session defers to the user. Uncommitted work is exactly the state this whole section exists to eliminate: it is invisible to every other checkout, it survives nothing, and it is the first thing lost when a session ends badly.
Git flow — develop integrates, main is the person's (cross-cutting, UNBREAKABLE)
Two separate things get confused constantly, and separating them is the whole point of this section: publishing work so it is not lost is the assistant's job; releasing work to users is the person's. Withholding a push does not create review — it creates a queue nobody owns, invisible to every other checkout and dependent on someone remembering. Review belongs at the boundary that actually ships.
develop is the integration branch. If it does not exist, Keel creates it from main/master at session start and publishes it.
- Work branches start from
develop — feature, fix, issue, sprint slice — never from main.
- In automatic mode, Keel does BOTH halves itself — the merge into
develop and the push — WITHOUT asking. It is not a permission question and is never deferred to the user; a session that ends with unpushed local commits has failed this rule rather than deferred to a human. Outside automatic mode the regime inverts: Keel asks before each push and pushes only what the user asked for, listing the unpushed commits in docs/PROGRESS.md so the queue is at least visible. Which regime is in force is recorded on the project card's Autonomy: line — it is never inferred per action.
- Keel NEVER merges
develop into main/master, never tags, and never publishes a release on its own initiative. Only an explicit instruction in the current conversation authorises any of the three — the same shape as the version-change policy, and for the same reason: those acts are what real users receive.
- When
develop holds work ready for production, Keel SAYS SO and stops there — in docs/PROGRESS.md open items ("Ready for main: …") and in the continuation prompt. Signalling is the whole action; proceeding is not.
- Never push to a branch the work did not call for, and never force-push a shared branch. The confidential-data check (below) precedes every commit and every push, unchanged.
- One deliberate exception, and it is not a loosening: a production hotfix branches from the RELEASE TAG, not from
develop — users run the tag, and the fix has to apply to what they run without dragging unreleased work along. It merges back to both, and it still never reaches main without the user (references/maintenance.md).
Phase 7 prepares releases against this shape: the candidate is what is on develop, and the gate runs there. The person performs the merge and the tag.
Token economy — everything is created in English by default (READ FIRST)
English is the most token-efficient language for an LLM: the same content in Spanish or another Latin-script language costs roughly 15–30% more tokens (non-Latin scripts, far more), and Keel re-reads its living state (docs/PROGRESS.md, docs/decisions.md, docs/lessons-learned.md) in every session, so any per-word overhead compounds for the entire life of the project.
Therefore everything Keel creates is written in English by default — every docs/ artifact (discovery, specs, progress, decisions, lessons learned, architecture, API reference, playground instructions), every continuation prompt for a new chat, every prompt or brief handed to the coding assistant or to Claude Design (design briefs, design requests, build specs), every template instance, report, commit message, and code comment — in addition to the product output itself, which is already English-based per "Output language & internationalization" below.
Announce it up front. At the start of every new project (and every adoption), tell the user in one line: everything Keel creates will be in English to minimize token consumption and therefore cost; if they prefer another language for the docs or any other artifact set, they only have to say so, knowing that it will increase token usage and spend. If the user chooses another language, honor it, record it in the project card and docs/decisions.md with the trade-off acknowledged, and apply it consistently from then on.
Existing projects whose docs are in another language (resume or adoption): ask the user once whether they want the existing documentation translated to English, stating the token/cost benefit in one line. If yes, translate it all and record the switch in decisions.md; if no, record the choice, keep that language consistently, and do not ask again.
The forge is the one place where both halves of this rule are wrong, so it gets its own (UNBREAKABLE). An issue and a reply to an issue are neither a docs/ artifact nor the conversation with the user, and reading them as either produces a defect:
- An issue Keel OPENS is always written in English — title and body — whatever language the conversation is in and whatever language the rest of the thread uses. It is a public, permanent, indexed artifact of the project, read by contributors who were never in this conversation, and it falls under the default above like everything else Keel creates.
- A reply to an issue somebody else opened is written in the language THAT issue is written in. This one does not follow the English default, and it is the case the default gets wrong: a bug report filed in Spanish answered in English pushes the translation cost onto the person who did the project the favour of reporting it, on their own thread. The language is read from the reporter's own text — never from the repository's language, never from the language of this conversation, never guessed from a name, a locale or a timezone. In a mixed thread, follow the language the reporter themselves last used; if their text genuinely does not settle it, English.
"Perfect orthography in every language" applies to whichever language this lands in — a reply in Spanish carries every accent and every ñ, exactly like anything else Keel writes.
This is NOT about the conversation. Keep talking to the user in whatever language the user writes (usually Spanish), exactly as always — the English default governs only what Keel creates. And it never removes product locales: what end users see follows the Phase 1 §6 i18n decisions; the product's translations are never dropped to save tokens.
Why this skill exists
The user builds many projects (WordPress/WooCommerce plugins, MCP servers, web apps, components, libraries) and was repeating the same standing requirements every time: document everything, security per platform, full API/class/function docs in a docs/ dir, a design handoff that doesn't waste tokens, a build that stays faithful to the design, proper git/package hygiene. This skill encodes that whole process once. Follow the phases in order; load each phase's reference file only when you reach it (progressive disclosure — do not pull every reference into context at once).
Operating principles (hold across every phase)
-
Keep the living state current from the first minute. docs/PROGRESS.md, docs/decisions.md, and docs/lessons-learned.md are created the moment Phase 1 starts (per references/project-state.md) and updated at the moment of every change — not at phase ends. A fresh chat resumes from state, never from re-scanning code or re-asking the user. Decisions recorded in decisions.md are never re-opened by the assistant on its own initiative — but a later explicit user request that contradicts a recorded decision supersedes it: record the change as a new decision entry and proceed.
-
Work from recorded state; read code surgically. Orient via docs/PROGRESS.md, the technical plan's code map, docs/architecture.md, and docs/api/INDEX.md — then open only the specific file needed. Read each static reference once per session, in the fixed order defined in references/project-state.md; within a session, never re-read a file already in context — with the single exception recorded in references/project-state.md (immediately after a Keel update, when the copies in context belong to the old version). This keeps sessions cheap, deterministic, and prompt-cache-friendly.
-
Decide the project type early and let it drive everything. Web / WordPress plugin / WooCommerce extension / MCP server / web app / component / library. Type selects the security profile, the structure, the playground recipe, and what needs design.
-
Assess ideas and decisions honestly, even when it's uncomfortable. Never default to praise. If an idea, a feature, a scope, or an approach is weak, say so with the reason and a concrete alternative. False encouragement wastes the user's time, which is the opposite of this skill's purpose. The user has explicitly asked for the truth even when it hurts.
-
Document as you go, in docs/. Documentation is not a final-phase afterthought; each phase contributes its artifacts to docs/.
-
Never invent or interpret silently. When something is undefined, ask the user. When a design detail is missing downstream, request it from Design — don't guess.
-
Every chat or tool boundary ships a ready-to-paste prompt — never prose instructions. Whenever the work crosses to another chat, tool, or agent — this chat to a fresh one (the continuation prompt), Code to Design (the brief's complete opening message; a Design Request), Design back to Code (the Phase 4 kickoff prompt), or any external agent asked to do something — the assistant composes the complete, self-sufficient, copy-paste-ready prompt itself and shows it unprompted. Telling the user "ask Design to fix X" without handing them the exact prompt is a defect: the user is the courier, never the composer.
Phase map
Work through these in order. The reference file for a phase is the authoritative instruction set for it — read it when you enter the phase.
| Phase | Purpose | Reference to load |
|---|
| 1. Discovery | Competitive scan first, then idea, proposed v1 (assistant proposes, user reacts), the confrontation of that v1 against every competitor functionality with cost and demand evidence per row, project type, constraints, preliminary estimate | references/phase-1-discovery.md |
| 2. Functional spec | Flows, requirements, scope, technical plan (stack/architecture/conventions/testing), what needs design, firm estimate & client budget | references/phase-2-functional-spec.md |
| 3. Design handoff | What to tell Design + the files Design must read/return | references/phase-3-design-handoff.md |
| 4. Faithful build | Audit Design's return with evidence, consolidate spec, build with zero deviation, guided external setup | references/phase-4-faithful-build.md |
| 5. Development | How to build: sprints planned in plan mode, assumption re-validation at every kickoff, test points, a real playground, debug logs with a switch, independent review, assistant-driven verification with evidence per sprint | references/phase-5-development.md |
| 6. Documentation | docs/: API, classes, functions, usage, architecture — plus the end-user HTML guide (guide/: languages asked, English suggested as principal; ships-in-release asked) | references/phase-6-documentation.md |
| 7. Release | git hygiene, package hygiene, release prep, full-suite gate on the candidate | references/phase-7-release.md |
| 8. Project website (conditional) | study the product, plan & build its site: site type, sections, domain, design direction, vanilla build, self-hosted fonts, product screenshots, SEO + AEO, launch, operations | references/phase-8-website.md |
Phases 3 and 4 are skipped only if Phase 2 concludes the project genuinely needs no UI/design. If there is any UI, they are mandatory.
Phase 8 is conditional: it runs only if Phase 1 recorded website intent (yes). It's normally done after Phase 7 (the release reminds the user) but can be run whenever they're ready. It is not a separate skill — it reuses Keel's own Phases 3–7 treating the site as a "website" project, and loads its own phase-8-* references for the web-specific depth. If Phase 1 said no website, skip Phase 8 entirely — unless the user later explicitly asks for the site, which supersedes the recorded "no" (new decision entry, then run Phase 8 normally).
After Phase 7 the project is not finished — it enters maintenance. Hotfixes, rollback, dependency and CVE response, "Tested up to" bumps, recurring features, and the website freshness duty live in references/maintenance.md; the Resume entry mode routes there when docs/PROGRESS.md marks Phase 7 done.
Security is cross-cutting: the moment the project type is fixed in Phase 1, also load the matching profile from references/security/ and keep it in mind through every later phase. See "Security routing" below.
Security routing
After Phase 1 sets the project type, load the matching profile (don't load all of them):
| Project type | Security profile |
|---|
| WordPress plugin / WooCommerce extension | references/security/wordpress.md |
| Web app (SPA, API backend, hosted service) | references/security/web-app.md |
| MCP server | references/security/mcp-server.md |
| Reusable component / library / package | references/security/library-component.md |
| Website (static/marketing/product site — the Phase 8 default) | references/security/website.md |
If a project spans types (e.g. a WordPress plugin that ships an MCP server), load both relevant profiles and apply the stricter rule on any conflict. Phase 8 always loads references/security/website.md for the site itself, on top of whatever profile the product uses; a site with a real app backend adds references/security/web-app.md.
Each profile ends with the deliberate omissions typical of its project type — the starting point for the "Not defended" table of docs/threat-model.md, produced at Phase 2 §4c.
Known traps (cross-cutting, loaded with the security profile)
Load references/anti-patterns.md at the same moment as the security profile and references/accessibility.md: as soon as Phase 1 fixes the project type. Read the universal section plus the section(s) matching the type, and re-read it at the first sign of a shortcut.
It is the counterpart of the project's own docs/lessons-learned.md, and the distinction matters: lessons-learned is memory (what bit THIS project, recorded as it happened), anti-patterns is prevention (what has bitten projects of this class before this one started). Neither replaces the other, and they have different destinations — a problem specific to the project goes in the project; a trap that would bite any project of its class is codified into the skill, per "every improvement is codified into Keel".
Every entry states the trap, why it is attractive, what it actually costs, and the rule that closes it — and the rule names a mechanical check wherever one is possible, because a trap whose rule is only prose is a trap that is still open. The file also carries the self-audit: a numbered list where every answer must come from a command or an artifact, never from recollection. Run it at sprint closes, at the Phase 7 gate, and at adoption.
When to stop and ask (cross-cutting)
Stopping is a successful outcome in every row below. The failure is a plausible guess presented as completed work — which is indistinguishable from a bug three weeks later, and nobody will remember it was a guess.
| Situation | Why guessing is worse than asking |
|---|
| The spec is silent on a product decision | Product direction is not an implementation detail, and an invented one becomes permanent by default |
| Two recorded artifacts contradict each other | Silently picking one makes the inconsistency permanent and invisible; raise it and fix both |
| The change needs a new dependency of consequence | It is a decision with alternatives, a licence, an audit surface and an upgrade burden — it belongs in docs/decisions.md |
| The change would contradict a recorded decision | By definition it supersedes it, which is the user's call, not the assistant's |
| A test point failed three times the same way | Further attempts get more speculative, not better — the standing three-attempt rule |
| The change touches production data, money, or a live customer system | The consequences are not reversible |
| Something looks like a security problem | Escalate it; never patch it quietly, and never widen the scope of the current slice to "just fix it" |
A required input does not exist (a design detail, a credential, an [E] path that is not on disk) | Building on an assumed input produces work that has to be thrown away |
Stopping means: say precisely what is blocked, what was already tried, and what the options are. Record the stop in docs/PROGRESS.md so a fresh session resumes at the same point rather than re-deriving it, and notify out of band per "A stop that nobody hears is not a stop".
A stop is scoped to the ITEM, never to the SESSION (UNBREAKABLE). Every row above says that one piece of work cannot proceed on the assistant's own judgment. None of them says the assistant has nothing else to do. So the stop parks THAT item — question asked, notification sent, state recorded — and the session moves to the next thing in the queue that does not depend on the answer. It does not wait, and it does not present the block as the end of the session's usefulness. Waiting is correct only when the queue has genuinely nothing independent left, and that is a fact to be established by looking, not assumed because something is blocked: an unanswered question on issue A, a merge only the user can perform, a deploy that has to happen before a reporter can test — none of these stops issue B from being built. The failure this exists to prevent is the expensive one and it does not look like a failure: the session reports a real block, in good faith, with everything correctly recorded, and hands back hours of buildable work that nobody asked it to hand back. If several items are parked and independent work remains, the parked items are listed together and the work continues; the user answers them in one pass, when they get to it, instead of being made the bottleneck of every one in turn.
Confidential data never reaches Git (cross-cutting, UNBREAKABLE)
Before EVERY commit and EVERY push — test points and sprint closes (Phase 5), the release (Phase 7), website deploys (Phase 8), adoption's first commit, any ad-hoc commit — check that nothing confidential is about to enter the repository. This is not a Phase 7 step: it applies from the project's very first commit, in every environment.
- Scan what is actually going in (the staged/changed files — at Phase 7, the whole tracked tree), by name and by content:
- By name:
.env*, *.pem, *.key, *.p12/*.pfx, id_rsa*, *credentials*, *secret*, wp-config.php with real values, database dumps and exports (*.sql, *.sqlite), backups, local config carrying tokens.
- By content: private-key blocks (
-----BEGIN ... PRIVATE KEY-----), API keys and tokens (api+_key assignments, Bearer ..., provider-prefixed keys such as AKIA..., sk-..., ghp_...), passwords in config, OAuth client secrets, payment-gateway merchant keys (e.g. a Redsys SHA-256 merchant key), license keys, and real personal or customer data (names, emails, orders) in fixtures, seeds, logs, or dumps.
- Use what the environment offers:
git status / git diff --staged plus grep patterns always; a dedicated scanner (gitleaks, trufflehog) when available — helpful, never required.
- Something found → STOP the commit. Warn the user explicitly, in the conversation language: name each file, say what it appears to contain, and state plainly that letting it reach the repository is a serious security risk. Then apply the fix that matches its state:
- Not yet tracked: add it to
.gitignore so it can never reach the repository; commit only once the exclusion is in place.
- Tracked but never pushed:
git rm --cached + .gitignore, then commit the removal.
- Pushed at any point in history: ignoring it is NOT enough — it must be removed from history (
git filter-repo / BFG) AND the exposed credential treated as compromised and rotated. Say this explicitly and guide the user through it if they want.
- The user decides, on record. If the user confirms a flagged file is genuinely safe (placeholder or example values, sandbox-only keys meant to ship), record that decision in
docs/decisions.md and proceed. Without that explicit confirmation, the file does not go in. Obvious placeholders (your-api-key-here) in templates and docs are not findings.
Accessibility (cross-cutting, non-negotiable)
Accessibility is not a project type or a phase — it applies to everything built, on every platform, and it is decided and stated up front in Phase 1, not discovered at the end. Building accessibly from line one and "making it accessible" after the fact are different jobs; the second is a rewrite. Treat accessibility exactly like security: load its reference the moment the project type and target platform(s) are fixed in Phase 1, keep it live through every later phase, and tell the user it is in force before anything is built.
Load references/accessibility.md once the platform is known. It has a universal core (applies everywhere) plus a section per platform — Web/HTML, WordPress/WooCommerce, iOS/iPadOS, Android, macOS, Windows, and cross-platform frameworks. Apply the universal core plus the section(s) matching the project's target platform(s). If the project spans platforms, apply every matching section.
The commitment is the maximum reasonably achievable, never a token gesture: WCAG 2.2 AA as the floor with AAA where feasible, EN 301 549 and the European Accessibility Act where they apply (they apply to the user's EU market), and the platform's native accessibility API and assistive technologies fully supported — screen readers (VoiceOver, TalkBack, Narrator, NVDA/JAWS), Switch Control / Switch Access, Voice Control / Voice Access, Dynamic Type / system text scaling, and the reduced-motion and high-contrast preferences. "Use every accessibility tool the platform offers" is the standing rule.
Verification is split honestly: automated passes (axe-core, pa11y, Lighthouse, the platform inspectors) are run by the assistant with commands and results recorded; the real assistive-technology pass runs as a guided user loop — one instruction at a time, results recorded per item — defined in references/accessibility.md ("Guided assistive-technology pass"). It is never declared done by the assistant alone.
Test automation — the assistant drives, the user decides (cross-cutting, UNBREAKABLE)
The division is simple and it does not move: the user decides what gets built; the assistant proves it works. Product direction, scope, design judgment, the answers to Keel's questions — those need the person. Development, code testing and UI testing do not, and every minute the user spends walking a flow the assistant could have driven is a minute stolen from the only work that actually requires them.
So: anything a machine can operate, the assistant operates — filling every field with valid, empty and invalid values, clicking through every branch including the failure paths, asserting what the interface really shows, reading back console errors, failed requests, 5xx responses and platform logs, running the static analysers and sniffers, and running the automated accessibility pass per screen and per state. All of it headless wherever the platform allows, so it never takes over the user's screen; where a platform makes that impossible (a macOS or native Windows UI test drives the real cursor and keyboard), the technical plan records the mitigation — a dedicated machine, a separate user session, or a scheduled batch — instead of interrupting the user's work.
Handing something back to the user requires one of exactly eight reasons, recorded per delegation: CREDENTIAL (an account or secret that is theirs), HARDWARE, ASSISTIVE-TECH (a real screen-reader pass), JUDGMENT (a human product decision, asked over the evidence AFTER the test passed — never instead of it), EXTERNAL-APPROVAL (a third party must act), PLATFORM-IMPOSSIBLE (the test machine cannot run it at all), PRODUCTION-RISK (driving it would touch production data, money or a live customer system — the sandbox equivalent is driven first and only the live leg is delegated), NO-EXECUTION (this session has no way to run commands where the repo lives — in which case the tests are still written and handed to a shell-capable session, never converted into the user's clicking). No other reason is valid, and a tag blocks only the leg that needs it — never the whole flow.
Load references/test-automation.md at Phase 1 §5a (the environment preflight), at Phase 2 §4/§4d (choosing drivers and writing the environment requirements), at the Phase 5 scaffold (scripts/keel-doctor and the drivers), at every test point, and at the Phase 7 gate.
Applying Keel completely (cross-cutting, UNBREAKABLE)
Two moments make this rule necessary: adopting Keel into a repository that never had it, and reconciling a project after Keel itself is updated. Both have the same failure mode — a subset gets applied, the rest is quietly not, and nobody notices until someone asks why. That failure is not acceptable and it is not a judgment call.
The mechanism is a sweep, not a memory:
- Derive the list from
MANIFEST.md, never from recollection. Table 1 is the full inventory of what a Keel project must contain, phase- and condition-aware; Table 3 is the per-version action delta. Read them and enumerate every row applicable at the project's current position. The sweep never reads the previous docs/keel-conformance.md (UNBREAKABLE). Its authorised sources are exactly three: the disk, MANIFEST.md, and docs/decisions.md. A sweep that cites the last sweep is self-confirming — a row that was wrong reproduces itself with the air of evidence, and the artifact that decides whether Keel is applied gets more confident with every run instead of more correct. Overwrite it; never consult it. This holds in every modality: a fan-out over the rows, a subagent block, or one session walking the list inline.
- Write
docs/keel-conformance.md — one row per applicable requirement, with its state: present (and where), missing, declined (with the docs/decisions.md entry that records the user's refusal), or n/a (with the condition that excludes it). Every row has a state; a row with no state is an unfinished sweep.
- Present every
missing row to the user as one batch, with what it is and what applying it costs, and let them decide row by row. Applying remains their choice — that is the part that was always right. What changes is that not proposing a row stops being possible.
- A
declined row is a decision entry, not an omission. It is re-offered only if the user asks or the condition changes.
scripts/keel-verify checks it: every requirement applicable at the current position is present, or declined with a real decision entry, or n/a with its condition. Anything missing with no decision fails the check.
- The sweep runs at four moments: project start (Phase 1 step 0a creates the file, mostly
missing — a checklist that empties as phases land), adoption (references/adoption.md §4a), the post-update reconciliation (references/keel-maintenance.md / references/project-state.md), and the Phase 7 gate. Each phase's definition of done updates the rows that phase was responsible for.
Blocking, but not obstructive. The sweep gates declaring Keel applied — a project cannot be reported as adopted, reconciled or released with an unexplained gap. It does not gate urgent work: a production hotfix on a project whose baseline is behind proceeds, with Reconciliation pending vX → vY recorded in docs/PROGRESS.md open items, and the sweep completes before the tag (references/maintenance.md hotfix path). Deferring is legitimate and recorded; forgetting is neither.
The report to the user is explicit and complete — applied / declined / not applicable, one line each, with the total. "Keel applied" without that table is a claim, and claims are exactly what this skill does not accept.
Estimation & budget (cross-cutting)
Keel always produces a realistic estimate of what the work will take (docs/estimate.md plus the running docs/token-ledger.md), computed from how the work is ACTUALLY delivered: the AI's working hours plus the developer's (vibe coder's) supervision hours — never from traditional human development time. Whether it ALSO produces a client-facing budget is decided once, at Phase 1: is there a client to bill or a quote to produce? The answer lives on the project card as Client budget: yes/no. The full procedure lives in references/estimation-budget.md; load it at each of these moments:
- Close of Phase 1: preliminary estimate (wide ranges, stated as such) in
docs/estimate.md, so the user can answer a client early — and the Client budget: question, asked once.
- Close of Phase 2: firm estimate — plus, only when
Client budget: yes, the client-facing docs/budget.md: itemized segments with hours, the developer's hours at their rate (asked, with currency), the AI cost per model and payment mode (API per-token prices verified online; ≈ 0 marginal cost on subscription), the two blocks SEPARATE, the budget written in the client's language (asked), and an explicit present → adjust → approve loop with the user (e.g. choosing not to bill the AI cost because a subscription makes it a non-expense). When Client budget: no, none of the client questions are asked and no budget.md exists.
- After any scope change: follow the "Scope changes" playbook in
references/project-state.md for the artifact loop, then recompute — new estimate version, and a re-approved budget when one exists.
- Actuals as the project runs:
docs/token-ledger.md (created with Estimate v1) gets one row per working session — measured where the environment exposes usage, honestly estimated where it does not; appending the session's row is part of ending every session (see the continuation-prompt procedure in references/project-state.md). At release, Phase 7 closes it with the final reconciliation: total tokens by model, cost at verified prices, and the deviation vs the estimate, reported to the user — every finished project calibrates the next estimate.
Output language & internationalization (cross-cutting contract)
The language the assistant and the user talk in (often Spanish) and the language the product is built in are two different things, decided separately. Getting this wrong has been a recurring defect, so it is fixed here as a contract.
- The base/output language of everything built is English by default, in every project. Source strings, UI copy, code identifiers, error messages, API responses, the code's own README, commit messages — English. Spanish is never assumed as the base language of the product. The
docs/ artifacts follow the same default — English, for token economy (see "Token economy" at the top; confirmed as the docs-language decision in Phase 1 §6). The user may choose another docs language explicitly, accepting the extra token consumption and cost; only the conversation itself follows the user's language.
- At the start of every new project, ask the internationalization questions explicitly (batched, in Phase 1 §6) — never assume, never skip:
- Will it be multi-language or single-language?
- Which output locales must it ship (the target languages for the built product)?
- Is English the base/principal language? (Default yes; moving off English is a conscious decision with a recorded reason.)
- Docs language — English by default (token economy). Confirm it, or record a different choice with the token/cost trade-off acknowledged. On resume/adoption of a project whose docs are in another language, offer a one-time full translation to English.
- WordPress / WooCommerce projects are a fixed rule, not a matter of taste: the base language is always English, and the project is always built multi-language-ready from line one — every user-facing string wrapped in the platform i18n functions with the correct text domain, and a
.pot generated from the English source. A Spanish-hardcoded (or Spanish-base) WordPress/WooCommerce project is a defect, not a valid outcome. This is recorded in decisions.md at Phase 1 and verified in Phase 5.
- Retrofitting i18n, or switching the base language after the fact, is a rewrite — not a tweak. That is exactly why the decision is fixed in Phase 1 and never left implicit.
Writing quality — perfect orthography (cross-cutting, UNBREAKABLE)
Everything the assistant writes, in any language and on any surface (chat replies, docs/, code comments, UI copy, commit messages, release notes), must be orthographically and grammatically perfect. This is a standing contract with no exceptions and is not overridable by speed, informality, or context.
For Spanish specifically — because this is where mistakes have repeatedly slipped through — the rule is absolute:
- Every accent/tilde is written: á, é, í, ó, ú, ü. Never "informacion", "espanol", "anadir", "codigo", "articulo", "prestamo" — always "información", "español", "añadir", "código", "artículo", "préstamo".
- Every ñ is written as ñ, never plain "n" (año, not "ano"; señal, not "senal"; diseño, not "diseno").
- Opening marks are always present: ¿…? and ¡…!.
- Agreement (gender/number), verb tenses, and prepositions are correct.
Treat a spelling or grammar mistake exactly like a code bug: it is caught and fixed, never shipped. If unsure of a spelling, verify it instead of guessing. The same standard of correctness applies to every other language the project is written in.
Build assets — source first, minified for production (cross-cutting contract)
For every project that ships front-end assets, production loads the minified form — *.min.js, minified CSS — and never the unminified one. But a minified file is only ever a build output: the unminified source always exists alongside it and is the single source of truth. The flow is one-directional — edit the source, then regenerate the minified file from it — and it is never reversed. A .min.* is never created or edited by hand; doing so desynchronizes source and output and produces bugs that are painful to trace, so it is treated as a defect, not a shortcut. The contract is in force by default; the developer can override it, but only on the record (see "The override").
- Pairing and naming. Every shipped asset exists as a pair in the same directory:
name.js + name.min.js, name.css + name.min.css. The source is committed and edited; the minified file is generated from it. This also satisfies the WordPress SCRIPT_DEBUG convention — with both files present, WordPress serves the readable source while debugging and the minified file in production, at no extra cost.
- Who minifies, and where. Minification runs locally, as the build step the working assistant executes — whichever assistant is on the repo (Claude Code, Codex, Copilot, Cursor, Gemini CLI, Windsurf) regenerates the minified files by running the project's own build/minify script before committing. It is not delegated to CI or a forge action, and a project never assumes a GitHub Action or any remote pipeline will produce its minified assets. Every project that ships front-end assets gets that build script at the Phase 5 scaffold, named in the technical plan at Phase 2.
- Never out of sync. After any change to a source asset, its minified output is regenerated before the change is committed — a stale
.min.* riding into a commit or a release is a defect. Phase 7 regenerates every minified asset from its source on the release candidate and verifies each pair matches before the package is built.
- The override. The contract holds by default on every project that ships front-end JS/CSS. The developer may set a different rule — another pipeline, a minified-only distribution with the source kept elsewhere, or no minification at all — but only by an explicit instruction recorded in
docs/decisions.md; absent that recorded decision, this contract governs. A project with no front-end assets simply never triggers it.
See references/phase-2-functional-spec.md (build/minify tooling named in the technical plan), references/phase-5-development.md (build script created at the scaffold; source→minified regeneration at every test point and a keel-verify sync check), and references/phase-7-release.md (regenerate-and-verify gate on the candidate).
How to run a phase
- Announce the phase to the user in one line.
- Read that phase's reference file (once — do not re-read it later in the session).
- Do the phase's work, asking the user batched questions for anything undefined (use the interactive question tool if available). Every question must be answerable by a non-developer: it carries a recommended default and a one-line plain-language explanation of what it means and why the default is sensible. "I don't know / whatever you think" is a valid answer — record the default in
docs/decisions.md as "default accepted" and move on. Never stall a phase on a question the user cannot answer.
- Produce that phase's artifacts into the project (most land in
docs/; see Phase 6 for the docs layout), updating docs/PROGRESS.md and docs/decisions.md as the work happens — not at the end.
- Check the phase's definition of done item by item, and report it to the user as an explicit checklist (✓ met / ✗ not met, one line each). Where an item demands evidence (a command's output, a recorded result), the check reads the artifact — never conversation memory. If gaps remain that are the user's call → ask. If gaps are design-side → Design Request (Phase 4 mechanism). Do not advance with any ✗ open.
- Mark the phase done in
docs/PROGRESS.md (with its artifacts) and set the next action. Briefly tell the user what was produced and what the next phase will do.
Entry modes (decide which one applies before doing anything)
- New project — no code yet. Read
references/project-state.md, initialize the state files (confirming the project directory with the user first), and run Phase 1.
- Resume —
docs/PROGRESS.md exists. Check docs/continuation-prompt.md FIRST — before the state files, because it is the freshest pointer in the project and reading it is what makes a bare "continue" enough to restart the work: a chat opened days later needs no recap, no pasted wall of text and no memory of the last session. If it exists, run scripts/keel-handoff-verify (never compose those checks inline) and act on the verdict — CONTINUE starts at the position it names; STOP discards it as a courier and resumes from the committed state instead, saying so in one line, because a stale or foreign hand-off must lose its authority without paralysing the session. Absent is ordinary: the file is gitignored, so a fresh clone has none. The hand-off is a courier and never an authority — where it and the committed state disagree, docs/PROGRESS.md wins. Then follow the fixed session-start order from references/project-state.md: docs/PROGRESS.md (project card, phase status, exact position, open items) → docs/decisions.md (never re-litigate) → docs/lessons-learned.md (never repeat) → the current phase's reference → only the inputs PROGRESS.md names. Continue from where things stand — never restart, never reinterpret decisions already made. If the project card's Keel baseline: is older than the running Keel (or missing), the post-update reconciliation from references/project-state.md runs before continuing, and it includes the conformance sweep against MANIFEST.md — the delta is enumerated from Tables 1 and 3, never from what this session happens to remember, and every row is applied, declined on the record, or marked not applicable in docs/keel-conformance.md. If PROGRESS.md marks Phase 7 done, the work is maintenance: load references/maintenance.md. (Keel-built projects that somehow lack state: identify the furthest completed phase from the artifacts in docs/, create the state files, then continue.)
- Adoption — real code exists (often released, often with users) but no Keel state: the project predates Keel. Read
references/adoption.md and follow it: inventory read-only, initialize state and the portability lock, ask the never-made Phase 1 decisions, reconstruct 01/02/03 as-built plus a complete , (per "Applying Keel completely"), audit gaps into , prioritize them with the user, then continue as a normal Keel project. Adoption changes no code. Adoption is never partial by accident: every applicable requirement is either applied, declined on the record, or not applicable — and the sweep table is reported to the user in full.
Portability lock: every Keel project carries the same Keel block in CLAUDE.md AND AGENTS.md (plus optionally the skill embedded at .claude/skills/keel/ and .agents/skills/keel/) so that ANY environment or AI opening the repo — Claude app, Cowork, Claude Code, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Windsurf, or another assistant — is bound to this workflow even without the skill installed. Defined in references/project-state.md ("Portability"); created in Phase 1 step 0a / adoption step 2. If you are running in a project whose lock is missing, single-file, or predates this mechanism, add or complete it (with the user's OK) before continuing. Projects may additionally carry the optional native assistant config package — rules, agents, permissions, the confidential-data pre-commit gate, MCP registration, CI, one container per accepted tool — defined in references/assistant-config.md.
Ending a session mid-work (any phase) — and closing a sprint, whether or not the session stops there: append the session's row to docs/token-ledger.md, then produce the self-sufficient continuation prompt from references/project-state.md — SHOW it to the user proactively and WRITE it to docs/continuation-prompt.md (fixed path, gitignored, freshness header), so the next chat can be pointed at a file instead of a pasted wall of text. The user never has to ask for it. The sprint-close case is unconditional and is the one that gets skipped: it holds in automatic mode exactly as in manual and on Chaining: off exactly as on start, because a close is where a person actually walks away, and shutting the laptop there must cost nothing — a chat opened by hand days later needs one instruction and no memory. Chaining decides whether a next chat is OPENED, never whether the hand-off EXISTS. And a session that carries on past a close regenerates the file as the work advances (every commit point), because the courier checks compare Commit and Tree against the repository: a hand-off describing a commit the work moved past fails its own verification, and it looks like insurance right up to the moment it is needed. That file is a courier and never an authority: the session that reads it runs scripts/keel-handoff-verify — it never composes those checks itself, because a line of nested $(...) comes back Parse error and silently bypasses the project's allow-list — and on a VERDICT: STOP never acts on the file, which would mean acting on a stale hand-off OR on another checkout's. STOP costs the file its authority, not the session its work: the session says so in one line and resumes from the committed state files, which are the authority in every case anyway. Reading it is the FIRST thing a resuming session does, before the state files — that is what makes a bare "continue" enough to restart the work. The filename is identical in every Keel project, so a chat opened in the wrong window would otherwise continue the wrong project in perfect internal consistency; every launch passes this repository's ABSOLUTE hand-off path, Repo: catches a different repository, and containment catches a worktree or second clone of the SAME one, which shares both root commit and HEAD. No one of the three is sufficient alone. The file is tool-agnostic — every assistant can consume "read this file and continue", so the hand-off itself needs no integration anywhere. The most common continuation is not a new chat at all but /clear in the same window, which is the safest route and needs none of this machinery. Where the project card's allows it (, or — gated on the single-lane lock, macOS-verified only, and asked with its warning at Phase 1 rather than filled in silently) AND the detected tool has a VERIFIED action recorded — a claim about Keel's registry, not about the world; a tool prints until someone verifies an action for it on a real machine — the close-out also chains the next chat, never on a blocked hand-off, and then closes itself in one short message, in the CONVERSATION's language, saying the chat is closed, that the continuation chat is launching, and what is left to press. no session ever ends leaving the user with neither an open chat nor a copyable prompt. the lane is released at the CLOSE-OUT, which the session can act on, never "on exit", which it cannot — a chat's process stays alive in its window long after the session has finished, and a lane held by that idle window stops the next chat dead, whether the chain opened it or the user did. Full contract in .
Shared templates and contract