diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index f49fcbd..53e018a 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -7,7 +7,7 @@
"plugins": [
{
"name": "freethinking",
- "source": "./",
+ "source": "./plugins/freethinking",
"description": "Shared engineering skills: standard workflows and prompts for day-to-day development."
}
]
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
deleted file mode 100644
index 37eef90..0000000
--- a/.claude-plugin/plugin.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "name": "freethinking",
- "version": "0.12.0",
- "description": "Shared engineering skills: standard workflows and prompts for day-to-day development.",
- "author": {
- "name": "Developer Handbook contributors"
- },
- "license": "MIT"
-}
diff --git a/.claude/specs/directory-listing/design.md b/.claude/specs/directory-listing/design.md
new file mode 100644
index 0000000..1ba817e
--- /dev/null
+++ b/.claude/specs/directory-listing/design.md
@@ -0,0 +1,218 @@
+# Design Document
+
+## Overview
+
+The plugin's files move into `plugins/freethinking/` and everything else stays at the repository root, so the folder the Directory scans holds only what an installed plugin loads. Three things follow from that move. The eval runner only reads cases from inside the plugin root, so the Makefile builds a throwaway Run_Tree per invocation, a copy of `plugins/freethinking/` with a copy of `evals/` inside it, and writes results back to the root. The Directory's validator follows a subfolder plugin's hook scripts only when they use literal paths under `${CLAUDE_PLUGIN_ROOT}` and nothing else, so both scripts are rewritten to that shape, each reading `rules/*.md` straight into one `awk` program. The free checks, the Makefile's validate target and the four guides are re-pointed at the new paths, and the manifest gains the listing fields with the README gaining the Disclosure_Section.
+
+## Architecture
+
+```mermaid
+flowchart LR
+ subgraph root["Repository root"]
+ MP[".claude-plugin/marketplace.json
source: ./plugins/freethinking"]
+ subgraph plugin["plugins/freethinking/ (scanned by the Directory, loaded by Claude Code)"]
+ MAN[".claude-plugin/plugin.json + icon.png"]
+ SK["skills/ agents/ rules/ hooks/"]
+ PR["README.md + Disclosure_Section"]
+ end
+ EV["evals/"]
+ CK["checks/"]
+ MK["Makefile"]
+ SP[".claude/specs/"]
+ end
+ MP --> plugin
+ MK -->|"validate"| CK
+ MK -->|"validate"| MAN
+ CK -->|"read"| SK
+ MK -->|"eval-*"| RT
+ subgraph RT["Run_Tree (temp, deleted after the run)"]
+ RP["copy of plugins/freethinking/"]
+ RE["copy of evals/ (no results/)"]
+ RG[".git (git init)"]
+ end
+ RT -->|"--output-dir"| RES["evals/results/<timestamp>/"]
+```
+
+```mermaid
+sequenceDiagram
+ participant C as Contributor
+ participant M as make eval-skill-pr
+ participant T as Run_Tree
+ participant H as claude plugin eval
+ participant S as scaffold.sh (copy)
+ C->>M: make eval-skill-pr EVAL_ARGS="--runs 1"
+ M->>T: mktemp -d; copy plugins/freethinking/. and evals/ (minus results/); git init
+ M->>H: claude plugin eval --tag pr --output-dir /evals/results/ ...
+ H->>S: run scaffold in the case sandbox
+ S->>T: git rev-parse --show-toplevel = Run_Tree; bash $ROOT/evals/fixtures/setup.sh
+ H-->>M: table, exit status
+ M->>T: rm -rf (trap on EXIT, pass or fail)
+ M-->>C: exit status of claude plugin eval
+```
+
+Key decision, the plugin in `plugins/freethinking/` with the marketplace file left at the root. The Directory scans the folder holding the manifest and nothing above it, and a marketplace entry may point at any folder in the repository, so this is the one layout that hides `evals/`, `checks/` and `.claude/specs/` from the scan while keeping them in the repository beside the code they test and record (Requirements 1.1 to 1.4). The install id does not change, because neither name changes, so no `renames` entry is needed (Requirement 1.6). Rejected alternative, a second repository for the evals: the runner would still need the cases inside a plugin folder, so the copy would still be needed, and a case would no longer sit beside the skill it grades. Rejected alternative, `export-ignore` in `.gitattributes`: the Directory stops validation on it. Rejected alternative, a gitignored copy of `evals/` inside `plugins/freethinking/`: it leaves a transient tree in the source folder that `claude plugin validate` and `--plugin-dir` would see, and a stale copy would grade stale cases.
+
+Key decision, the Run_Tree is a plain copy with `git init`, plus the one repository file a scaffold reads, `.github/pull_request_template.md`. Probed on 2026-10-16: `--eval-dir ../evals` is refused ("must stay inside the plugin root"), an absolute path is refused, and a symbolic link to `evals/` outside the plugin is refused by name at run time, while a copy with a git repository initialised at its top ran a scaffolded case to a score of 1.00 with results written to the real tree through `--output-dir`. The `git init` is what lets the 23 scaffolds that locate `evals/fixtures/setup.sh` through `git rev-parse --show-toplevel` keep working unchanged (Requirement 5.3). The copy is about 350 small files; `evals/results/` is excluded so old results are never copied. Rejected alternative, scaffolds that locate the fixtures by a relative path from `$0`: 23 edits for no gain once the copy has a git top. Rejected alternative, `--output-dir` left at the default: results would land inside the Run_Tree and be deleted with it.
+
+Key decision, both hook scripts become one `awk` program each over `"${CLAUDE_PLUGIN_ROOT}"/rules/*.md`. The Validator_Shape forbids any other variable, any command substitution and any call to another file, so the session script's `$(dirname "$0")` fallback and loop variable go, and the subagent script stops running the session script and capturing its output (Requirement 4.1). `awk` reading the files directly replaces the sentinel trick, because the files are the input rather than a captured string: each record is emitted with `\n`, which reproduces every file that ends in a newline byte for byte and adds one to a file that does not, in both scripts alike, so the two outputs stay equal (Requirement 4.3). The two-line "Handbook rules" header is now written in both scripts, with a comment in each naming the other, which is the cost of not calling across files. The `awk` program itself is inline in the script, as it is today; the portal flagged only the script-runs-script step on the current layout, and Requirement 7 is where a hold on an inline `awk` would show. Rejected alternative, leaving the scripts as they are: one hold, "Scripts the validator couldn't follow", on every version.
+
+Key decision, `plugins//` rather than a single `plugin/` folder. The docs' marketplace layout keeps every plugin under `plugins/`, so a second plugin is a sibling folder and one more entry in the marketplace file, and nothing moves twice (Requirement 6.5). The Makefile and each check name the folder once, in `PLUGIN` and a top-of-file constant. What this design does not do is split the eval suite: the runner ties a suite to one plugin's eval directory, so a second plugin would need `evals//` and the Makefile copying the matching one; that is one edit when the day comes and nothing now. Rejected alternative, splitting `evals/` today: churn in 274 files and every eval target's enumeration for a plugin that does not exist.
+
+Key decision, two READMEs. The Directory shows the plugin's README as the listing's description, so `plugins/freethinking/README.md` keeps the user-facing sections and ends with the Disclosure_Section, while a short root README orients a contributor and takes the Testing and "Add or change a skill or rule" sections, which describe the repository's checks and evals rather than the installed plugin (Requirements 3.1 to 3.5). Rejected alternative, one README at the root with a link from `plugins/freethinking/`: the Directory requires a README of at least 40 words inside the plugin folder.
+
+## Components and Interfaces
+
+```mermaid
+classDiagram
+ class Makefile {
+ +validate()
+ +check()
+ +eval-targets()
+ -run_tree(flags)
+ }
+ class load_rules_sh {
+ +run(CLAUDE_PLUGIN_ROOT) text
+ }
+ class load_rules_subagent_sh {
+ +run(CLAUDE_PLUGIN_ROOT) json
+ }
+ class Checks {
+ +test_hook_encoder
+ +test_rules_size
+ +test_agents / test_descriptions / test_report_skill / test_issue_forms
+ +test_eval_cases
+ }
+ Makefile --> Checks : make check
+ Checks --> load_rules_sh : runs
+ Checks --> load_rules_subagent_sh : runs
+```
+
+### Repository layout
+
+Moves files; no logic.
+
+| Operation | Input | Output | Requirements |
+|-----------|-------|--------|--------------|
+| `git mv` of `.claude-plugin/plugin.json`, `.claude-plugin/icon.png`, `skills/`, `agents/`, `rules/`, `hooks/`, `README.md` into `plugins/freethinking/` | the root tree | the Plugin_Folder, with `.claude-plugin/marketplace.json` left at the root | 1.1, 1.2, 1.3 |
+| `marketplace.json` entry `source` | `"./"` | `"./plugins/freethinking"` | 1.4 |
+| new root `README.md` | the moved README's Testing and "Add or change" sections | a root README that names the layout and links to `plugins/freethinking/README.md` and the contributing guide | 3.4 |
+
+### Manifest
+
+`plugins/freethinking/.claude-plugin/plugin.json`, fields in this order: `name`, `displayName`, `version`, `description`, `author`, `homepage`, `repository`, `license`, `icon`, `keywords`. Values as Requirement 2 gives them. `claude plugin validate ./plugins/freethinking --strict` is the check (Requirements 2.1 to 2.6).
+
+### Hook scripts
+
+| Script | Behaviour | Requirements |
+|--------|-----------|--------------|
+| `plugins/freethinking/hooks/load-rules.sh` | `[ -d "${CLAUDE_PLUGIN_ROOT:-}/rules" ] \|\| exit 0`; then one `awk` over `"${CLAUDE_PLUGIN_ROOT}"/rules/*.md` that prints the header, then every record with `\n`, a blank line before each file after the first and one after the last, so the text equals today's `cat` loop output | 4.1, 4.2, 4.4 |
+| `plugins/freethinking/hooks/load-rules-subagent.sh` | `[ -d "${CLAUDE_PLUGIN_ROOT:-}/rules" ] \|\| { echo "load-rules-subagent.sh: no rules under CLAUDE_PLUGIN_ROOT, so this sub-agent gets no rules" >&2; exit 1; }`; then one `awk` over the same files that prints the JSON prefix, the header and every record JSON-escaped with `\\n`, the same blank-line rule, and the JSON suffix | 4.1, 4.3, 4.4 |
+
+Both scripts keep `set -eu` and `#!/bin/sh`. Neither names `$0`, `$1`, `$f`, `$dir`, `$hooks` or `$text`. The `awk` escape table is the one in today's subagent script.
+
+### Makefile
+
+| Variable or target | Definition | Requirements |
+|--------------------|------------|--------------|
+| `PLUGIN := plugins/freethinking` | the Plugin_Folder | 5.1, 6.2 |
+| the results folder, set inside each recipe as `results=$$(pwd)/evals/results/$$(date +%Y-%m-%dT%H-%M-%S)` | one Results_Folder per eval recipe, made with `mkdir -p` before the run; computed in the recipe rather than in a make variable so two targets in one invocation get two folders and `make help` runs no `date` | 5.2 |
+| `validate: check` | `claude plugin validate . --strict`, `claude plugin validate ./$(PLUGIN) --strict`, `claude plugin validate ./$(PLUGIN)/skills --strict` | 6.1, 6.2 |
+| `RUN_EVAL` (a `define` the eval recipes expand) | `set -eu; results=$$(pwd)/evals/results/$$(date +%Y-%m-%dT%H-%M-%S); run=$$(mktemp -d "$${TMPDIR:-/tmp}/freethinking-eval.XXXXXX"); trap 'rm -rf "$$run"' EXIT; trap 'exit 130' HUP INT TERM; cp -R $(PLUGIN)/. "$$run"/; tar -C . --exclude=evals/results -cf "$$run/evals.tar" evals; tar -C "$$run" -xf "$$run/evals.tar"; rm "$$run/evals.tar"; mkdir -p "$$run/.github"; cp .github/pull_request_template.md "$$run/.github/"; git -C "$$run" init -q; mkdir -p "$$results"; claude plugin eval "$$run" --trust-plugin --ablation none --no-publish --max-cost-usd $(EVAL_MAX_COST_USD) --output-dir "$$results" $(EVAL_ARGS) $(CASE_ARGS) $(1)`. The copy of `evals/` is three commands rather than a pipeline, because under `/bin/sh` a pipeline hides the first `tar`'s failure from `set -e`. The pull request template is copied in because the `opens-pr-with-ticket` scaffold reads it from the repository top, the one file outside `evals/` any scaffold reads. The second `trap` turns a signal into an exit so the EXIT trap runs under `dash` too, where a signal alone would skip it | 5.1, 5.2, 5.3, 5.5 |
+| `eval`, `eval-hook`, `eval-skill-%`, `eval-rule-%`, `eval-agent-%` | each recipe is `$(call RUN_EVAL,--tag $*)` or the `eval` form with no tag; names, enumeration, per-target flags and `help` unchanged | 5.4 |
+
+The recipe is one shell line, so `set -e` and the `trap` hold for the whole run and the exit status of `claude plugin eval` is the recipe's status. The `PATH := bin:$(PATH)` exports stay as they are: `bin` is relative to the case sandbox the harness creates, not to the repository.
+
+### Checks
+
+| File | Change | Requirements |
+|------|--------|--------------|
+| `test_agents.py`, `test_descriptions.py`, `test_report_skill.py`, `test_rules_size.py` | `PLUGIN = ROOT / "plugins" / "freethinking"` (or the `os.path.join` form), one constant at the top of each file, and every `skills`, `agents`, `rules`, `hooks` path under it; `test_rules_size.py` sets `CLAUDE_PLUGIN_ROOT` to the Plugin_Folder | 4.6, 6.1 |
+| `test_issue_forms.py` | the skill, agent, rule and hook enumeration reads under `plugins/freethinking/`; `FORMS_DIR` stays at the root | 6.1 |
+| `test_listing.py` (new) | reads the Manifest; asserts `README.md` in the Plugin_Folder with at least 40 words outside fenced blocks; asserts `LICENSE` in the Plugin_Folder or a `license` field; opens the file the `icon` field names, checks the 8-byte PNG signature and reads width and height from the IHDR chunk with `struct`, and asserts square and 512 to 2048 | 8.1, 8.2, 8.3, 8.4 |
+| `test_eval_cases.py` | unchanged; `evals/` is still at the root | 5.6 |
+| `test_hook_encoder.py` | `roundtrip(part, text)` writes a temp folder with `rules/a.md` holding `text`, runs the subagent script with `CLAUDE_PLUGIN_ROOT` set to it, and compares the decoded `additionalContext` with the session script's output on the same folder, byte for byte; `test_fixed_table` and `test_seeded_fuzz` keep their inputs, with a file that does not end in a newline as one more fixed case; `test_real_run` runs both scripts on the Plugin_Folder and compares; a fourth test proves the no-rules branch: an empty temp folder gives the session script exit 0 with no output and the subagent script a non-zero exit with one stderr line | 4.4, 4.5 |
+
+The fixed table writes one file per case; the 500 fuzz strings are written twenty to a folder, as `rules/f00.md` to `rules/f19.md`, so the fuzz test makes 25 folders and 50 subprocesses and the whole check runs in about three seconds, and the multi-file blank-line separator is exercised on fuzz data as well as on the real rules.
+
+### Guides
+
+| File | Change | Requirements |
+|------|--------|--------------|
+| `plugins/freethinking/README.md` | the moved README minus Testing and "Add or change"; Install section without the read-access and `gh auth` paragraphs; the Disclosure_Section last, with the sentences Requirement 3.2 lists | 3.1, 3.2, 3.3, 3.5 |
+| `README.md` (root) | title, one paragraph on what the repository is, a short layout list (`plugins/freethinking/`, `evals/`, `checks/`, `.claude/specs/`), links to `plugins/freethinking/README.md` and `.github/CONTRIBUTING.md`, then "Add or change a skill or rule" and Testing, the Validator subsection naming the three validate commands and the Evals subsection saying an Eval_Target copies the plugin and `evals/` into a temporary tree and writes results to `evals/results/` | 3.4, 6.4 |
+| `.github/CONTRIBUTING.md` | `claude --plugin-dir ./plugins/freethinking`; the Test section's link to the root README's Testing section stays valid; one sentence that `evals/` sits outside the plugin and the Makefile copies it in for a run | 6.4 |
+| `evals/README.md` | "the Makefile runs `claude plugin eval` against a temporary copy of `plugins/freethinking/` with `evals/` inside it" in place of `claude plugin eval . --tag `; results path unchanged; the scaffold note that `git rev-parse --show-toplevel` resolves to the copy | 6.4 |
+
+## Data Models
+
+Manifest, the fields the Directory reads:
+
+| Field | Type | Value | Requirement |
+|-------|------|-------|-------------|
+| `name` | string | `freethinking`, unchanged | 1.4 |
+| `displayName` | string | `Freethinking` | 2.1 |
+| `version` | string | `0.13.0` | 2.4 |
+| `author` | object | `name`, `email`, `url` as Requirement 2.2 | 2.2 |
+| `homepage`, `repository` | string (URL) | the GitHub repository | 2.1 |
+| `license` | string | `MIT` | 2.1 |
+| `icon` | path | `./.claude-plugin/icon.png`, a 1024 by 1024 PNG | 2.1, 2.5 |
+| `keywords` | array of strings | the eight of Requirement 2.3 | 2.3 |
+
+Run_Tree, as the Makefile builds it:
+
+| Entry | Source | Notes |
+|-------|--------|-------|
+| `/` | `mktemp -d` under `$TMPDIR` or `/tmp` | deleted by the `trap` on exit |
+| `/.claude-plugin/`, `skills/`, … | `cp -R plugins/freethinking/.` | the plugin as installed |
+| `/evals/` | `tar` of `evals/` excluding `evals/results` | cases, fixtures, graders |
+| `/.github/pull_request_template.md` | `cp` from the repository root | read by the `opens-pr-with-ticket` scaffold |
+| `/.git` | `git init -q` | gives the scaffolds their top level |
+
+## Correctness Properties
+
+### Property 1: The plugin folder is the plugin
+
+*For any* commit after the move, the set of skill, agent, rule and hook names found under `plugins/freethinking/` equals the set found at the root before it, and no path under `plugins/freethinking/` starts with `evals/`, `checks/` or `.claude/specs/`.
+
+**Validates: Requirements 1.1, 1.3**
+
+### Property 2: Both hooks say the same rules
+
+*For any* folder with a `rules/` subfolder of Markdown files, the `additionalContext` the subagent script prints, JSON-decoded, equals the bytes the session script prints for the same folder.
+
+**Validates: Requirements 4.2, 4.3**
+
+### Property 3: An eval run leaves only results behind
+
+*For any* Eval_Target invocation, pass or fail, the repository differs afterwards only under `evals/results/`, and no Run_Tree remains under `$TMPDIR`.
+
+**Validates: Requirements 5.1, 5.2**
+
+### Property 4: Every named path exists
+
+*For any* backticked repository path in the root README, `plugins/freethinking/README.md`, the contributing guide and `evals/README.md`, a file or folder exists at that path after the move.
+
+**Validates: Requirement 6.6**
+
+## Error Handling
+
+| Condition | Category | Behavior | Requirements |
+|-----------|----------|----------|--------------|
+| `CLAUDE_PLUGIN_ROOT` unset or without `rules/` | MISSING_INPUT | session script exits 0 with no output; subagent script exits 1 with one stderr line | 4.4 |
+| A rule file without a trailing newline | EDGE | both scripts print it with a trailing newline, so their outputs stay equal | 4.3 |
+| `mktemp`, `cp`, `tar` or `git init` fails | BUILD_FAILURE | `set -e` stops the recipe with that command's error; the `trap` removes whatever was made; no eval runs | 5.5 |
+| `claude plugin eval` exits non-zero | EVAL_FAILURE | the `trap` removes the Run_Tree; the recipe exits with the same status; results already written stay | 5.1 |
+| A listing field missing or mistyped | VALIDATION | `claude plugin validate ./plugins/freethinking --strict` fails and so does `make validate` and CI | 2.6, 6.1 |
+| README under 40 words, no license, or an icon that is missing, not PNG, not square or outside 512 to 2048 px | VALIDATION | `test_listing.py` fails and so does `make validate` and CI | 8.1, 8.2, 8.3 |
+| A guide names a path that no longer exists | REVIEW | the wave reviewer reports it as a finding, fixed in the same wave | 6.6 |
+| The portal still holds a version | MANUAL | the author records the finding on issue #43 | 7.1, 7.2 |
+
+## Testing Strategy
+
+- Property 2 is proved by `checks/test_hook_encoder.py`: the fixed table, the 500 seeded fuzz strings and the real plugin, each written as a rules file and compared across the two scripts. Property 1 is an ephemeral check in the move's task: list the component names before and after and diff them.
+- Example tests cover each Error Handling row the checks can reach: the no-rules branch of both scripts in the encoder check, and the size limits in `test_rules_size.py` against `plugins/freethinking/`.
+- `test_listing.py` is proved on the real Plugin_Folder and by an ephemeral negative check in its task: a temp folder with a 300 px icon and a 10-word README fails each assertion.
+- `make validate` is the integration check for the layout, the checks and the manifest, and CI runs it on the pull request.
+- One eval invocation with a scaffolded case, `make eval-skill-pr EVAL_ARGS="--runs 1 --case opens-pr-with-stub-gh"`, proves Property 3 and Requirement 5.3 on the real suite: the results folder appears at the root, the scaffold ran from the copy, and nothing is left in `$TMPDIR`. One `make eval-hook EVAL_ARGS="--runs 1"` proves the rewritten hooks load the rules in a session and a sub-agent.
+- A local install proves Requirement 1.6: `claude plugin marketplace add ` from another folder, `claude plugin install freethinking@freethinkingit`, and `claude plugin details freethinking` listing five skills, two agents and two hooks.
+- Requirement 7 is the author's manual check in the developer portal after the merge, recorded on issue #43.
diff --git a/.claude/specs/directory-listing/requirements.md b/.claude/specs/directory-listing/requirements.md
new file mode 100644
index 0000000..060dbd6
--- /dev/null
+++ b/.claude/specs/directory-listing/requirements.md
@@ -0,0 +1,122 @@
+# Requirements Document
+
+## Introduction
+
+The plugin is submitted to Anthropic's plugin directory, which scans every file in the folder that holds the manifest. Today that folder is the repository root, so the scan reads the eval suite and the spec records as if an installed plugin ran them, and holds the submission for a reviewer on each version. This task moves the plugin into a folder of its own, leaves the test and design material outside it, gives the manifest the fields the listing shows, writes down what the plugin runs and sends, and keeps every check and eval target working from the new layout. Contributors keep the same commands; users keep the same install id; the directory's reviewer sees only the plugin.
+
+## Glossary
+
+- **Plugin_Folder**: The folder `plugins/freethinking/` at the repository root that holds the Manifest and every file an installed plugin loads: `skills/`, `agents/`, `rules/`, `hooks/`, the plugin README and the Icon.
+- **Repository_Root**: The top folder of the repository, holding the Marketplace_File, `evals/`, `checks/`, `.claude/specs/`, the Makefile, the CI workflow, the contributing guide, the LICENSE and the root README.
+- **Manifest**: The file `plugins/freethinking/.claude-plugin/plugin.json`.
+- **Marketplace_File**: The file `.claude-plugin/marketplace.json` at the Repository_Root, whose one entry names the plugin and points at the Plugin_Folder.
+- **Listing_Fields**: The Manifest fields the Directory shows: `displayName`, `homepage`, `repository`, `icon`, `keywords`, `author` and `version`.
+- **Icon**: The square PNG at `plugins/freethinking/.claude-plugin/icon.png`, 1024 pixels on each side.
+- **Directory**: Anthropic's plugin directory, submitted to from the developer portal at claude.ai/directory/manage.
+- **Validator_Shape**: The form of a hook script that the Directory's validator follows when the Plugin_Folder is a subfolder: every path written in full from `${CLAUDE_PLUGIN_ROOT}`, no other shell variable, no command substitution, no package launcher, and no call to another file in the plugin.
+- **Hook_Script**: One of `hooks/load-rules.sh` and `hooks/load-rules-subagent.sh` in the Plugin_Folder.
+- **Run_Tree**: A temporary folder, built by the Makefile for one eval invocation, holding a copy of the Plugin_Folder's contents with a copy of `evals/` inside it and a git repository initialised at its top.
+- **Results_Folder**: The folder `evals/results//` at the Repository_Root where one eval invocation writes `aggregate-result.json` and `report.html`.
+- **Free_Check**: A `unittest` file under `checks/`, run by `make check` and `make validate` with no model call.
+- **Disclosure_Section**: The last section of the plugin README, titled "What the plugin runs and sends", written for the Directory's reviewer.
+- **Listing_Check**: The Free_Check `checks/test_listing.py` that proves the directory's mechanical listing rules on the Plugin_Folder: the README length, the license and the Icon.
+- **Eval_Target**: Any of `make eval`, `make eval-hook`, `make eval-skill-`, `make eval-rule-` and `make eval-agent-`.
+
+## Requirements
+
+### Requirement 1: The plugin lives in its own folder
+
+**User Story:** As the plugin's maintainer, I want every file an installed plugin loads in one folder and nothing else in it, so that the Directory scans the plugin and not the repository.
+
+#### Acceptance Criteria
+
+1. THE Plugin_Folder SHALL hold the Manifest, the Icon, `skills/`, `agents/`, `rules/`, `hooks/` and a `README.md`, each moved from the Repository_Root with its content otherwise unchanged except where a later requirement says.
+2. THE Repository_Root SHALL keep the Marketplace_File, `evals/`, `checks/`, `.claude/specs/`, `Makefile`, `.github/`, `LICENSE` and `.gitignore` where they are.
+3. THE Plugin_Folder SHALL contain no file under `evals/`, `checks/` or `.claude/specs/`, and no symbolic link.
+4. THE Marketplace_File SHALL keep its `name` `freethinkingit` and its entry's `name` `freethinking`, and SHALL point the entry's `source` at `./plugins/freethinking`.
+5. WHEN `claude plugin validate . --strict` runs at the Repository_Root and `claude plugin validate ./plugins/freethinking --strict` runs, THE System SHALL print `Validation passed` for each with no warning.
+6. WHEN a machine with the plugin installed from this marketplace runs `claude plugin update freethinking@freethinkingit`, THE System SHALL install the new version under the same install id, with every skill, agent, rule and hook loaded.
+
+### Requirement 2: The manifest carries the listing fields
+
+**User Story:** As a developer browsing the Directory, I want the listing to show the plugin's name, publisher, icon, links and tags, so that I can judge it before installing.
+
+#### Acceptance Criteria
+
+1. THE Manifest SHALL set `displayName` to `Freethinking`, `homepage` and `repository` to `https://github.com/freethinkingit/dev_tools`, `icon` to `./.claude-plugin/icon.png`, and `license` to `MIT`.
+2. THE Manifest SHALL set `author` to name `FreeThinking IT`, email `hello@freethinking.it` and url `https://freethinking.it`.
+3. THE Manifest SHALL set `keywords` to `developer-workflow`, `feature-development`, `bug-fixing`, `pull-requests`, `code-review`, `spec-driven`, `engineering-standards` and `accountability`.
+4. THE Manifest SHALL set `version` to `0.13.0`.
+5. THE Icon SHALL be a PNG of 1024 by 1024 pixels at the path the Manifest names.
+6. IF a Listing_Field is absent, misspelt or not of the type the manifest reference gives, THEN `claude plugin validate ./plugins/freethinking --strict` SHALL fail, and THE System SHALL not be submitted.
+
+### Requirement 3: The plugin README discloses what runs and sends
+
+**User Story:** As the Directory's reviewer, I want the README to say what the plugin runs, reads and sends, so that the security scan's holds can be confirmed against a stated behaviour.
+
+#### Acceptance Criteria
+
+1. THE plugin README SHALL end with the Disclosure_Section.
+2. THE Disclosure_Section SHALL state that the `pr` and `bugfix` skills read issues and open pull requests through the `gh` command already signed in on the user's machine and run `git` on the current repository; that the `SessionStart` and `SubagentStart` hooks read only the files under the plugin's `rules/`; that no skill, agent or hook reads a credential, runs a package launcher or sends anything anywhere else; and that the repository's `evals/` and `.claude/specs/` folders are the test suite and the design records, outside the plugin and never run by an installed plugin.
+3. THE plugin README's Install section SHALL describe a public repository: no mention of read access, `gh auth login` or `gh auth setup-git`.
+4. THE root README SHALL say what the repository holds, link to the plugin README and the contributing guide, and carry the Testing section that the plugin README holds today, since the checks and evals belong to the repository and not to the installed plugin.
+5. THE plugin README SHALL keep the Install, Stay up to date, Skills, Rules, Agents and "Tune the plugin for a repository" sections as they are today.
+
+### Requirement 4: The hook scripts take the validator's shape
+
+**User Story:** As the plugin's maintainer, I want the two hook scripts to be ones the Directory's validator follows, so that a submission from a subfolder carries no hold for an unfollowed script.
+
+#### Acceptance Criteria
+
+1. THE Hook_Scripts SHALL each be in Validator_Shape.
+2. WHEN `hooks/load-rules.sh` runs with `CLAUDE_PLUGIN_ROOT` set to the Plugin_Folder, THE System SHALL print the "Handbook rules" heading and its introductory sentence followed by the content of every `rules/*.md` file, in name order, each followed by a newline, exactly as the script prints today.
+3. WHEN `hooks/load-rules-subagent.sh` runs with `CLAUDE_PLUGIN_ROOT` set to the Plugin_Folder, THE System SHALL print one JSON object whose `hookSpecificOutput.additionalContext` is the exact text of criterion 2, with every byte of it reproduced after JSON decoding, trailing newlines and non-ASCII text included.
+4. IF `CLAUDE_PLUGIN_ROOT` is unset or names a folder with no `rules/`, THEN `hooks/load-rules.sh` SHALL exit 0 and print nothing, and `hooks/load-rules-subagent.sh` SHALL exit non-zero with one line on stderr saying the sub-agent gets no rules.
+5. THE Free_Check for the JSON encoder SHALL prove criterion 3 by running the subagent Hook_Script against a fixture rules folder it writes itself, covering quotes, backslashes, control characters, non-ASCII text and trailing newlines, in place of feeding the script on stdin.
+6. THE Free_Check for the rules size SHALL keep its limit on the session Hook_Script's output and on each rule file, run against the Plugin_Folder.
+
+### Requirement 5: The eval targets run from a copy
+
+**User Story:** As a contributor, I want `make eval--` to keep working with the evals outside the plugin, so that moving them costs no change to a case.
+
+#### Acceptance Criteria
+
+1. WHEN an Eval_Target runs, THE Makefile SHALL build a Run_Tree, run `claude plugin eval` against it with the same flags, tags and `EVAL_ARGS` as today, and delete the Run_Tree afterwards, whether the eval passed or failed.
+2. WHEN an Eval_Target runs, THE System SHALL write the run's `aggregate-result.json` and `report.html` to a Results_Folder at the Repository_Root, and nothing to `evals/` inside the Run_Tree that survives it.
+3. WHEN a case's scaffold runs in the Run_Tree, THE System SHALL resolve `git rev-parse --show-toplevel` to the Run_Tree, so that `$ROOT/evals/fixtures/setup.sh` is the copy inside it and no scaffold or fixture file changes.
+4. THE Eval_Targets SHALL keep their names, the enumeration from the `evals/` folders, the per-target flags, `EVAL_MAX_COST_USD`, `EVAL_ARGS` and `make help`'s output.
+5. IF the Run_Tree cannot be built, THEN THE Makefile SHALL stop with the failing command's error and run no eval.
+6. THE Free_Check for eval cases SHALL keep reading the cases under `evals/` at the Repository_Root.
+
+### Requirement 6: The free checks and the guides follow the layout
+
+**User Story:** As a contributor, I want `make validate` and the guides to describe the repository as it is, so that a change to one part still reaches every part it must.
+
+#### Acceptance Criteria
+
+1. THE Free_Checks that read `skills/`, `agents/`, `rules/`, `hooks/` or the Manifest SHALL read them under the Plugin_Folder, and `make validate` SHALL pass.
+2. THE `validate` target SHALL run `make check`, then `claude plugin validate . --strict` for the Marketplace_File and `claude plugin validate ./plugins/freethinking --strict` for the plugin.
+3. THE CI workflow SHALL keep running `make validate` unchanged.
+4. THE contributing guide, the root README, the plugin README and `evals/README.md` SHALL name paths as they are after the move, including that the plugin loads for one session with `claude --plugin-dir ./plugins/freethinking`, and SHALL say that `evals/` sits outside the plugin and how an Eval_Target runs it.
+5. THE Makefile SHALL name the Plugin_Folder in one `PLUGIN` variable, and each Free_Check that reads the Plugin_Folder SHALL name it in one constant at its top, so that adding a second plugin under `plugins/` is one edit per file.
+6. IF a backticked repository path in the contributing guide, the root README, the plugin README or `evals/README.md` names a file or folder that does not exist after the move, THEN the wave review SHALL report it as a finding to fix in this task.
+
+### Requirement 7: The portal confirms the result
+
+**User Story:** As the plugin's maintainer, I want the developer portal's validation of the new layout to carry no hold, so that each version goes live without a reviewer wait.
+
+#### Acceptance Criteria
+
+1. WHEN the author validates the repository in the Directory's developer portal with the plugin path `plugins/freethinking` after the change merges, THE report SHALL show no finding with the result "Policy hold" and no finding with the result "Blocks".
+2. WHILE a warning remains in that report, THE System SHALL record the warning's title and cause as a comment on issue #43, so the next change knows what is left.
+
+### Requirement 8: The listing rules are checked before the portal
+
+**User Story:** As the plugin's maintainer, I want the directory's mechanical listing rules checked by `make validate`, so that a wrong icon, which the portal takes only once, never reaches a submission.
+
+#### Acceptance Criteria
+
+1. THE Listing_Check SHALL fail when the Plugin_Folder has no `README.md`, or when that README holds fewer than 40 words outside fenced code blocks.
+2. THE Listing_Check SHALL fail when the Plugin_Folder has no `LICENSE` file and the Manifest has no `license` field.
+3. THE Listing_Check SHALL fail when the Manifest's `icon` names no file, or names a file that is not a PNG, not square, or not between 512 and 2048 pixels on each side, read from the PNG header with the standard library only.
+4. THE Listing_Check SHALL pass on the Plugin_Folder as this task leaves it, and `make validate` SHALL run it with the other Free_Checks.
diff --git a/.claude/specs/directory-listing/tasks.md b/.claude/specs/directory-listing/tasks.md
new file mode 100644
index 0000000..a270bf5
--- /dev/null
+++ b/.claude/specs/directory-listing/tasks.md
@@ -0,0 +1,74 @@
+# Implementation Plan
+
+## Overview
+
+The move comes first, because every other task writes paths under `plugins/freethinking/`. Then, in one wave, the manifest, the hook scripts, the re-rooted checks, the Makefile and the four guides, which touch disjoint files. The two checks that depend on new content, the encoder check on the rewritten hooks and the listing check on the new manifest, follow. The last wave is the author's: the eval runs that cost tokens and the local install. Every task is proved by `make validate` plus the ephemeral check its text names; the eval runs are the integration proof. Requirement 7, the portal's validation, happens after the merge and is recorded on issue #43, so it is not a task here.
+
+## Tasks
+
+- [x] 1. Move the plugin into `plugins/freethinking/`
+ - `git mv` `.claude-plugin/plugin.json`, `skills/`, `agents/`, `rules/`, `hooks/` and `README.md` under `plugins/freethinking/`, and move the untracked `.claude-plugin/icon.png` to `plugins/freethinking/.claude-plugin/icon.png`; leave `.claude-plugin/marketplace.json` at the root and set its entry's `source` to `./plugins/freethinking`. Ephemeral check for Property 1: list the skill, agent, rule and hook names at the old and new paths and diff them; `claude plugin validate . --strict` passes
+ - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
+
+- [x] 2. Fill the manifest's listing fields
+ - In `plugins/freethinking/.claude-plugin/plugin.json`, set `displayName`, `version` 0.13.0, `author`, `homepage`, `repository`, `license`, `icon` and `keywords` to the values of Requirement 2, in the field order the design gives; `claude plugin validate ./plugins/freethinking --strict` passes with no warning
+ - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6_
+
+- [x] 3. Rewrite the hook scripts to the validator's shape
+ - Rewrite `plugins/freethinking/hooks/load-rules.sh` and `load-rules-subagent.sh` as the design's table says: one `awk` program each over `"${CLAUDE_PLUGIN_ROOT}"/rules/*.md`, no other variable, no command substitution, no call between them, the header written in both with a comment naming the other, and the no-rules branches of Requirement 4.4. Ephemeral check: run the old scripts from `git show HEAD:` and the new ones on the real rules folder and diff the session output and the decoded subagent context, byte for byte
+ - _Requirements: 4.1, 4.2, 4.3, 4.4_
+
+- [x] 4. Re-point the free checks
+ - [x] 4.1 Re-root the existing checks on the plugin folder
+ - In `test_agents.py`, `test_descriptions.py`, `test_report_skill.py`, `test_rules_size.py` and `test_issue_forms.py`, add one `PLUGIN` constant at the top and read `skills/`, `agents/`, `rules/`, `hooks/` under it; `test_rules_size.py` sets `CLAUDE_PLUGIN_ROOT` to it; `test_eval_cases.py` is untouched; `make check` passes
+ - _Requirements: 4.6, 6.1, 6.5_
+ - [x] 4.2 Rewrite the encoder check around a fixture rules folder
+ - In `test_hook_encoder.py`, make `roundtrip` write a temp folder with `rules/a.md`, run both scripts with `CLAUDE_PLUGIN_ROOT` set to it and compare byte for byte; keep the fixed table and the seeded fuzz, add a file with no trailing newline to the fixed table, make `test_real_run` compare both scripts on the plugin folder, and add the no-rules test of Requirement 4.4; `make check` passes
+ - _Requirements: 4.4, 4.5_
+ - [x] 4.3 Add the listing check
+ - Write `checks/test_listing.py` as the design's Checks row says: README of at least 40 words outside fenced blocks, `LICENSE` or a `license` field, and the icon a square PNG of 512 to 2048 px read from the IHDR chunk with `struct`. Ephemeral negative check: a temp folder with a 300 px PNG and a 10-word README fails each assertion; `make check` passes on the real folder
+ - _Requirements: 8.1, 8.2, 8.3, 8.4_
+
+- [x] 5. Teach the Makefile the layout and the Run_Tree
+ - Add `PLUGIN := plugins/freethinking`; make `validate` run `check`, then the three strict validate commands; add the `RUN_EVAL` define of the design, with a per-recipe results folder, `mktemp`, the traps, `cp -R` of the plugin, `tar` of `evals/` without `results/` and no pipeline, the pull request template, `git init`, `mkdir -p` of the results folder and `claude plugin eval` with `--output-dir`; expand it in every eval recipe with the same tags; keep the per-target flags, `help` and the `PATH` exports. Ephemeral check: `make -n eval-skill-pr` shows the recipe, and `make validate` passes
+ - _Requirements: 5.1, 5.2, 5.4, 5.5, 6.2, 6.3, 6.5_
+
+- [x] 6. Update the guides
+ - [x] 6.1 The plugin README
+ - In `plugins/freethinking/README.md`, drop the read-access sentence and the `gh auth` paragraph from Install, cut the "Add or change a skill or rule" and Testing sections, and add the Disclosure_Section last with the sentences of Requirement 3.2; every backticked path in the file exists
+ - _Requirements: 3.1, 3.2, 3.3, 3.5_
+ - [x] 6.2 The root README
+ - Write a new `README.md` at the root: the title, what the repository is, the layout list, links to `plugins/freethinking/README.md` and `.github/CONTRIBUTING.md`, then the "Add or change a skill or rule" and Testing sections moved from the plugin README, with the Validator subsection naming the three validate commands and the Evals subsection saying how an Eval_Target copies the plugin and `evals/` into a temporary tree; every backticked path in the file exists
+ - _Requirements: 3.4, 6.4_
+ - [x] 6.3 The contributing guide and the evals README
+ - In `.github/CONTRIBUTING.md`, `claude --plugin-dir ./plugins/freethinking` and one sentence on `evals/` sitting outside the plugin; in `evals/README.md`, the Makefile's copy in place of `claude plugin eval . --tag ` and the note that `git rev-parse --show-toplevel` resolves to the copy; every backticked path in both files exists
+ - _Requirements: 6.4_
+
+- [x] 7. Prove the run tree and the install by hand
+ - [x] 7.1 Run two eval targets and keep the tables
+ - The author runs `make eval-skill-pr EVAL_ARGS="--runs 1 --case opens-pr-with-stub-gh"` and `make eval-hook EVAL_ARGS="--runs 1"`, confirms the results folder appeared under `evals/results/`, that nothing was left under `$TMPDIR`, and pastes both tables into the PR's Testing section
+ - **Property 3: An eval run leaves only results behind**
+ - **Validates: Requirements 5.1, 5.2, 5.3**
+ - [x] 7.2 Install from the checkout
+ - The author registers the checkout as a local marketplace from another folder, installs `freethinking@freethinkingit`, runs `claude plugin details freethinking` and records that it lists five skills, two agents and two hooks, then removes the probe marketplace
+ - _Requirements: 1.6_
+
+## Notes
+
+- Every task runs `make validate` before it is done; a task's ephemeral check is created in a scratch location and deleted afterwards.
+- Task 7's two leaves are actions for the author, since the eval runs cost tokens and the install touches the author's own Claude settings; they get no sub-agent.
+- The spec records (`.claude/specs/`) are not edited to match the move; they stay as written.
+- Requirement 7, the portal's validation after merge, is recorded on issue #43 and is not a task.
+
+## Task Dependency Graph
+
+```json
+{
+ "waves": [
+ { "id": 0, "tasks": ["1"] },
+ { "id": 1, "tasks": ["2", "3", "4.1", "5", "6.1", "6.2", "6.3"] },
+ { "id": 2, "tasks": ["4.2", "4.3"] },
+ { "id": 3, "tasks": ["7.1", "7.2"] }
+ ]
+}
+```
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index ba24e1d..8f1967f 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1,7 +1,8 @@
# Contributing
-This repository is a Claude Code plugin: shared skills, agents and rules for a
-software team; its hooks put the rules in every session's context. This guide
+This repository holds a Claude Code plugin, in `plugins/freethinking/`: shared
+skills, agents and rules for a software team, whose hooks put the rules in every
+session's context. This guide
says how to set up, test and submit a change, and how the repository uses
issues, labels, milestones and discussions.
@@ -14,19 +15,24 @@ the [Code of Conduct](CODE_OF_CONDUCT.md). Report a vulnerability as the
Clone the repository and work from its root:
```bash
-claude --plugin-dir . # load your working copy for one session
-make validate # the unit tests and the validators; CI runs this on every PR
+claude --plugin-dir ./plugins/freethinking # load your working copy for one session
+make validate # the unit tests and the validators; CI runs this on every PR
```
## Test
-The free checks are `make validate`: the unit tests under `checks/`, then the
-two validators. They make no model call.
+The free checks are `make validate`: the unit tests under `checks/`, then
+three `claude plugin validate` runs, on `.`, `./plugins/freethinking` and
+`./plugins/freethinking/skills`. They make no model call.
Evals run by component with `make eval--`, such as
`make eval-skill-pr`, and `make eval-hook` for the hooks. Every case is a model
call on your own Claude seat, so evals run by hand, never in CI. Paste the
table the command prints into the Testing section of your pull request.
+`evals/` sits outside the plugin folder, so Anthropic's plugin directory, which
+scans every file in the plugin folder when the plugin is submitted, sees only
+the plugin, and each `make eval-*` target copies the plugin and `evals/` into a
+temporary tree to run them.
A change to a skill, agent, rule or hook comes with an eval case or a change to
one. A new skill comes with at least one case under `evals/skills//`. A
diff --git a/Makefile b/Makefile
index c7fbf82..62923c7 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
# Checks for the handbook plugin.
#
-# make validate free: check, manifests and skill frontmatter (CI runs this)
+# make validate free: check, marketplace, plugin manifest and skill frontmatter (CI runs this)
# make check free: the Python checks under checks/, such as the hook's JSON encoder
# make help list the eval targets that exist
# make eval every eval case
@@ -19,18 +19,42 @@
# The eval-skill-*, eval-rule-* and eval-agent-* targets are enumerated from
# the evals/ directories and select their cases with --tag, so every case
# carries its component's name as a tag and a new case needs no Makefile
-# change. A case that needs its own flags gets one line in the "Per-target
-# flags" block below; those flags are kept in CASE_ARGS so EVAL_ARGS on the
-# command line still adds to them. The cost ceiling is per invocation, so raise
-# it for the whole suite: make eval EVAL_MAX_COST_USD=5. Results go to
-# evals/results// (gitignored). Paste the table the command prints
-# into the PR's Testing section.
+# change. The runner reads cases only from inside the plugin root, and the
+# plugin lives in $(PLUGIN) with evals/ beside it at the repository root, so
+# each eval target runs claude plugin eval against a temporary copy of the
+# plugin with evals/ copied inside it and a git repository initialised at its
+# top, for the scaffolds that find evals/fixtures/ through git rev-parse. The
+# pr scaffold also reads .github/pull_request_template.md from that top, so
+# that one file is copied in too. The copy is deleted when the run ends, pass or fail. A case that needs its own
+# flags gets one line in the "Per-target flags" block below; those flags are
+# kept in CASE_ARGS so EVAL_ARGS on the command line still adds to them. The
+# cost ceiling is per invocation, so raise it for the whole suite: make eval
+# EVAL_MAX_COST_USD=5. Results go to evals/results// at the
+# repository root (gitignored). Paste the table the command prints into the
+# PR's Testing section.
+PLUGIN := plugins/freethinking
EVAL_MAX_COST_USD ?= 2
EVAL_ARGS ?=
CASE_ARGS :=
-EVAL = claude plugin eval . --trust-plugin --ablation none --no-publish \
- --max-cost-usd $(EVAL_MAX_COST_USD) $(EVAL_ARGS) $(CASE_ARGS)
+
+define RUN_EVAL
+set -eu; \
+results=$$(pwd)/evals/results/$$(date +%Y-%m-%dT%H-%M-%S); \
+run=$$(mktemp -d "$${TMPDIR:-/tmp}/freethinking-eval.XXXXXX"); \
+trap 'rm -rf "$$run"' EXIT; trap 'exit 130' HUP INT TERM; \
+cp -R $(PLUGIN)/. "$$run"/; \
+tar -C . --exclude=evals/results -cf "$$run/evals.tar" evals; \
+tar -C "$$run" -xf "$$run/evals.tar"; \
+rm "$$run/evals.tar"; \
+mkdir -p "$$run/.github"; \
+cp .github/pull_request_template.md "$$run/.github/"; \
+git -C "$$run" init -q; \
+mkdir -p "$$results"; \
+claude plugin eval "$$run" --trust-plugin --ablation none --no-publish \
+ --max-cost-usd $(EVAL_MAX_COST_USD) --output-dir "$$results" \
+ $(EVAL_ARGS) $(CASE_ARGS) $(1)
+endef
SKILLS := $(notdir $(patsubst %/,%,$(wildcard evals/skills/*/)))
RULES := $(notdir $(patsubst %/,%,$(wildcard evals/rules/*/)))
@@ -43,7 +67,8 @@ AGENT_TARGETS := $(addprefix eval-agent-,$(AGENTS))
validate: check
claude plugin validate . --strict
- claude plugin validate ./skills --strict
+ claude plugin validate ./$(PLUGIN) --strict
+ claude plugin validate ./$(PLUGIN)/skills --strict
check:
python3 -m unittest discover -s checks
@@ -78,16 +103,16 @@ eval: CASE_ARGS += --allow-tools Bash Write Edit
eval: export PATH := bin:$(PATH)
eval:
- $(EVAL)
+ $(call RUN_EVAL)
eval-hook:
- $(EVAL) --tag hook
+ $(call RUN_EVAL,--tag hook)
$(SKILL_TARGETS): eval-skill-%:
- $(EVAL) --tag $*
+ $(call RUN_EVAL,--tag $*)
$(RULE_TARGETS): eval-rule-%:
- $(EVAL) --tag $*
+ $(call RUN_EVAL,--tag $*)
$(AGENT_TARGETS): eval-agent-%:
- $(EVAL) --tag $*
+ $(call RUN_EVAL,--tag $*)
diff --git a/README.md b/README.md
index f807bf8..6b7e105 100644
--- a/README.md
+++ b/README.md
@@ -1,161 +1,48 @@
# Developer Handbook
-Shared Claude Code skills and rules for a software team, packaged as a plugin.
-Install it once and the team's standard workflows and development rules are
-available in every repo you work in.
-
-## Install
-
-You need [Claude Code](https://claude.com/claude-code) and read access to this
-repo. From any terminal:
-
-```bash
-claude plugin marketplace add freethinkingit/dev_tools
-claude plugin install freethinking@freethinkingit
-```
-
-The plugin loads the next time you start Claude Code. In a session that is
-already open, run `/reload-plugins`.
-
-The clone uses the git credentials already on your machine and never prompts.
-If the first command fails on access, sign in with `gh auth login`, run
-`gh auth setup-git`, and try again.
-
-## Stay up to date
-
-Auto-update is off by default for team marketplaces. Turn it on once:
-
-1. Run `/plugin` in a Claude Code session.
-2. Open the **Marketplaces** tab and select `freethinkingit`.
-3. Select **Enable auto-update**.
-
-To update by hand:
-
-```bash
-claude plugin update freethinking@freethinkingit
-```
-
-## Skills
-
-| Skill | Run it with | What it does |
-| ----- | ----------- | ------------ |
-| `start-task` | `/freethinking:start-task `, or ask Claude to change something, or to start or spec a task | The front door for any change: reads the code the request touches, then sends one contained change to the `task-executor` agent, a defect to `bugfix` and everything else to a spec, asking you before taking either of the first two. On the spec route, writes `requirements.md`, `design.md` and `tasks.md` in `.claude/specs//` before any code, following the team template, with you approving each document before the next. |
-| `implement-spec` | `/freethinking:implement-spec [task] [task\|wave\|spec]`, or ask Claude to implement, build or continue a spec | Builds a task from its approved spec, wave by wave from `tasks.md`, each leaf task in its own sub-agent, ticking a task only once its tests pass, running the full suite at the end of every wave, and handing back to you after each task, after each wave, or only once every wave is done (with a short report after each wave and a final review of the whole run), whichever you chose. Each discovery outside the task in hand is listed with its size and the choice between a task and an issue. Pauses with a proposal when the design cannot be followed, and hands off to `pr` when the spec is done. |
-| `bugfix` | `/freethinking:bugfix [what you already know]`, or ask Claude to fix a bug | Reads the ticket, and anything you already know that you put after it, reproduces the bug before touching code, confirms the root cause with you, fixes the cause with a regression test, verifies the fix, then hands off to `pr` or leaves its findings on the ticket. |
-| `pr` | `/freethinking:pr [ticket] [base-branch]`, or ask Claude to open a PR | Takes the ticket as its first argument and reads it, offers to create the branch and commit if needed, walks the branch's changes with you one at a time with a short set of review prompts, drafts the title and description from your answers, and opens the PR with `gh` once you approve. Needs the `gh` CLI signed in. |
-| `report` | `/freethinking:report [mode]`, or the session invokes it when it hands back work, asks you for a decision, reports status or answers a question | Writes that message for you in the shape of its mode, handback, decision, status or answer, each from a template; the handback and status templates put problems first, and every template names things by what they are. A repository can replace a template under `.freethinking/report/`. The skill edits nothing. |
-
-## Rules
-
-The plugin also loads a short set of team rules into every Claude Code session,
-in every repository, so Claude follows them without being asked. They live in
-`rules/`:
-
-| File | Covers |
-| ---- | ------ |
-| `rules/workflow.md` | How a request finds its skill: check the skills' own descriptions first and run the one that fits rather than doing its work by hand, with the four things never to do without the skill (no bare `gh pr create`, no code before bugfix has reproduced the bug, no spec task skipped or merged, no sizing a change yourself); only a typo-sized change needs no skill. Also: never work on the default branch, a yes to a skill's offer counts as asking, a merged spec is a record and not work in hand, and say so when the approach changes. |
-| `rules/code-changes.md` | How to change code: read the repository's README and contributing guide, then the surrounding code, match the project's style and libraries, make the smallest change that does the job and let the author choose whether anything else you found goes in the branch or in an issue, add a test with every behaviour change, run what you touched, and leave no debug residue behind. |
-| `rules/comments.md` | How to comment: docstrings on public contracts only, saying intent, parameters, returns and exceptions; inline comments only for non-obvious details such as workarounds and edge cases, never for what a line does; no divider or closing-brace comments; no commented-out dead code. |
-| `rules/collaboration.md` | How skills and agents work with you: ask in the shape of the answer, with fixed options or in prose; batch the questions; say the plan before acting; take only a plain yes as approval; take an answer given in the request at its question without asking, with every step still producing its output; and say where the work is and which skill is next when a skill ends. |
-| `rules/communication.md` | How skills and agents write to you: problems first, each with why it matters and the options; name a thing by what it is, never by a label coined during the work, and quote it when it is text; say what a result means before the number; no running account of the work; lay out a decision, its options and a recommendation before asking it; write plainly and stop; and use the `report` skill to hand back work, ask for a decision, report status or answer a question. |
-
-Claude Code does not load a `CLAUDE.md` or a `rules/` folder from a plugin by
-itself, so `hooks/hooks.json` registers two hooks. The `SessionStart` hook runs
-`hooks/load-rules.sh`, which prints every `.md` file under `rules/` into the
-session context on every session start, whatever started it (a new session,
-a resume, a clear, a compact or a fork). The `SubagentStart` hook runs
-`hooks/load-rules-subagent.sh`, which puts the same rules into every sub-agent,
-which is how both agents below get them. Keep the
-combined rules under 10,000 characters; Claude Code truncates hook output past
-that, and `make validate` fails when they go over, or when `collaboration` or
-`communication` goes over its own limit of 1,600 characters. A rule a repository needs
-to override belongs in that repository's own `CLAUDE.md`, which Claude reads
-alongside these.
-
-## Agents
-
-The plugin's agents live under `agents/`. `implement-spec` launches them,
-`start-task` launches the executor for one contained change, and you can
-launch either directly: mention it in a session as
-`@agent-freethinking:task-executor` or `@agent-freethinking:wave-reviewer`, or have a
-skill or session launch it with the Agent tool as
-`subagent_type: freethinking:task-executor` or `freethinking:wave-reviewer`.
-
-What the prompt carries:
-
-- **`task-executor`**: the change, in your own words. When the task comes from
- a spec, say so in the prompt; that sentence switches it to spec mode, where
- it writes no test unless the task asks for one.
-- **`wave-reviewer`**: the files that changed, or a git range; the requirement
- or design text they implement, if there is any; the files that cite them;
- and, when they come from a spec, the sentence "These changes come from a
- spec wave", which is the only thing that waives a missing test.
-
-| Agent | What it does | What it returns |
-| ----- | ------------ | --------------- |
-| `task-executor` | Builds one well-specified change as the smallest change in the project's style, proves it, and deletes its scratch work. Stops and asks instead of guessing when the task is unclear, the design does not fit the code, or the task would need a new dependency, framework or tool. `start-task` launches it for one contained change, and `implement-spec` for every leaf task. | A Summary: `status` (`done` or `stopped`), `files changed`, `checks run` with their results, the ephemeral check it ran, or why none applied, what it is `unsure` of, and, when stopped, the question or evidence under `stopped because`. |
-| `wave-reviewer` | Reviews one set of changes against the requirements and design they implement and the files that cite them, gathering the diff itself and changing nothing. `implement-spec` launches it at the end of every wave, and once more over the whole run when you chose to hand back only at the end. | Findings: a `verdict` (`holds` or `findings`), the `base` commit the diffs were measured against, then each problem with its `file`, an exact `quote`, the `problem` and a `severity` (`blocker`, `should-fix` or `nit`), and what it is `unsure` of. |
-
-Both agents follow one protocol for their reports, and a new agent must too:
-its report opens with its status on the first line, `status: done` or `stopped`
-for the executor and `verdict: holds` or `findings` for the reviewer, has one
-field for each thing the launcher must act on, always has an `unsure` field,
-quotes exactly whatever it quotes, and has nothing after the shape.
-
-The executor works in one of two modes, set by the launch prompt alone:
-
-- **Spec mode**, when the prompt says the task comes from a spec, in any words;
- `implement-spec` says "This task is a leaf task from a spec; tests are tasks
- of their own." Tests are tasks of their own there, so it writes a test only
- when the task asks for one, and proves a code task with an ephemeral check
- that it deletes and reports in its Summary.
-- **Standalone mode**, otherwise. The change comes with its test, written the
- way the project writes tests.
-
-The reviewer treats a missing test as a finding unless the launch says the
-changes come from a spec wave.
+This repository holds the `freethinking` Claude Code plugin, its eval suite,
+its free checks and its spec records. To install and use the plugin, read
+[the plugin README](plugins/freethinking/README.md). To change it, read
+[the contributing guide](.github/CONTRIBUTING.md).
+
+- `plugins/freethinking/`: the plugin itself, with its manifest, skills,
+ agents, rules, hooks and its README.
+- `evals/`: the eval cases, kept outside the plugin folder because Anthropic's
+ plugin directory scans every file in that folder when the plugin is
+ submitted.
+- `checks/`: the free unit tests.
+- `.claude/specs/`: the design records of past tasks.
+- `.claude-plugin/marketplace.json`: the marketplace entry that points at the
+ plugin folder.
## Add or change a skill or rule
-Each skill is a folder under `skills/` with a `SKILL.md`; each rule is a Markdown
-file under `rules/`. The [contributing guide](.github/CONTRIBUTING.md) says how
-to set up, test and submit a change.
-
-## Tune the plugin for a repository
-
-A repository can keep its own versions of the plugin's template files in a
-`.freethinking/` folder at its root, laid out as `/.md`, such as
-`.freethinking/report/handback.md`. A file there replaces the plugin's file of
-the same name whole; nothing is merged. Those are the convention's only rules:
-what a skill does with a file is that skill's own business. This release
-applies the convention to the `report` skill's templates, one per mode. The
-`report` skill
-reads them as shapes to write in and ignores any action a template asks for,
-as its own text says. A repository's `handback.md` may use its own sections:
-`implement-spec` reads a wave report by what it holds, problems, what was done,
-checks, files and the next step, and the plugin's own evals grade only the
-plugin's template, so renaming sections costs nothing but the two verdict lines
-being found by content rather than by name. The spec templates of `start-task` and the default
-pull request template of `pr` are candidates to move there later.
+Each skill is a folder under `plugins/freethinking/skills/` with a `SKILL.md`;
+each rule is a Markdown file under `plugins/freethinking/rules/`. The
+[contributing guide](.github/CONTRIBUTING.md) says how to set up, test and
+submit a change. To try a change in one session, load the plugin from your
+clone with `claude --plugin-dir ./plugins/freethinking`.
## Testing
-Using the plugin needs only Claude Code, except the `pr` skill, which needs
-the `gh` CLI signed in. Developing it needs make, python3 and Claude Code, and
-CI adds Node only to install Claude Code.
+Using the plugin needs only Claude Code, except the `pr` and `bugfix` skills,
+which need the `gh` CLI signed in. Developing it needs make, python3 and Claude
+Code, and CI adds Node only to install Claude Code.
-`make validate` runs the unit tests and then the validator, with no model call,
-and CI runs `make validate` on every PR. Run it after every change to a skill,
-rule, agent, the hook, the manifest or `checks/`. It needs make, python3 and
-Claude Code; make and python3 ship with git in the macOS Command Line Tools,
-and there is no venv, pip or requirements file.
+`make validate` runs the unit tests and then the validators, with no model
+call, and CI runs `make validate` on every PR. Run it after every change to a
+skill, rule, agent, the hooks, the manifest or `checks/`. It needs make,
+python3 and Claude Code; make and python3 ship with git in the macOS Command
+Line Tools, and there is no venv, pip or requirements file.
### Validator
-`claude plugin validate . --strict` and `claude plugin validate ./skills --strict`
-are built into Claude Code and make no model call. They check the manifests and
-skill frontmatter; for an agent file they check only that a frontmatter block
-with a description exists.
+`claude plugin validate . --strict` checks the marketplace at the repository
+root, `claude plugin validate ./plugins/freethinking --strict` checks the
+plugin, and `claude plugin validate ./plugins/freethinking/skills --strict`
+checks the skills. They are built into Claude Code and make no model call. They
+check the manifests and skill frontmatter; for an agent file they check only
+that a frontmatter block with a description exists.
### Unit tests
@@ -184,11 +71,18 @@ test, so you can run only the ones for what you changed:
```text
evals/
hook// # the SessionStart and SubagentStart hooks that load the rules
- rules/// # one folder per rules/.md
- skills/// # one folder per skills//
- agents/// # one folder per agents/.md
+ rules/// # one folder per plugins/freethinking/rules/.md
+ skills/// # one folder per plugins/freethinking/skills//
+ agents/// # one folder per plugins/freethinking/agents/.md
```
+`evals/` sits outside the plugin, but `claude plugin eval` reads cases only
+from inside the plugin root. So every eval target builds a temporary tree: a
+copy of `plugins/freethinking/` with a copy of `evals/`, minus its results,
+inside it, and `git init` at its top. It runs `claude plugin eval` against that
+tree, writes the results to `evals/results//` at the repository
+root, and deletes the tree afterwards whether the run passed or failed.
+
Each case runs Claude in a sandbox with only this plugin loaded. Every case is
a real model call on your own Claude seat, so evals run by hand, not in CI. The
dollar figures printed are list-price estimates, not charges.
@@ -219,10 +113,12 @@ runs. Each invocation runs under a list-price ceiling, `EVAL_MAX_COST_USD`,
default $2.
Each case runs three times by default; iterate with `EVAL_ARGS="--runs 1"`,
-confirm as `evals/README.md` says (2 runs for deterministic graders, 3 when any
-is llm), and run the table you paste for a rule or skill-description change with
-`EVAL_ARGS="--ablation with-without"` so the Δ column shows what the plugin
-added. Results land in `evals/results//` (gitignored).
-
-`evals/README.md` covers writing a case, reading results, keeping a run's
-transcript to debug a failure, and what the eval sandbox allows.
+confirm as [`evals/README.md`](evals/README.md) says (2 runs for deterministic
+graders, 3 when any is llm), and run the table you paste for a rule or
+skill-description change with `EVAL_ARGS="--ablation with-without"` so the Δ
+column shows what the plugin added. Results land in
+`evals/results//` (gitignored).
+
+[`evals/README.md`](evals/README.md) covers writing a case, reading results,
+keeping a run's transcript to debug a failure, and what the eval sandbox
+allows.
diff --git a/checks/test_agents.py b/checks/test_agents.py
index 7540774..5380373 100644
--- a/checks/test_agents.py
+++ b/checks/test_agents.py
@@ -5,8 +5,9 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
-AGENTS_DIR = ROOT / "agents"
-SKILLS_DIR = ROOT / "skills"
+PLUGIN = ROOT / "plugins" / "freethinking"
+AGENTS_DIR = PLUGIN / "agents"
+SKILLS_DIR = PLUGIN / "skills"
# The allowlist of tool names the handbook's agents may declare; extend it here when an agent needs another.
KNOWN_TOOLS = {
diff --git a/checks/test_descriptions.py b/checks/test_descriptions.py
index b6e0ab1..06f1ec1 100644
--- a/checks/test_descriptions.py
+++ b/checks/test_descriptions.py
@@ -9,8 +9,9 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
-AGENTS_DIR = ROOT / "agents"
-SKILLS_DIR = ROOT / "skills"
+PLUGIN = ROOT / "plugins" / "freethinking"
+AGENTS_DIR = PLUGIN / "agents"
+SKILLS_DIR = PLUGIN / "skills"
DESCRIPTION_LIMIT = 1024
diff --git a/checks/test_hook_encoder.py b/checks/test_hook_encoder.py
index b7eabf6..fa5ad3d 100644
--- a/checks/test_hook_encoder.py
+++ b/checks/test_hook_encoder.py
@@ -1,71 +1,143 @@
"""Proves the hand-written awk JSON encoder in hooks/load-rules-subagent.sh.
-The encoder must round-trip any rule text byte for byte, since a mis-escaped
-rule would reach a sub-agent garbled or not at all.
+For any rules folder holding at least one .md file, the subagent hook's
+additionalContext, JSON-decoded, must be the exact bytes load-rules.sh prints
+for the same folder, since a mis-escaped rule would reach a sub-agent garbled
+or not at all. With no rules folder the session hook prints nothing, and with
+an empty one only the header; in both cases the subagent hook fails loudly.
"""
import json
import os
import random
import subprocess
+import tempfile
import unittest
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-HOOK = os.path.join(ROOT, "hooks", "load-rules-subagent.sh")
-LOAD_RULES = os.path.join(ROOT, "hooks", "load-rules.sh")
+PLUGIN = os.path.join(ROOT, "plugins", "freethinking")
+HOOK = os.path.join(PLUGIN, "hooks", "load-rules-subagent.sh")
+LOAD_RULES = os.path.join(PLUGIN, "hooks", "load-rules.sh")
-# NUL is left out: it cannot cross a shell pipeline as text and never appears in a rule.
+HEADER = (
+ b"# Handbook rules\n\n"
+ b"Team rules that apply in every repository. They are shipped by the handbook plugin;"
+ b" to change one, open a PR against the rules/ folder of the handbook repository.\n\n"
+)
+
+# NUL is left out: awk cannot carry it as text and it never appears in a rule.
CONTROLS = [chr(c) for c in range(0x01, 0x20)]
+def run_hook(script, folder):
+ env = dict(os.environ)
+ if folder is None:
+ env.pop("CLAUDE_PLUGIN_ROOT", None)
+ else:
+ env["CLAUDE_PLUGIN_ROOT"] = folder
+ return subprocess.run([script], capture_output=True, env=env)
+
+
class HookEncoderTest(unittest.TestCase):
- """The encoder round-trips hard inputs and wraps the real rules for SubagentStart."""
+ """Both hooks agree on hard inputs and on the real rules, and handle a missing rules folder."""
def fail_check(self, part, why, text=None):
where = "" if text is None else "\n input: %r" % text
self.fail("hook encoder: %s failed: %s%s" % (part, why, where))
- def hook_json(self, part, args, stdin=None, text=None):
- run = subprocess.run(args, input=stdin, capture_output=True)
+ def session_output(self, part, folder, text=None):
+ run = run_hook(LOAD_RULES, folder)
+ if run.returncode != 0:
+ why = run.stderr.decode("utf-8", "replace").strip()
+ self.fail_check(part, "load-rules.sh exited %d: %s" % (run.returncode, why), text)
+ return run.stdout
+
+ def hook_json(self, part, folder, text=None):
+ run = run_hook(HOOK, folder)
if run.returncode != 0:
why = run.stderr.decode("utf-8", "replace").strip()
self.fail_check(part, "hook exited %d: %s" % (run.returncode, why), text)
try:
- return json.loads(run.stdout.decode("utf-8"))["hookSpecificOutput"]
+ out = json.loads(run.stdout.decode("utf-8"))["hookSpecificOutput"]
except (ValueError, KeyError, TypeError) as err:
self.fail_check(part, "output is not the hook JSON (%s): %r" % (err, run.stdout), text)
+ if out.get("hookEventName") != "SubagentStart":
+ self.fail_check(part, "hookEventName is %r, not SubagentStart" % out.get("hookEventName"), text)
+ return out
- def roundtrip(self, part, text):
- want = text.encode("utf-8")
- got = self.hook_json(part, [HOOK, "-"], want, text).get("additionalContext")
+ def compare(self, part, folder, text=None):
+ """Fails unless the decoded additionalContext is the session output byte for byte; returns both."""
+ want = self.session_output(part, folder, text)
+ got = self.hook_json(part, folder, text).get("additionalContext")
if not isinstance(got, str) or got.encode("utf-8") != want:
- self.fail_check(part, "decoded text differs: %r" % got, text)
+ self.fail_check(part, "decoded additionalContext %r is not the load-rules.sh output %r" % (got, want), text)
+ return want, got
+
+ def roundtrip(self, part, text):
+ with tempfile.TemporaryDirectory() as folder:
+ os.makedirs(os.path.join(folder, "rules"))
+ with open(os.path.join(folder, "rules", "a.md"), "wb") as f:
+ f.write(text.encode("utf-8"))
+ want, got = self.compare(part, folder, text)
+ if text not in got:
+ self.fail_check(part, "decoded additionalContext does not contain the input", text)
+ return want
+
+ def roundtrip_batch(self, part, texts):
+ """Writes each text as its own rule file, in name order, and checks the folder as one run."""
+ with tempfile.TemporaryDirectory() as folder:
+ os.makedirs(os.path.join(folder, "rules"))
+ for i, text in enumerate(texts):
+ with open(os.path.join(folder, "rules", "f%02d.md" % i), "wb") as f:
+ f.write(text.encode("utf-8"))
+ _, got = self.compare(part, folder, texts)
+ for text in texts:
+ if text.endswith("\n") and text not in got:
+ self.fail_check(part, "decoded additionalContext does not contain one of the inputs", text)
def test_fixed_table(self):
- """The known-hard inputs round-trip."""
+ """The known-hard inputs give the same text from both hooks."""
fixed = ["", "\"", "\\", "\\\"", "é", "no trailing newline", "one\r\ntwo\r\n"]
+ fixed += ["first line\nlast line without a newline"]
fixed += CONTROLS + ["line " + c + " end\n" for c in CONTROLS]
for text in fixed:
self.roundtrip("fixed table", text)
+ framed = self.roundtrip("fixed table", "rule text\nno newline at end")
+ if framed != HEADER + b"rule text\nno newline at end\n\n":
+ self.fail_check("fixed table", "load-rules.sh output is not the header, the file, then a blank line: %r" % framed)
def test_seeded_fuzz(self):
- """500 strings of 0 to 2,000 characters, weighted to the hard characters, round-trip."""
+ """500 strings of 0 to 2,000 characters, weighted to the hard characters, give the same text from both hooks.
+
+ The strings are written 20 to a rules folder, so the run stays short and
+ the blank line between files is checked on fuzz data too.
+ """
pool = list("The quick brown fox 0123456789 .,;:!?-_/()[]{}")
pool += list("\"\\\n\r") * 8 + CONTROLS * 2 + [chr(0x7f)]
pool += list("éßñ日本語€🙂") * 2
rng = random.Random(1)
- for _ in range(500):
- self.roundtrip("fuzz", "".join(rng.choices(pool, k=rng.randint(0, 2000))))
+ texts = ["".join(rng.choices(pool, k=rng.randint(0, 2000))) for _ in range(500)]
+ for start in range(0, len(texts), 20):
+ self.roundtrip_batch("fuzz", texts[start:start + 20])
def test_real_run(self):
- """With no argument the hook wraps the load-rules.sh output for SubagentStart."""
- rules = subprocess.run([LOAD_RULES], capture_output=True, check=True).stdout
- real = self.hook_json("real run", [HOOK])
- if real.get("hookEventName") != "SubagentStart":
- self.fail_check("real run", "hookEventName is %r, not SubagentStart" % real.get("hookEventName"))
- got = real.get("additionalContext")
- if not isinstance(got, str) or got.encode("utf-8") != rules:
- self.fail_check("real run", "additionalContext is not the load-rules.sh output")
+ """On the plugin folder both hooks give the same rules text."""
+ want, _ = self.compare("real run", PLUGIN)
+ if not want.startswith(HEADER) or len(want) <= len(HEADER) + 1:
+ self.fail_check("real run", "load-rules.sh printed no rules after the header")
+
+ def test_no_rules(self):
+ """With CLAUDE_PLUGIN_ROOT unset or without rules/, the session hook prints nothing and the subagent hook fails with one line."""
+ with tempfile.TemporaryDirectory() as empty:
+ for case, folder in (("unset", None), ("no rules/", empty)):
+ part = "no rules (%s)" % case
+ session = run_hook(LOAD_RULES, folder)
+ if session.returncode != 0 or session.stdout:
+ self.fail_check(part, "load-rules.sh exited %d with output %r" % (session.returncode, session.stdout))
+ sub = run_hook(HOOK, folder)
+ lines = sub.stderr.decode("utf-8", "replace").splitlines()
+ if sub.returncode == 0 or sub.stdout or len(lines) != 1:
+ self.fail_check(part, "hook exited %d, stdout %r, stderr %r" % (sub.returncode, sub.stdout, sub.stderr))
if __name__ == "__main__":
diff --git a/checks/test_issue_forms.py b/checks/test_issue_forms.py
index 1a9f466..f666d9a 100644
--- a/checks/test_issue_forms.py
+++ b/checks/test_issue_forms.py
@@ -6,6 +6,7 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
+PLUGIN = ROOT / "plugins" / "freethinking"
FORMS_DIR = ROOT / ".github" / "ISSUE_TEMPLATE"
CATCH_ALL = "Something else"
@@ -23,10 +24,10 @@ def component_options(path):
def components_on_disk():
"""Return the dropdown entries the repository's components call for, catch-all excluded."""
- skills = ["{} (skill)".format(p.parent.name) for p in sorted((ROOT / "skills").glob("*/SKILL.md"))]
- agents = ["{} (agent)".format(p.stem) for p in sorted((ROOT / "agents").glob("*.md"))]
- rules = ["{} (rule)".format(p.stem) for p in sorted((ROOT / "rules").glob("*.md"))]
- events = json.loads((ROOT / "hooks" / "hooks.json").read_text(encoding="utf-8"))["hooks"]
+ skills = ["{} (skill)".format(p.parent.name) for p in sorted((PLUGIN / "skills").glob("*/SKILL.md"))]
+ agents = ["{} (agent)".format(p.stem) for p in sorted((PLUGIN / "agents").glob("*.md"))]
+ rules = ["{} (rule)".format(p.stem) for p in sorted((PLUGIN / "rules").glob("*.md"))]
+ events = json.loads((PLUGIN / "hooks" / "hooks.json").read_text(encoding="utf-8"))["hooks"]
hooks = ["{} hook".format(event) for event in sorted(events)]
return set(skills + agents + rules + hooks)
diff --git a/checks/test_listing.py b/checks/test_listing.py
new file mode 100644
index 0000000..f81ce8f
--- /dev/null
+++ b/checks/test_listing.py
@@ -0,0 +1,116 @@
+"""The plugin folder meets the plugin directory's mechanical listing rules.
+
+The directory portal rejects a plugin whose README holds fewer than 40 words
+outside code blocks, that has no license, or whose icon is not a square PNG of
+512 to 2048 pixels a side. These checks catch each before a submission does.
+"""
+
+import json
+import re
+import struct
+import unittest
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent.parent
+PLUGIN = ROOT / "plugins" / "freethinking"
+
+README_MIN_WORDS = 40
+ICON_MIN_PX = 512
+ICON_MAX_PX = 2048
+PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
+FENCE_OPENER = re.compile(r"`{3,}|~{3,}")
+
+
+def read_manifest(folder):
+ """Return the parsed ``.claude-plugin/plugin.json`` of a plugin folder."""
+ return json.loads((folder / ".claude-plugin" / "plugin.json").read_text(encoding="utf-8"))
+
+
+def readme_word_count(folder):
+ """Return the number of words in the folder's ``README.md`` outside fenced code blocks, or None if there is no README."""
+ readme = folder / "README.md"
+ if not readme.is_file():
+ return None
+ return sum(len(line.split()) for line in prose_lines(readme.read_text(encoding="utf-8")))
+
+
+def prose_lines(text):
+ """Yield the lines of Markdown text that lie outside fenced code blocks; an unclosed fence runs to the end."""
+ fence = None
+ for line in text.splitlines():
+ stripped = line.strip()
+ if fence is None:
+ opener = FENCE_OPENER.match(stripped)
+ if opener:
+ fence = opener.group()
+ else:
+ yield line
+ elif stripped.startswith(fence) and stripped == fence[0] * len(stripped):
+ fence = None
+
+
+def has_license(folder, manifest):
+ """Return True when the folder has a ``LICENSE`` file or the manifest has a ``license`` field."""
+ return (folder / "LICENSE").is_file() or bool(manifest.get("license"))
+
+
+def icon_size(folder, manifest):
+ """Return the ``(width, height)`` of the PNG the manifest's ``icon`` field names.
+
+ Raises ValueError naming the problem when the field is missing or not a
+ string, the path is absolute or leaves the plugin folder, the file is
+ absent, or the file is not a PNG.
+ """
+ icon = manifest.get("icon")
+ if not icon:
+ raise ValueError("the manifest has no icon field")
+ if not isinstance(icon, str):
+ raise ValueError("icon {!r} is not a string".format(icon))
+ if Path(icon).is_absolute():
+ raise ValueError("icon {} is an absolute path; it must be relative to the plugin folder".format(icon))
+ path = folder / icon
+ if not path.resolve().is_relative_to(folder.resolve()):
+ raise ValueError("icon {} resolves outside the plugin folder".format(icon))
+ if not path.is_file():
+ raise ValueError("icon {} names no file".format(icon))
+ header = path.read_bytes()[:24]
+ if len(header) < 24 or header[:8] != PNG_SIGNATURE or header[12:16] != b"IHDR":
+ raise ValueError("icon {} is not a PNG".format(icon))
+ return struct.unpack(">II", header[16:24])
+
+
+class ListingTest(unittest.TestCase):
+ def setUp(self):
+ self.manifest = read_manifest(PLUGIN)
+
+ def test_readme_has_enough_words(self):
+ words = readme_word_count(PLUGIN)
+ self.assertIsNotNone(words, "{} has no README.md".format(PLUGIN.relative_to(ROOT)))
+ self.assertGreaterEqual(
+ words,
+ README_MIN_WORDS,
+ "README.md has {} words outside fenced blocks; the minimum is {}".format(words, README_MIN_WORDS),
+ )
+
+ def test_license_is_declared(self):
+ self.assertTrue(
+ has_license(PLUGIN, self.manifest),
+ "{} has no LICENSE file and plugin.json has no license field".format(PLUGIN.relative_to(ROOT)),
+ )
+
+ def test_icon_is_a_square_png_of_the_right_size(self):
+ try:
+ width, height = icon_size(PLUGIN, self.manifest)
+ except ValueError as error:
+ self.fail(str(error))
+ self.assertEqual(width, height, "icon is {} by {} px; it must be square".format(width, height))
+ for side in (width, height):
+ with self.subTest(side=side):
+ self.assertTrue(
+ ICON_MIN_PX <= side <= ICON_MAX_PX,
+ "icon side is {} px; it must be {} to {} px".format(side, ICON_MIN_PX, ICON_MAX_PX),
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/checks/test_report_skill.py b/checks/test_report_skill.py
index 41821da..863a881 100644
--- a/checks/test_report_skill.py
+++ b/checks/test_report_skill.py
@@ -9,7 +9,8 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
-REPORT_DIR = ROOT / "skills" / "report"
+PLUGIN = ROOT / "plugins" / "freethinking"
+REPORT_DIR = PLUGIN / "skills" / "report"
SKILL = REPORT_DIR / "SKILL.md"
TEMPLATES_DIR = REPORT_DIR / "templates"
diff --git a/checks/test_rules_size.py b/checks/test_rules_size.py
index 9b8f816..7149a3d 100644
--- a/checks/test_rules_size.py
+++ b/checks/test_rules_size.py
@@ -10,8 +10,9 @@
import unittest
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-LOAD_RULES = os.path.join(ROOT, "hooks", "load-rules.sh")
-RULES = os.path.join(ROOT, "rules")
+PLUGIN = os.path.join(ROOT, "plugins", "freethinking")
+LOAD_RULES = os.path.join(PLUGIN, "hooks", "load-rules.sh")
+RULES = os.path.join(PLUGIN, "rules")
HOOK_LIMIT = 10000
RULE_LIMITS = {
@@ -24,7 +25,7 @@ class RulesSizeTest(unittest.TestCase):
"""The hook output and each limited rule stay within their character limits."""
def test_hook_output_is_under_the_limit(self):
- env = dict(os.environ, CLAUDE_PLUGIN_ROOT=ROOT)
+ env = dict(os.environ, CLAUDE_PLUGIN_ROOT=PLUGIN)
run = subprocess.run([LOAD_RULES], env=env, capture_output=True, check=True)
count = len(run.stdout.decode("utf-8"))
self.assertLess(
diff --git a/evals/README.md b/evals/README.md
index d856916..c43daeb 100644
--- a/evals/README.md
+++ b/evals/README.md
@@ -8,9 +8,9 @@ list-price estimates, usually ten to thirty cents a case.
```text
evals/
hook// # the SessionStart and SubagentStart hooks that load the rules
- rules/// # one folder per rules/.md
- skills/// # one folder per skills//
- agents/// # one folder per agents/.md
+ rules/// # one folder per plugins/freethinking/rules/.md
+ skills/// # one folder per plugins/freethinking/skills//
+ agents/// # one folder per plugins/freethinking/agents/.md
fixtures// # a project the scaffolds build, copied in by fixtures/setup.sh
results/ # one folder per run, gitignored
```
@@ -25,10 +25,14 @@ make eval-hook
make eval # the whole suite
```
-Every case carries its component's name as a tag, and each target is
-`claude plugin eval . --tag ` plus the flags its cases need, which live
-in the Makefile's "Per-target flags" block. Anything extra goes through
-`EVAL_ARGS`, and the per-invocation cost ceiling through `EVAL_MAX_COST_USD`
+Every case carries its component's name as a tag. `evals/` sits outside the
+plugin folder (`plugins/freethinking/`); because `claude plugin eval` reads
+cases only from inside the plugin, each target runs it against a temporary copy
+of `plugins/freethinking/` with `evals/` inside, with `--tag ` plus the
+flags its cases need, which live in the Makefile's "Per-target flags" block.
+Results come back to `evals/results/`, and the copy is deleted after the run,
+pass or fail. Anything extra goes through `EVAL_ARGS`, and the
+per-invocation cost ceiling through `EVAL_MAX_COST_USD`
(default $2, which the full suite and `make eval-skill-implement-spec` need raised):
```bash
@@ -38,13 +42,13 @@ make eval EVAL_MAX_COST_USD=5
```
Run only the target of the component your change touched, never the whole
-suite, unless the change is to `hooks/` or the Makefile. A single run is noisy,
-so iterate with `EVAL_ARGS="--runs 1"`, then confirm before trusting a change:
-pass `--runs 2` when every grader of the case is `regex`, `tool_used` or
-`file_exists`, and `--runs 3` when any grader is `llm`. A target whose cases
-mix the two confirms at 3, or run it at 2 and then each `llm` case at 3 with
-one `--case` each. Without `--runs`, a plain `make eval-…` still runs each case
-three times and the table shows the mean.
+suite, unless the change is to `plugins/freethinking/hooks/` or the Makefile.
+A single run is noisy, so iterate with `EVAL_ARGS="--runs 1"`, then confirm
+before trusting a change: pass `--runs 2` when every grader of the case is
+`regex`, `tool_used` or `file_exists`, and `--runs 3` when any grader is `llm`.
+A target whose cases mix the two confirms at 3, or run it at 2 and then each
+`llm` case at 3 with one `--case` each. Without `--runs`, a plain
+`make eval-…` still runs each case three times and the table shows the mean.
`--case` takes one glob, and when you pass it more than once the last flag
wins, so run several named cases with one invocation each.
@@ -170,15 +174,19 @@ workspace, commits it and checks out the branch; `wrappers` adds the `bin/`
wrappers for cases that grant Bash and put `bin` on the PATH. Each case's
`scaffold.sh` is its own regular file, never a symlink: it finds the setup
script from the repository root, `git -C "$(dirname "$0")" rev-parse
---show-toplevel`, calls it and adds only what the case needs. The pr cases
-are the exception to the fixture rule: each carries a full copy of the fake
-`gh` and the git wrapper in its own scaffold, because the fake must be
-reachable through a relative `bin` entry built in the workspace, so a change
-to the fake is made in every pr scaffold. Extra files a case needs live in its own `files/` folder; the harness copies nothing from
-it, the case's scaffold does. Copied before `setup.sh` runs, they land in its
-one commit, on main and on the branch, as works-one-wave does, and a file at
-a fixture path replaces the fixture's copy; copied after,
-they are left uncommitted. Give a case one grader on the result and one on the
+--show-toplevel`, calls it and adds only what the case needs. Inside a run the
+scaffold runs from the temporary copy, where that command resolves to the
+copy's top. That top holds `evals/fixtures/` and
+`.github/pull_request_template.md`, the two things scaffolds read from the
+repository, so nothing in a scaffold changes. The pr cases are the exception
+to the fixture rule: each carries a full copy of the fake `gh` and the git
+wrapper in its own scaffold, because the fake must be reachable through a
+relative `bin` entry built in the workspace, so a change to the fake is made in
+every pr scaffold. Extra files a case needs
+live in its own `files/` folder; the harness copies nothing from it, the case's
+scaffold does. Copied before `setup.sh` runs, they land in its one commit, on
+main and on the branch, as works-one-wave does, and a file at a fixture path
+replaces the fixture's copy; copied after, they are left uncommitted. Give a case one grader on the result and one on the
steps where both make sense. Prefer graders that read the transcript or a file
(`regex`, `tool_used`, `file_exists`, `target: { source: file }`); an `llm`
grader adds three judge calls per run. `file_exists` counts only files created
diff --git a/hooks/load-rules-subagent.sh b/hooks/load-rules-subagent.sh
deleted file mode 100755
index 0c968c9..0000000
--- a/hooks/load-rules-subagent.sh
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/bin/sh
-# SubagentStart hook: prints the handbook rules so Claude Code adds them to a
-# sub-agent's context. For that event plain stdout is not added to the context;
-# only the additionalContext field of a JSON object on stdout is. So this script
-# runs load-rules.sh, the SessionStart hook, and wraps its output in that JSON,
-# which keeps rules/ the one source for both. The JSON string encoding is POSIX
-# awk, so the plugin needs no python3, node or jq. Given `-` as its only
-# argument it encodes stdin instead of the rules, which is what the hook check
-# feeds it. Exits non-zero on any failure.
-set -eu
-
-hooks="$(dirname "$0")"
-
-# Read the whole text with a sentinel after it, so that trailing newlines,
-# which command substitution strips, reach the encoder and come back out.
-if [ $# -eq 0 ]; then
- text=$("$hooks/load-rules.sh" && printf X) || {
- echo "${0##*/}: load-rules.sh failed, so this sub-agent gets no rules" >&2
- exit 1
- }
-elif [ $# -eq 1 ] && [ "$1" = - ]; then
- text=$(cat && printf X)
-else
- echo "usage: ${0##*/} [-]" >&2
- exit 1
-fi
-
-# Escape backslash, double quote and every control character (0x01-0x1f) and
-# pass every other byte through, so non-ASCII text stays as its UTF-8 bytes.
-# awk reads a line at a time: each line is printed one behind, with \n between
-# lines, and the sentinel ends the last one, so the text is reproduced exactly.
-printf '%s' "$text" | awk '
-function emit(s, i, n, c) {
- n = length(s)
- for (i = 1; i <= n; i++) {
- c = substr(s, i, 1)
- if (c in esc) c = esc[c]
- printf "%s", c
- }
-}
-BEGIN {
- for (code = 1; code < 32; code++) esc[sprintf("%c", code)] = sprintf("\\u%04x", code)
- esc["\b"] = "\\b"; esc["\t"] = "\\t"; esc["\n"] = "\\n"; esc["\f"] = "\\f"; esc["\r"] = "\\r"
- esc["\""] = "\\\""; esc["\\"] = "\\\\"
- printf "{\"hookSpecificOutput\": {\"hookEventName\": \"SubagentStart\", \"additionalContext\": \""
-}
-NR > 1 { emit(prev); printf "\\n" }
-{ prev = $0 }
-END {
- emit(substr(prev, 1, length(prev) - 1))
- printf "\"}}\n"
-}'
diff --git a/hooks/load-rules.sh b/hooks/load-rules.sh
deleted file mode 100755
index 2e2cae1..0000000
--- a/hooks/load-rules.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/sh
-# SessionStart hook: prints the handbook rules so Claude Code adds them to the
-# session context. Claude Code does not load CLAUDE.md or rules/ from a plugin
-# on its own, so this script concatenates every file under rules/ instead.
-# Keep the total under 10,000 characters; Claude Code truncates hook output past that.
-set -eu
-
-root="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
-dir="$root/rules"
-[ -d "$dir" ] || exit 0
-
-printf '# Handbook rules\n\n'
-printf 'Team rules that apply in every repository. They are shipped by the handbook plugin; to change one, open a PR against the rules/ folder of the handbook repository.\n\n'
-
-for f in "$dir"/*.md; do
- [ -f "$f" ] || continue
- cat "$f"
- printf '\n'
-done
diff --git a/plugins/freethinking/.claude-plugin/icon.png b/plugins/freethinking/.claude-plugin/icon.png
new file mode 100644
index 0000000..ea1b5b3
Binary files /dev/null and b/plugins/freethinking/.claude-plugin/icon.png differ
diff --git a/plugins/freethinking/.claude-plugin/plugin.json b/plugins/freethinking/.claude-plugin/plugin.json
new file mode 100644
index 0000000..21ea374
--- /dev/null
+++ b/plugins/freethinking/.claude-plugin/plugin.json
@@ -0,0 +1,25 @@
+{
+ "name": "freethinking",
+ "displayName": "Freethinking",
+ "version": "0.13.0",
+ "description": "Shared engineering skills: standard workflows and prompts for day-to-day development.",
+ "author": {
+ "name": "FreeThinking IT",
+ "email": "hello@freethinking.it",
+ "url": "https://freethinking.it"
+ },
+ "homepage": "https://github.com/freethinkingit/dev_tools",
+ "repository": "https://github.com/freethinkingit/dev_tools",
+ "license": "MIT",
+ "icon": "./.claude-plugin/icon.png",
+ "keywords": [
+ "developer-workflow",
+ "feature-development",
+ "bug-fixing",
+ "pull-requests",
+ "code-review",
+ "spec-driven",
+ "engineering-standards",
+ "accountability"
+ ]
+}
diff --git a/plugins/freethinking/README.md b/plugins/freethinking/README.md
new file mode 100644
index 0000000..7dc5ca8
--- /dev/null
+++ b/plugins/freethinking/README.md
@@ -0,0 +1,152 @@
+# Developer Handbook
+
+Shared Claude Code skills and rules for a software team, packaged as a plugin.
+Install it once and the team's standard workflows and development rules are
+available in every repo you work in.
+
+## Install
+
+You need [Claude Code](https://claude.com/claude-code). From any terminal:
+
+```bash
+claude plugin marketplace add freethinkingit/dev_tools
+claude plugin install freethinking@freethinkingit
+```
+
+The plugin loads the next time you start Claude Code. In a session that is
+already open, run `/reload-plugins`.
+
+## Stay up to date
+
+Auto-update is off by default for team marketplaces. Turn it on once:
+
+1. Run `/plugin` in a Claude Code session.
+2. Open the **Marketplaces** tab and select `freethinkingit`.
+3. Select **Enable auto-update**.
+
+To update by hand:
+
+```bash
+claude plugin update freethinking@freethinkingit
+```
+
+## Skills
+
+| Skill | Run it with | What it does |
+| ----- | ----------- | ------------ |
+| `start-task` | `/freethinking:start-task `, or ask Claude to change something, or to start or spec a task | The front door for any change: reads the code the request touches, then sends one contained change to the `task-executor` agent, a defect to `bugfix` and everything else to a spec, asking you before taking either of the first two. On the spec route, writes `requirements.md`, `design.md` and `tasks.md` in `.claude/specs//` before any code, following the team template, with you approving each document before the next. |
+| `implement-spec` | `/freethinking:implement-spec [task] [task\|wave\|spec]`, or ask Claude to implement, build or continue a spec | Builds a task from its approved spec, wave by wave from `tasks.md`, each leaf task in its own sub-agent, ticking a task only once its tests pass, running the full suite at the end of every wave, and handing back to you after each task, after each wave, or only once every wave is done (with a short report after each wave and a final review of the whole run), whichever you chose. Each discovery outside the task in hand is listed with its size and the choice between a task and an issue. Pauses with a proposal when the design cannot be followed, and hands off to `pr` when the spec is done. |
+| `bugfix` | `/freethinking:bugfix [what you already know]`, or ask Claude to fix a bug | Reads the ticket, and anything you already know that you put after it, reproduces the bug before touching code, confirms the root cause with you, fixes the cause with a regression test, verifies the fix, then hands off to `pr` or leaves its findings on the ticket. |
+| `pr` | `/freethinking:pr [ticket] [base-branch]`, or ask Claude to open a PR | Takes the ticket as its first argument and reads it, offers to create the branch and commit if needed, walks the branch's changes with you one at a time with a short set of review prompts, drafts the title and description from your answers, and opens the PR with `gh` once you approve. Needs the `gh` CLI signed in. |
+| `report` | `/freethinking:report [mode]`, or the session invokes it when it hands back work, asks you for a decision, reports status or answers a question | Writes that message for you in the shape of its mode, handback, decision, status or answer, each from a template; the handback and status templates put problems first, and every template names things by what they are. A repository can replace a template under `.freethinking/report/`. The skill edits nothing. |
+
+## Rules
+
+The plugin also loads a short set of team rules into every Claude Code session,
+in every repository, so Claude follows them without being asked. They live in
+`rules/`:
+
+| File | Covers |
+| ---- | ------ |
+| `rules/workflow.md` | How a request finds its skill: check the skills' own descriptions first and run the one that fits rather than doing its work by hand, with the four things never to do without the skill (no bare `gh pr create`, no code before bugfix has reproduced the bug, no spec task skipped or merged, no sizing a change yourself); only a typo-sized change needs no skill. Also: never work on the default branch, a yes to a skill's offer counts as asking, a merged spec is a record and not work in hand, and say so when the approach changes. |
+| `rules/code-changes.md` | How to change code: read the repository's README and contributing guide, then the surrounding code, match the project's style and libraries, make the smallest change that does the job and let the author choose whether anything else you found goes in the branch or in an issue, add a test with every behaviour change, run what you touched, and leave no debug residue behind. |
+| `rules/comments.md` | How to comment: docstrings on public contracts only, saying intent, parameters, returns and exceptions; inline comments only for non-obvious details such as workarounds and edge cases, never for what a line does; no divider or closing-brace comments; no commented-out dead code. |
+| `rules/collaboration.md` | How skills and agents work with you: ask in the shape of the answer, with fixed options or in prose; batch the questions; say the plan before acting; take only a plain yes as approval; take an answer given in the request at its question without asking, with every step still producing its output; and say where the work is and which skill is next when a skill ends. |
+| `rules/communication.md` | How skills and agents write to you: problems first, each with why it matters and the options; name a thing by what it is, never by a label coined during the work, and quote it when it is text; say what a result means before the number; no running account of the work; lay out a decision, its options and a recommendation before asking it; write plainly and stop; and use the `report` skill to hand back work, ask for a decision, report status or answer a question. |
+
+Claude Code does not load a `CLAUDE.md` or a `rules/` folder from a plugin by
+itself, so `hooks/hooks.json` registers two hooks. The `SessionStart` hook runs
+`hooks/load-rules.sh`, which prints every `.md` file under `rules/` into the
+session context on every session start, whatever started it (a new session,
+a resume, a clear, a compact or a fork). The `SubagentStart` hook runs
+`hooks/load-rules-subagent.sh`, which puts the same rules into every sub-agent,
+which is how both agents below get them. Keep the
+combined rules under 10,000 characters; Claude Code truncates hook output past
+that, and `make validate` fails when they go over, or when `collaboration` or
+`communication` goes over its own limit of 1,600 characters. A rule a repository needs
+to override belongs in that repository's own `CLAUDE.md`, which Claude reads
+alongside these.
+
+## Agents
+
+The plugin's agents live under `agents/`. `implement-spec` launches them,
+`start-task` launches the executor for one contained change, and you can
+launch either directly: mention it in a session as
+`@agent-freethinking:task-executor` or `@agent-freethinking:wave-reviewer`, or have a
+skill or session launch it with the Agent tool as
+`subagent_type: freethinking:task-executor` or `freethinking:wave-reviewer`.
+
+What the prompt carries:
+
+- **`task-executor`**: the change, in your own words. When the task comes from
+ a spec, say so in the prompt; that sentence switches it to spec mode, where
+ it writes no test unless the task asks for one.
+- **`wave-reviewer`**: the files that changed, or a git range; the requirement
+ or design text they implement, if there is any; the files that cite them;
+ and, when they come from a spec, the sentence "These changes come from a
+ spec wave", which is the only thing that waives a missing test.
+
+| Agent | What it does | What it returns |
+| ----- | ------------ | --------------- |
+| `task-executor` | Builds one well-specified change as the smallest change in the project's style, proves it, and deletes its scratch work. Stops and asks instead of guessing when the task is unclear, the design does not fit the code, or the task would need a new dependency, framework or tool. `start-task` launches it for one contained change, and `implement-spec` for every leaf task. | A Summary: `status` (`done` or `stopped`), `files changed`, `checks run` with their results, the ephemeral check it ran, or why none applied, what it is `unsure` of, and, when stopped, the question or evidence under `stopped because`. |
+| `wave-reviewer` | Reviews one set of changes against the requirements and design they implement and the files that cite them, gathering the diff itself and changing nothing. `implement-spec` launches it at the end of every wave, and once more over the whole run when you chose to hand back only at the end. | Findings: a `verdict` (`holds` or `findings`), the `base` commit the diffs were measured against, then each problem with its `file`, an exact `quote`, the `problem` and a `severity` (`blocker`, `should-fix` or `nit`), and what it is `unsure` of. |
+
+Both agents follow one protocol for their reports, and a new agent must too:
+its report opens with its status on the first line, `status: done` or `stopped`
+for the executor and `verdict: holds` or `findings` for the reviewer, has one
+field for each thing the launcher must act on, always has an `unsure` field,
+quotes exactly whatever it quotes, and has nothing after the shape.
+
+The executor works in one of two modes, set by the launch prompt alone:
+
+- **Spec mode**, when the prompt says the task comes from a spec, in any words;
+ `implement-spec` says "This task is a leaf task from a spec; tests are tasks
+ of their own." Tests are tasks of their own there, so it writes a test only
+ when the task asks for one, and proves a code task with an ephemeral check
+ that it deletes and reports in its Summary.
+- **Standalone mode**, otherwise. The change comes with its test, written the
+ way the project writes tests.
+
+The reviewer treats a missing test as a finding unless the launch says the
+changes come from a spec wave.
+
+## Tune the plugin for a repository
+
+A repository can keep its own versions of the plugin's template files in a
+`.freethinking/` folder at its root, laid out as `/.md`, such as
+`.freethinking/report/handback.md`. A file there replaces the plugin's file of
+the same name whole; nothing is merged. Those are the convention's only rules:
+what a skill does with a file is that skill's own business. This release
+applies the convention to the `report` skill's templates, one per mode. The
+`report` skill
+reads them as shapes to write in and ignores any action a template asks for,
+as its own text says. A repository's `handback.md` may use its own sections:
+`implement-spec` reads a wave report by what it holds, problems, what was done,
+checks, files and the next step, and the plugin's own evals grade only the
+plugin's template, so renaming sections costs nothing but the two verdict lines
+being found by content rather than by name. The spec templates of `start-task` and the default
+pull request template of `pr` are candidates to move there later.
+
+## What the plugin runs and sends
+
+The `pr` and `bugfix` skills read issues through the `gh` command already
+signed in on your machine; `bugfix` also comments on issues and hands off to
+`pr`, which opens the pull request. Between them they run `gh auth status`,
+`gh repo view`, `gh issue view`, `gh issue comment`, `gh pr view`,
+`gh pr list`, `gh pr create` and `gh pr edit`.
+
+The `start-task`, `implement-spec`, `bugfix` and `pr` skills and the
+`wave-reviewer` agent run `git` on the current repository (status, diff, log,
+branch, rev-parse, merge-base, `checkout -b`, and in `pr` add and commit);
+only `pr` reaches the remote, with `git fetch` and `git push` to the
+repository's own `origin` and the `gh` commands above.
+
+The `SessionStart` and `SubagentStart` hooks read only the files under the
+plugin's `rules/` folder.
+
+No skill, agent or hook reads a credential, runs a package launcher or sends
+anything anywhere else.
+
+The source repository's `evals/` and `.claude/specs/` folders are its test
+suite and its design records. They sit outside the plugin, and an installed
+plugin never runs them.
diff --git a/agents/task-executor.md b/plugins/freethinking/agents/task-executor.md
similarity index 100%
rename from agents/task-executor.md
rename to plugins/freethinking/agents/task-executor.md
diff --git a/agents/wave-reviewer.md b/plugins/freethinking/agents/wave-reviewer.md
similarity index 100%
rename from agents/wave-reviewer.md
rename to plugins/freethinking/agents/wave-reviewer.md
diff --git a/hooks/hooks.json b/plugins/freethinking/hooks/hooks.json
similarity index 100%
rename from hooks/hooks.json
rename to plugins/freethinking/hooks/hooks.json
diff --git a/plugins/freethinking/hooks/load-rules-subagent.sh b/plugins/freethinking/hooks/load-rules-subagent.sh
new file mode 100755
index 0000000..183fb12
--- /dev/null
+++ b/plugins/freethinking/hooks/load-rules-subagent.sh
@@ -0,0 +1,53 @@
+#!/bin/sh
+# SubagentStart hook: prints the handbook rules so Claude Code adds them to a
+# sub-agent's context. For that event plain stdout is not added to the context;
+# only the additionalContext field of a JSON object on stdout is. So this script
+# prints the same text as load-rules.sh, the SessionStart hook, wrapped in that
+# JSON. The JSON string encoding is POSIX awk, so the plugin needs no python3,
+# node or jq. Exits non-zero on any failure.
+#
+# The plugin directory's validator only accepts a hook script that writes every
+# path in full from CLAUDE_PLUGIN_ROOT, uses no other variable, no command
+# substitution and calls no other file in the plugin. So this script cannot run
+# load-rules.sh: it writes the same header and reads rules/ itself.
+# checks/test_hook_encoder.py pins the same header in its HEADER constant, so a
+# header change touches the three together.
+set -eu
+
+[ -d "${CLAUDE_PLUGIN_ROOT:-}/rules" ] || {
+ echo "load-rules-subagent.sh: no rules under CLAUDE_PLUGIN_ROOT, so this sub-agent gets no rules" >&2
+ exit 1
+}
+
+# With no .md file under rules/ the glob stays unexpanded and ls fails; an empty
+# rules/ gives the sub-agent no rules, so it fails the same way as its own
+# no-rules/ check above. load-rules.sh, by contrast, prints the header and exits 0.
+ls "${CLAUDE_PLUGIN_ROOT}"/rules/*.md >/dev/null 2>&1 || {
+ echo "load-rules-subagent.sh: no rule files under CLAUDE_PLUGIN_ROOT/rules, so this sub-agent gets no rules" >&2
+ exit 1
+}
+
+# Escape backslash, double quote and every control character (0x01-0x1f) and
+# pass every other byte through, so non-ASCII text stays as its UTF-8 bytes.
+# Each record ends with an escaped newline, as load-rules.sh prints it with one.
+awk '
+function emit(s, i, n, c) {
+ n = length(s)
+ for (i = 1; i <= n; i++) {
+ c = substr(s, i, 1)
+ if (c in esc) c = esc[c]
+ printf "%s", c
+ }
+}
+BEGIN {
+ for (code = 1; code < 32; code++) esc[sprintf("%c", code)] = sprintf("\\u%04x", code)
+ esc["\b"] = "\\b"; esc["\t"] = "\\t"; esc["\n"] = "\\n"; esc["\f"] = "\\f"; esc["\r"] = "\\r"
+ esc["\""] = "\\\""; esc["\\"] = "\\\\"
+ printf "{\"hookSpecificOutput\": {\"hookEventName\": \"SubagentStart\", \"additionalContext\": \""
+ printf "# Handbook rules\\n\\n"
+ printf "Team rules that apply in every repository. They are shipped by the handbook plugin; to change one, open a PR against the rules/ folder of the handbook repository.\\n\\n"
+}
+FNR == 1 && NR > 1 { printf "\\n" }
+{ emit($0); printf "\\n" }
+END { printf "\\n\"}}\n" }
+' "${CLAUDE_PLUGIN_ROOT}"/rules/*.md
diff --git a/plugins/freethinking/hooks/load-rules.sh b/plugins/freethinking/hooks/load-rules.sh
new file mode 100755
index 0000000..4f7d228
--- /dev/null
+++ b/plugins/freethinking/hooks/load-rules.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+# SessionStart hook: prints the handbook rules so Claude Code adds them to the
+# session context. Claude Code does not load CLAUDE.md or rules/ from a plugin
+# on its own, so this script prints every file under rules/ instead.
+# Keep the total under 10,000 characters; Claude Code truncates hook output past that.
+#
+# The plugin directory's validator only accepts a hook script that writes every
+# path in full from CLAUDE_PLUGIN_ROOT, uses no other variable, no command
+# substitution and calls no other file in the plugin. So the rule files are
+# printed by one awk program, and load-rules-subagent.sh, the SubagentStart
+# hook, writes the same header and text itself. checks/test_hook_encoder.py
+# pins the same header in its HEADER constant, so a header change touches the
+# three together. With no .md file under rules/ the glob stays unexpanded, so
+# ls fails and only the header is printed.
+set -eu
+
+[ -d "${CLAUDE_PLUGIN_ROOT:-}/rules" ] || exit 0
+
+printf '# Handbook rules\n\n'
+printf 'Team rules that apply in every repository. They are shipped by the handbook plugin; to change one, open a PR against the rules/ folder of the handbook repository.\n\n'
+
+ls "${CLAUDE_PLUGIN_ROOT}"/rules/*.md >/dev/null 2>&1 || exit 0
+
+awk '
+FNR == 1 && NR > 1 { print "" }
+{ print }
+END { print "" }
+' "${CLAUDE_PLUGIN_ROOT}"/rules/*.md
diff --git a/rules/code-changes.md b/plugins/freethinking/rules/code-changes.md
similarity index 100%
rename from rules/code-changes.md
rename to plugins/freethinking/rules/code-changes.md
diff --git a/rules/collaboration.md b/plugins/freethinking/rules/collaboration.md
similarity index 100%
rename from rules/collaboration.md
rename to plugins/freethinking/rules/collaboration.md
diff --git a/rules/comments.md b/plugins/freethinking/rules/comments.md
similarity index 100%
rename from rules/comments.md
rename to plugins/freethinking/rules/comments.md
diff --git a/rules/communication.md b/plugins/freethinking/rules/communication.md
similarity index 100%
rename from rules/communication.md
rename to plugins/freethinking/rules/communication.md
diff --git a/rules/workflow.md b/plugins/freethinking/rules/workflow.md
similarity index 100%
rename from rules/workflow.md
rename to plugins/freethinking/rules/workflow.md
diff --git a/skills/bugfix/SKILL.md b/plugins/freethinking/skills/bugfix/SKILL.md
similarity index 100%
rename from skills/bugfix/SKILL.md
rename to plugins/freethinking/skills/bugfix/SKILL.md
diff --git a/skills/implement-spec/SKILL.md b/plugins/freethinking/skills/implement-spec/SKILL.md
similarity index 100%
rename from skills/implement-spec/SKILL.md
rename to plugins/freethinking/skills/implement-spec/SKILL.md
diff --git a/skills/pr/SKILL.md b/plugins/freethinking/skills/pr/SKILL.md
similarity index 100%
rename from skills/pr/SKILL.md
rename to plugins/freethinking/skills/pr/SKILL.md
diff --git a/skills/pr/pull_request_template.md b/plugins/freethinking/skills/pr/pull_request_template.md
similarity index 100%
rename from skills/pr/pull_request_template.md
rename to plugins/freethinking/skills/pr/pull_request_template.md
diff --git a/skills/report/SKILL.md b/plugins/freethinking/skills/report/SKILL.md
similarity index 100%
rename from skills/report/SKILL.md
rename to plugins/freethinking/skills/report/SKILL.md
diff --git a/skills/report/templates/answer.md b/plugins/freethinking/skills/report/templates/answer.md
similarity index 100%
rename from skills/report/templates/answer.md
rename to plugins/freethinking/skills/report/templates/answer.md
diff --git a/skills/report/templates/decision.md b/plugins/freethinking/skills/report/templates/decision.md
similarity index 100%
rename from skills/report/templates/decision.md
rename to plugins/freethinking/skills/report/templates/decision.md
diff --git a/skills/report/templates/handback.md b/plugins/freethinking/skills/report/templates/handback.md
similarity index 100%
rename from skills/report/templates/handback.md
rename to plugins/freethinking/skills/report/templates/handback.md
diff --git a/skills/report/templates/status.md b/plugins/freethinking/skills/report/templates/status.md
similarity index 100%
rename from skills/report/templates/status.md
rename to plugins/freethinking/skills/report/templates/status.md
diff --git a/skills/start-task/SKILL.md b/plugins/freethinking/skills/start-task/SKILL.md
similarity index 100%
rename from skills/start-task/SKILL.md
rename to plugins/freethinking/skills/start-task/SKILL.md
diff --git a/skills/start-task/spec-template/design.md b/plugins/freethinking/skills/start-task/spec-template/design.md
similarity index 100%
rename from skills/start-task/spec-template/design.md
rename to plugins/freethinking/skills/start-task/spec-template/design.md
diff --git a/skills/start-task/spec-template/requirements.md b/plugins/freethinking/skills/start-task/spec-template/requirements.md
similarity index 100%
rename from skills/start-task/spec-template/requirements.md
rename to plugins/freethinking/skills/start-task/spec-template/requirements.md
diff --git a/skills/start-task/spec-template/tasks.md b/plugins/freethinking/skills/start-task/spec-template/tasks.md
similarity index 100%
rename from skills/start-task/spec-template/tasks.md
rename to plugins/freethinking/skills/start-task/spec-template/tasks.md