diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 16a06c4..0597e45 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -46,10 +46,16 @@ jobs: ref: 7366ac128bdf95f45e6734f412b49a4031800169 path: .upstream/cursor-plugins sparse-checkout: pstack + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + with: + repository: mattpocock/skills + ref: 6654f6b60cd9d5be8b54c6fafe44346dabeb3b76 + path: .upstream/mattpocock-skills - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5 with: node-version: 22 - run: node scripts/check-upstream.mjs --source .upstream/cursor-plugins/pstack --strict + - run: node scripts/skill-baseline.mjs --check --source .upstream/cursor-plugins/pstack --matt-source .upstream/mattpocock-skills package: runs-on: ubuntu-latest diff --git a/README.md b/README.md index d233b78..944bb20 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,25 @@ To validate and print a user's model configuration, run: npm run check-models -- --file ~/.config/mstack/models.json ``` -To compare the local inventory with a pstack checkout, run: +To check the complete skill trees against both pinned upstreams, run: + +```bash +npm run skill-baseline -- --check --source /path/to/pstack --matt-source /path/to/mattpocock-skills +``` + +This checks every file in all 50 skill trees, including references, playbooks, +and scripts. It also checks tools moved out of skill directories. Local +additions and intentional upstream omissions are recorded explicitly. Both +source checkouts must be clean and at their pinned commits. `npm test` checks +the target files without needing upstream checkouts. + +For intentional changes, preview with the same source arguments and `--diff`, +review the source-to-adaptation patches, then use `--write` to update +`profiles/skill-manifest.json`. Commit the baseline with the corresponding +content changes. The hashes detect drift; they do not establish that an adapted +workflow behaves like its source. See [the baseline review process](docs/skill-integrity.md). + +To compare the pstack inventory and non-skill artifacts, run: ```bash npm run check-upstream -- --source /path/to/pstack diff --git a/docs/skill-integrity.md b/docs/skill-integrity.md new file mode 100644 index 0000000..e76202a --- /dev/null +++ b/docs/skill-integrity.md @@ -0,0 +1,83 @@ +# Reviewing skill baselines + +`profiles/skill-manifest.json` records each pinned source file, its local target, +and both SHA-256 digests. Text uses LF line endings. Binary files retain their +bytes. The complete target skill tree is checked, including files added locally. +`profiles/skill-sources.json` declares source skill ownership, moves, and +omissions. Every source file must map to an existing target or an omission with +a reason. Missing files cannot become accepted omissions by refreshing hashes. + +Target inventories include `skills/` and each move's exact target. A moved +directory includes local additions recursively; a single-file move includes +only that file, not its siblings. Installed `node_modules/` dependencies are +excluded from the target inventory. Preview and write use the same inventory +as checks, so intentional additions can be reviewed and recorded normally. + +These are adapted workflows. A matching baseline proves that the recorded files +have not drifted. It does not prove semantic equivalence, that a shorter rewrite +retains every instruction, or that a Harness executed a workflow successfully. +Review those claims through the source diff and workflow verification. + +## Refresh after an intentional edit + +1. Check out the commits in `profiles/upstreams.json` into separate clean + directories. The pstack argument points to the `pstack` subdirectory of + `cursor/plugins`; the Matt Pocock argument points to the repository root. +2. Run the review command. It prints baseline changes, full upstream-to-target + patches, local additions, and every omission reason: + + ```bash + npm run skill-baseline -- --source /path/to/pstack --matt-source /path/to/mattpocock-skills --diff + ``` + +3. Review changed instructions with their linked references. Check that removed + steps are deliberate, links still resolve, and replacements preserve the + workflow's required outcome. Restore accidental deletions. Add a move or + omission rule only when the destination or reason can be reviewed. +4. Run the same command with `--write` in place of `--diff`, then review the Git + diff and run `npm test`. CI additionally checks both pinned source trees. + +The write command changes only the new manifest and uses the upstream sync +lock. It leaves adapted content untouched. Artifact synchronization preserves +its existing transaction behavior and never refreshes skill baselines. + +The earlier `canonicalSkills` block in `profiles/upstream-manifest.json` still +checks the 47 pstack entry documents. When changing one of those entries, update +that existing target digest in the same review. The new manifest covers the +complete trees and the three Matt Pocock skills alongside that check. + +## Parallel branches + +Branches can edit separate workflows independently, but their baselines describe +their own complete checkout. After one branch lands, rebase the next branch and +review its changed content before refreshing the new manifest. Resolve legacy +entry digests against the final bodies. Run both source checks after integration; +do not choose one branch's entire manifest during conflict resolution. + +## Initial inventory decisions + +The initial complete inventory restores the missing Notes example recipes, +the skill-mechanics reference, and the optional Bash human-interaction template. +The template requires an interactive terminal; the reference explains how to +use the Harness's user-input tool when that terminal is unavailable. + +The three Matt Pocock skill bodies and their design references use the pinned +originals as the editing base. The restoration brings back the interface and +adapter vocabulary, independent alternative-design briefs and outputs, the +six diagnostic phases and completion checks, and the writing rules for context +budgets, information hierarchy, co-location, sequence splitting, and pruning. +Local frontmatter, moved reference paths, diagnosis-only authorization, and +available delegation or interaction tools remain explicit adaptations. + +The TypeScript reference again includes the upstream schema, narrowing, +exhaustiveness, boundary, derived-type, and object-argument examples. Corrections +retain validated non-negative durations, construct parsed objects without an +unchecked object assertion, make computed indexing total under +`noUncheckedIndexedAccess`, and qualify parser-specific unknown-field options. + +The three upstream `agents/openai.yaml` files are UI metadata for the upstream +skill package. Their omission reasons and source digests remain in the manifest. +The logger is a Node replacement for a Bash script; meta-mode runtime tools live +under `tools/meta-mode`. The two codebase-design references have explicit moved +targets. Other existing instruction rewrites remain adaptations for review, +with the pinned source path available for every comparison. diff --git a/package.json b/package.json index 503ecbe..1359886 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "check-package": "node scripts/check-package.mjs", "check-models": "node scripts/model-config.mjs", "check-upstream": "node scripts/check-upstream.mjs", + "skill-baseline": "node scripts/skill-baseline.mjs", "run-role": "node scripts/run-role.mjs", "remote-install": "node scripts/remote-install.mjs", "smoke-harnesses": "node scripts/smoke-harnesses.mjs", @@ -23,7 +24,7 @@ "install-skills": "node scripts/install.mjs", "optimize-context": "node scripts/optimize-context.mjs", "reconcile-context": "node scripts/reconcile-context.mjs", - "test": "node scripts/validate.mjs && node --test scripts/install.test.mjs scripts/context.test.mjs scripts/model-config.test.mjs scripts/audit-context.test.mjs scripts/worktree-audit.test.mjs scripts/sync-upstream.test.mjs scripts/runtime.test.mjs scripts/environment.test.mjs scripts/skill-integrity.test.mjs scripts/version-integrity.test.mjs scripts/agent-format.test.mjs scripts/history.test.mjs" + "test": "node scripts/validate.mjs && node scripts/skill-baseline.mjs --check && node --test scripts/install.test.mjs scripts/context.test.mjs scripts/model-config.test.mjs scripts/audit-context.test.mjs scripts/worktree-audit.test.mjs scripts/sync-upstream.test.mjs scripts/runtime.test.mjs scripts/environment.test.mjs scripts/skill-integrity.test.mjs scripts/skill-baseline.test.mjs scripts/version-integrity.test.mjs scripts/agent-format.test.mjs scripts/history.test.mjs" }, "bin": { "mstack": "scripts/install.mjs" diff --git a/profiles/skill-manifest.json b/profiles/skill-manifest.json new file mode 100644 index 0000000..08c133c --- /dev/null +++ b/profiles/skill-manifest.json @@ -0,0 +1,871 @@ +{ + "version": 1, + "sources": { + "pstack": "7366ac128bdf95f45e6734f412b49a4031800169", + "mattpocock/skills": "6654f6b60cd9d5be8b54c6fafe44346dabeb3b76" + }, + "files": { + "skills/architect/references/design-red-flags.md": { + "upstream": "pstack", + "source": "skills/architect/references/design-red-flags.md", + "sourceDigest": "905066f9bbac81c573c2b325be47751d2c0f9325e0a0772bd4384e82dafe9336", + "targetDigest": "905066f9bbac81c573c2b325be47751d2c0f9325e0a0772bd4384e82dafe9336" + }, + "skills/architect/references/rationale-template.md": { + "upstream": "pstack", + "source": "skills/architect/references/rationale-template.md", + "sourceDigest": "0cedfccdc7b47fc881febd5a8293eecbc542c20f8df825639904a8102d2eb5d3", + "targetDigest": "0cedfccdc7b47fc881febd5a8293eecbc542c20f8df825639904a8102d2eb5d3" + }, + "skills/architect/references/runner-prompt.md": { + "upstream": "pstack", + "source": "skills/architect/references/runner-prompt.md", + "sourceDigest": "983bf2820cc14ed8ecab6e448e1033e5cfe6031967d0a9287b48c7fa9c33d925", + "targetDigest": "983bf2820cc14ed8ecab6e448e1033e5cfe6031967d0a9287b48c7fa9c33d925" + }, + "skills/architect/SKILL.md": { + "upstream": "pstack", + "source": "skills/architect/SKILL.md", + "sourceDigest": "897a59beaa3cab107d98c256206e062cc4a5e9421a45b9d462fbded19291fea5", + "targetDigest": "cc8a035aa7a2aa417676c4e364b86f4a3e2b85748bf2d0916402105b011a1083" + }, + "skills/arena/SKILL.md": { + "upstream": "pstack", + "source": "skills/arena/SKILL.md", + "sourceDigest": "8d431f7b1aa19daf6300366fc876c4d1aadac06417ec5502407345ffaf4e90d9", + "targetDigest": "f100383e190b806567015d2cbbaf90b9a1b0151ae60b08aa6185fc6eca3c9552" + }, + "skills/automate-me/SKILL.md": { + "upstream": "pstack", + "source": "skills/automate-me/SKILL.md", + "sourceDigest": "1510979b0b71b54284427a19c09918f8b1fdfc168f9b7f9e30098793a5ec88ef", + "targetDigest": "6ab6c4c4a4b50a7cb182149408bd1758229d4386fbb8b1a66cf6927847b02c27" + }, + "skills/blast-radius/SKILL.md": { + "upstream": "pstack", + "source": "skills/blast-radius/SKILL.md", + "sourceDigest": "20cb2945f5fe62055166f745107274af5f19428ca033e53a9b8bc69650d3ded1", + "targetDigest": "ca63c902c065c3de95d5c7f5db665b4cf8c3b881eafbef9dde3db66ca340a752" + }, + "skills/bro/SKILL.md": { + "upstream": "pstack", + "source": "skills/bro/SKILL.md", + "sourceDigest": "aa329d0ceeeecb6822a7174ad76bb07a7826a3c52ceb6a588dec4138815084bb", + "targetDigest": "8b822d47b9299627f5e4fe42684a031105792c8531443da1ae3423adfa2de96f" + }, + "skills/codebase-design/references/deepening.md": { + "upstream": "mattpocock/skills", + "source": "skills/engineering/codebase-design/DEEPENING.md", + "sourceDigest": "f3dd099ce99289bd213914d8ee3e2429b78309c3957ca4583f7659551b1d53c1", + "targetDigest": "5176f5fcf2606d7d53d9d6d7560c489fc349571e65f03df8ec44862b1b7ed9f0", + "reason": "The adapted dependency guidance lives beside the other design references." + }, + "skills/codebase-design/references/design-alternatives.md": { + "upstream": "mattpocock/skills", + "source": "skills/engineering/codebase-design/DESIGN-IT-TWICE.md", + "sourceDigest": "8e740bf98446dbd4dfdc132ac4346d9a7eedaf93de6a495889171cf7f99f16bd", + "targetDigest": "fe64a751fc7e1a5399126886b1c0832b484364cfeaa7929d1c8e77b6a49eb208", + "reason": "The adapted design comparison workflow lives under references." + }, + "skills/codebase-design/SKILL.md": { + "upstream": "mattpocock/skills", + "source": "skills/engineering/codebase-design/SKILL.md", + "sourceDigest": "2c20617f87ec8af6a434859f381b2f061a69b530444e74eb39e78bb016a6d1e2", + "targetDigest": "2033d2660df023eddc9a72d840daf997bbbe15e9e768228948dbde906e03cf80" + }, + "skills/create-verification-skill/references/feature-map-example/create-note.md": { + "upstream": "pstack", + "source": "skills/create-verification-skill/references/feature-map-example/create-note.md", + "sourceDigest": "644a44c74f35d38c2feb7cd05a0121fdebbb623e8dc376c185b565a581d1ccf7", + "targetDigest": "644a44c74f35d38c2feb7cd05a0121fdebbb623e8dc376c185b565a581d1ccf7" + }, + "skills/create-verification-skill/references/feature-map-example/README.md": { + "upstream": "pstack", + "source": "skills/create-verification-skill/references/feature-map-example/README.md", + "sourceDigest": "cb7bd782cf89968a4ba3d58a5151db837430db92d19a6f52a906973b77b516ba", + "targetDigest": "3810cf289bdd6657f3f23f974262536304a7365ee038e125b7a80343ce7000bf" + }, + "skills/create-verification-skill/references/feature-map-example/search.md": { + "upstream": "pstack", + "source": "skills/create-verification-skill/references/feature-map-example/search.md", + "sourceDigest": "6e87b9e7f2791a7776ba1bb83f371cd285c306c67f19d245cc4dd6ca3015c823", + "targetDigest": "6e87b9e7f2791a7776ba1bb83f371cd285c306c67f19d245cc4dd6ca3015c823" + }, + "skills/create-verification-skill/references/harness-paths.md": { + "upstream": null, + "reason": "Local portability guidance or implementation; review alongside its owning skill.", + "targetDigest": "9b785b0c2dd47b2c9216710641107864a34db2940dd3633869c5d7ccdae182e5" + }, + "skills/create-verification-skill/SKILL.md": { + "upstream": "pstack", + "source": "skills/create-verification-skill/SKILL.md", + "sourceDigest": "644f2551403c1bca01a2855b34611b6e7be0ce0dc5b204514c376c0f6a6e6ac4", + "targetDigest": "7242b478eb0face01b1f1d27950efa69e415219b278993bc3e75f297707bba48" + }, + "skills/diagnosing-bugs/references/feedback-loops.md": { + "upstream": null, + "reason": "Local portability guidance or implementation; review alongside its owning skill.", + "targetDigest": "47ee0c6c9e64a3ae26d9009f0b3cee004cad7e0f559fd4ca455a2a09d9964a88" + }, + "skills/diagnosing-bugs/references/performance.md": { + "upstream": null, + "reason": "Local portability guidance or implementation; review alongside its owning skill.", + "targetDigest": "242bcf8a05319c0ccb214d39a6d0cadbddda49403f0751e2fa5ae5df6dfde1a9" + }, + "skills/diagnosing-bugs/scripts/hitl-loop.template.sh": { + "upstream": "mattpocock/skills", + "source": "skills/engineering/diagnosing-bugs/scripts/hitl-loop.template.sh", + "sourceDigest": "35103539fc36873eea36074769ad454f9379d6fc8b2dc0e26ce987fd3bfe5503", + "targetDigest": "35103539fc36873eea36074769ad454f9379d6fc8b2dc0e26ce987fd3bfe5503" + }, + "skills/diagnosing-bugs/SKILL.md": { + "upstream": "mattpocock/skills", + "source": "skills/engineering/diagnosing-bugs/SKILL.md", + "sourceDigest": "77f3cf31bc99b2f49af943222526531fcc9fc41d047626d3640e875e85af3e84", + "targetDigest": "ecc513932a6cbc3e83a0e8fbd6e6a80d669e8f86c473586b878be58712d58a81" + }, + "skills/figure-it-out/SKILL.md": { + "upstream": "pstack", + "source": "skills/figure-it-out/SKILL.md", + "sourceDigest": "e041c64a39f1084a20e60ee99762c4dbd3ec8be56ed33fd68ea867b8bd8bd3e4", + "targetDigest": "a892d780bae548ec537ad1edea44b11ab85c3223345c84276f567ef30bfdca97" + }, + "skills/how/references/explainer-prompt.md": { + "upstream": "pstack", + "source": "skills/how/references/explainer-prompt.md", + "sourceDigest": "ddb89cebefc24e2b54517a331aedfe045b6971e101f45c71b8cef4cccff45bff", + "targetDigest": "ddb89cebefc24e2b54517a331aedfe045b6971e101f45c71b8cef4cccff45bff" + }, + "skills/how/references/explorer-prompt.md": { + "upstream": "pstack", + "source": "skills/how/references/explorer-prompt.md", + "sourceDigest": "3a6965276d23be7d8da6c305bfd5d7bf29d2aef09e5dae4119463ac72022a02d", + "targetDigest": "3a6965276d23be7d8da6c305bfd5d7bf29d2aef09e5dae4119463ac72022a02d" + }, + "skills/how/SKILL.md": { + "upstream": "pstack", + "source": "skills/how/SKILL.md", + "sourceDigest": "06b714dbd830bc829bfb89e33a1c39daf6d72ea412e488ea9d65f5f33b885a0c", + "targetDigest": "a3e2acb068e126bad1ddadb4b16bb14154d5687fffc8ce2fe8209660b83c2e4b" + }, + "skills/interrogate/references/code-quality-review.md": { + "upstream": "pstack", + "source": "skills/interrogate/references/code-quality-review.md", + "sourceDigest": "623d3d8122d075c303b562783b471a732992e84166b318f85ab6d6c118b13304", + "targetDigest": "623d3d8122d075c303b562783b471a732992e84166b318f85ab6d6c118b13304" + }, + "skills/interrogate/references/lead-judgment.md": { + "upstream": "pstack", + "source": "skills/interrogate/references/lead-judgment.md", + "sourceDigest": "6e5bdf5670eb34017692e9b6c546f36ac7552e6bb9f3f2f3429164fcc46a9361", + "targetDigest": "6e5bdf5670eb34017692e9b6c546f36ac7552e6bb9f3f2f3429164fcc46a9361" + }, + "skills/interrogate/references/reviewer-prompt.md": { + "upstream": "pstack", + "source": "skills/interrogate/references/reviewer-prompt.md", + "sourceDigest": "a397cc61102add709803d917fb23d726920525bf23e7c06dc4ed0b5cbeb00e54", + "targetDigest": "a397cc61102add709803d917fb23d726920525bf23e7c06dc4ed0b5cbeb00e54" + }, + "skills/interrogate/references/rubric.md": { + "upstream": "pstack", + "source": "skills/interrogate/references/rubric.md", + "sourceDigest": "59e9b7cc4556e735e1b859a7dd1f7637b013a2426a84825ac1c51979942c158a", + "targetDigest": "59e9b7cc4556e735e1b859a7dd1f7637b013a2426a84825ac1c51979942c158a" + }, + "skills/interrogate/SKILL.md": { + "upstream": "pstack", + "source": "skills/interrogate/SKILL.md", + "sourceDigest": "2867910309e81411123ae4edca68ffca5795e66521d3f3bc0876f6213b542fb9", + "targetDigest": "ab2508809da00eac47444b102cd7d11730f15b2476cd6cd947e330cc08814e78" + }, + "skills/maintain-verification-skill/SKILL.md": { + "upstream": "pstack", + "source": "skills/maintain-verification-skill/SKILL.md", + "sourceDigest": "515c0eaa054b3f6be1b1fb06f2c2f173c80fddb58bbcac57576f89c479bc68e8", + "targetDigest": "bfa8b97a5ca35d618228c37eb02c74589c0fcb1af13d06201b10e0eb3274e32c" + }, + "skills/make-bot-ui/SKILL.md": { + "upstream": "pstack", + "source": "skills/make-bot-ui/SKILL.md", + "sourceDigest": "3624c8ef16cf2e48deb2a4049e72c6962a9431c8d6c6af90e7bd7ea0375224ec", + "targetDigest": "2f04984c34d64ad8119eac43f24298d480528b14a9876d672aee0c942aa0c2da" + }, + "skills/meta-mode/playbooks/authoring-a-skill.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/authoring-a-skill.md", + "sourceDigest": "f6a13a445c360b48d6cd72780ac01955f45b82826476efe0e67c7559329407aa", + "targetDigest": "51c83fbbc28b4081ca54757cd2a8ee2225644a3ed7618ab4f0673adbc0bc3b0d" + }, + "skills/meta-mode/playbooks/autonomous-run.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/autonomous-run.md", + "sourceDigest": "15e54ad3ac92565613980772d8708de6e19cc4690bfd4bb3fb8836522407377b", + "targetDigest": "ccfacb171d67c4a2b2ec8aec87c78ecba00234ccd99a15e4fe088f43413b1e60" + }, + "skills/meta-mode/playbooks/autopilot-full.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/autopilot-full.md", + "sourceDigest": "52f69170bd7fa6b436992d90d3ef7fba893201e0b497297b416e9aea0e567d5f", + "targetDigest": "0a649471ca2e98d91e2a83c05437ff6c5e4137769df8e12352cc8651c9c27579" + }, + "skills/meta-mode/playbooks/autopilot-stack.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/autopilot-stack.md", + "sourceDigest": "66364cd3a2286cbecfb73d3a7bf7d1fa387ede8525d37ef22b69d4ac03c2c5b2", + "targetDigest": "eb6f902e29a3546d50d95a6f5989654a5ecaa83c94a07a91495e812a923c803f" + }, + "skills/meta-mode/playbooks/babysit.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/babysit.md", + "sourceDigest": "51be21872152f97612d37258273269b88212b6696432d25d678cac30f02f4157", + "targetDigest": "a62385a3c3c3f9f7a54a04b313f76cc5176c188d7b25c32e510208451b3ed026" + }, + "skills/meta-mode/playbooks/bug-fix.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/bug-fix.md", + "sourceDigest": "e958c67d5454fc12d76ec645b50dfd85c423082aac13a49e2913ef2815948216", + "targetDigest": "930fd89634d0377fc9a3209baca2452fd5143827ebdfa38047b81450aa7d11b1" + }, + "skills/meta-mode/playbooks/eval.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/eval.md", + "sourceDigest": "b862bfa2fae2399abf047a8e80d3e3dddb64003af96fb1cebb625216131e36bb", + "targetDigest": "7541b311046f930691e77ab6a71f23b2587a41cbff4804d4b8d097adc92be4d7" + }, + "skills/meta-mode/playbooks/feature.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/feature.md", + "sourceDigest": "430dae0fd736554ff7dffc3ef0644c9f0c845e25742f69e598c35b9b89660301", + "targetDigest": "975a25bc49aa6d6e3d7c1ce984bde5814cab6af4eba6758449b0f81f7d47e876" + }, + "skills/meta-mode/playbooks/hillclimb.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/hillclimb.md", + "sourceDigest": "bc1fc4052c1ee918a56115a219cbcbbc4c147fa1a8080e77c7d58068eecac299", + "targetDigest": "1581e5ef7551e1495b7737bf344612707540ee96e69755e1c8e729b25617608a" + }, + "skills/meta-mode/playbooks/investigation.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/investigation.md", + "sourceDigest": "dc66bcf6af3a074fc7f67aa5e12b93ef571a4fb4d8e17f2f1af5ae5359f42e46", + "targetDigest": "dc66bcf6af3a074fc7f67aa5e12b93ef571a4fb4d8e17f2f1af5ae5359f42e46" + }, + "skills/meta-mode/playbooks/multi-phase-plan.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/multi-phase-plan.md", + "sourceDigest": "63426a55519fbc4fb9f00baf5cc7b1148f70dfb62ad360a0f6533d287dd36edf", + "targetDigest": "62c9a4b1d6bd6e5ad718af0d4972432773afa5af10ae05b8b751f0fc117ac78f" + }, + "skills/meta-mode/playbooks/opening-a-pr.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/opening-a-pr.md", + "sourceDigest": "b173ff9638ff62f91f6813c1a0ed3417cdf9f6545a659be0c1fb1f42130e0e66", + "targetDigest": "143945e5ded37c8d5b2bc0b3889d2af95ecf884dca1edfe597c59abf387a4200" + }, + "skills/meta-mode/playbooks/orchestrate.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/orchestrate.md", + "sourceDigest": "d42b4375f1bd4ce23da0b575a198571a2117df7eea10c3c9fc6d92ae1053f7be", + "targetDigest": "c25dcc2662e15ccb91ff19fbe1a156b7fb97d7d49d0c3478dcd9f001e643e398" + }, + "skills/meta-mode/playbooks/pause-safely.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/pause-safely.md", + "sourceDigest": "7d5fc10e144265f1b35f8157926541e738c1de64b6a85b3e61b51cf268c3dffd", + "targetDigest": "7d5fc10e144265f1b35f8157926541e738c1de64b6a85b3e61b51cf268c3dffd" + }, + "skills/meta-mode/playbooks/perf-issue.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/perf-issue.md", + "sourceDigest": "b2ee9aed1466583361cb61b4e8d79ebb3b61f7f9f6540be7633852afacab1d89", + "targetDigest": "708f59dbe6d969ef8388b575b0b4a614133fb24e207541184eb0d3b62f8e0bb3" + }, + "skills/meta-mode/playbooks/prototype.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/prototype.md", + "sourceDigest": "7aa1f52c691c3d46d5afc527b23f90977fce869ce33573c73c81a4c7f0cde962", + "targetDigest": "7aa1f52c691c3d46d5afc527b23f90977fce869ce33573c73c81a4c7f0cde962" + }, + "skills/meta-mode/playbooks/refactoring.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/refactoring.md", + "sourceDigest": "53bac1f1fe886600dd1991b5511b7975e6a206168038f9d282d88e45408e93cf", + "targetDigest": "4b71e75dc42e0ebf3a459a4cb6e36f88d6ba850dad4713f93e9f6f46c17abaf9" + }, + "skills/meta-mode/playbooks/runtime-forensics.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/runtime-forensics.md", + "sourceDigest": "594f02c40c243a346e1f57b688ecf2ed5e882f21b3d24ed2204d1ac6f1c24a8a", + "targetDigest": "594f02c40c243a346e1f57b688ecf2ed5e882f21b3d24ed2204d1ac6f1c24a8a" + }, + "skills/meta-mode/playbooks/session-pickup.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/session-pickup.md", + "sourceDigest": "1604fb463f7c7410d43fa024ca76c1c1824c166a01fd69d61f073d06fdca2a47", + "targetDigest": "97a3d7b0fcecf53c5c3fce9ec17f105196745086911c363d2dc7841bc56e40e2" + }, + "skills/meta-mode/playbooks/shipping.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/shipping.md", + "sourceDigest": "c301902136e0a9a0a55403c35374f6f5738e501aee66c27458dc197b87557550", + "targetDigest": "09ff98c955958fdf9b062ddafa292e1b6d2f9ae5d3a3eb11e3d979b950568828" + }, + "skills/meta-mode/playbooks/trace-forensics.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/trace-forensics.md", + "sourceDigest": "137ce3984fb96e45971fdff4d3c79fa4ccc647c5b30ae4945783b9fb23f8cec7", + "targetDigest": "137ce3984fb96e45971fdff4d3c79fa4ccc647c5b30ae4945783b9fb23f8cec7" + }, + "skills/meta-mode/playbooks/visual-parity.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/visual-parity.md", + "sourceDigest": "950ca51999a6454c98eee6bcbc3d1bbe501a4e3a5d5439b6f6ddc378991b256c", + "targetDigest": "950ca51999a6454c98eee6bcbc3d1bbe501a4e3a5d5439b6f6ddc378991b256c" + }, + "skills/meta-mode/playbooks/worktree-cleanup.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/playbooks/worktree-cleanup.md", + "sourceDigest": "fc464dc4121926df1704f483c5feca83717c0a40b4651d3e11bd3aedd89441b6", + "targetDigest": "db0c8eeef5dc5054e79c1b3b8215159f48e12a4fb81225d551080f8773fe1b2c" + }, + "skills/meta-mode/references/bugbot-triage.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/references/bugbot-triage.md", + "sourceDigest": "b2d146c770d8d3593e1ef3d77b668039b8e1f78093ea1a86f1d45d7f99614309", + "targetDigest": "b2d146c770d8d3593e1ef3d77b668039b8e1f78093ea1a86f1d45d7f99614309" + }, + "skills/meta-mode/references/capability-matrix.md": { + "upstream": null, + "reason": "Local portability guidance or implementation; review alongside its owning skill.", + "targetDigest": "2b57fe4aef9b300ea5b5e5a8ae93c1a5dbc266d6b9e47bd1ba26c84f1f7f7d39" + }, + "skills/meta-mode/SKILL.md": { + "upstream": "pstack", + "source": "skills/poteto-mode/SKILL.md", + "sourceDigest": "6bbf35cb2111c358461c7ba90f210dad4573f3a3ada92f6459eac0c69d83b01a", + "targetDigest": "53cb6ae809072973802f78fac92fc75efbe111330271513fa756cd2f0d053ad4" + }, + "skills/no-comments/SKILL.md": { + "upstream": "pstack", + "source": "skills/no-comments/SKILL.md", + "sourceDigest": "5c5b0882297d704c3a9720c52b7a793c68b013eaf717989f0945624efdfe2b05", + "targetDigest": "f5bc3892fd057751d5928cffbebabbcde58804021a520bf67fac6b550d3399f5" + }, + "skills/principle-attack-the-premise/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-attack-the-premise/SKILL.md", + "sourceDigest": "c87bd7536f772f8a403ad8155535ee17a1bbec9a353156693eb3bb1520c87ae3", + "targetDigest": "58c2998cbe0318b648d2f78c080003c5d3685a0ede2a98e56bc29e2fdfa7417a" + }, + "skills/principle-boundary-discipline/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-boundary-discipline/SKILL.md", + "sourceDigest": "62ac2862c2caa22dd14531f51c9c6bb445ceb198ca98a687e25b4d47c1f10a28", + "targetDigest": "0ebee28a5f8bf5cac890dc37a18f3cdf66c360d85336fe8efa0be04430ecdddf" + }, + "skills/principle-build-the-lever/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-build-the-lever/SKILL.md", + "sourceDigest": "458b269eab8e95a1573276955312f7d2a13ae8599879a9efc748248f6bc34b56", + "targetDigest": "b49623e8e41c072ce14e6479a0a93cb932cd1a320642334cf0ac8f9e7cf31ded" + }, + "skills/principle-encode-lessons-in-structure/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-encode-lessons-in-structure/SKILL.md", + "sourceDigest": "77044c83a9ac6df78fc643887ea5f1f11f082adaf14b717c060cdaed03cce863", + "targetDigest": "e66d95bd954a1e8ba298bc451ba1e9190c758f6d77c240540bedf8f28c895694" + }, + "skills/principle-exhaust-the-design-space/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-exhaust-the-design-space/SKILL.md", + "sourceDigest": "8583310d7297b7823d75494faa57b003564d436807febecdecbd8321abe5cf6e", + "targetDigest": "67e7e29ab4f484bfddcac55ece4d70f6efc25ccbf91de7de31fcec385f9f059e" + }, + "skills/principle-experience-first/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-experience-first/SKILL.md", + "sourceDigest": "58903529d733b1c1a7c9ccf127d45ef3c7d0d9f0b81164a4dc6b80efe6f2912b", + "targetDigest": "3013367ce42fabb55799ffbcc51ad20b2ae7ec1b898bad4ac4ceaa2f093edbe1" + }, + "skills/principle-fix-root-causes/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-fix-root-causes/SKILL.md", + "sourceDigest": "ba4cd38da1dcc8fe5432feb64457cba95594736a9b21b2ebf8f8c52aee068707", + "targetDigest": "2f48a508674c8b9f3339aba95929079af54fb7fbd367b4eb45175388f1e170f9" + }, + "skills/principle-foundational-thinking/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-foundational-thinking/SKILL.md", + "sourceDigest": "864b827e8199d946ce58beca0f89c4ed4099ed6e426fb698a3aaa45de2a9eea2", + "targetDigest": "e69eea824d81c4bb32cbcdb26513bf964882daeeb647bb99210978e86bec1a49" + }, + "skills/principle-guard-the-context-window/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-guard-the-context-window/SKILL.md", + "sourceDigest": "9f60e2c79b35f865cb103462f9128c6760b70f603475d1e535e5930f61840f66", + "targetDigest": "d42b8f8b02cdcb4f84b466ffcd623ca911a8bbf93b46b545d8b3ca4e950b2580" + }, + "skills/principle-laziness-protocol/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-laziness-protocol/SKILL.md", + "sourceDigest": "9006579e9e3f33955d17569733db688ae65f22577dc3b21803d219a5a8445aff", + "targetDigest": "f45159ad7ea5f01a08d364897307f69ca18fa57b87d6ed6b89ca222a2388bf7d" + }, + "skills/principle-make-operations-idempotent/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-make-operations-idempotent/SKILL.md", + "sourceDigest": "540738217c3da7bf513b9886924ad6a2bc77e58be867f5bd2e1831c74bf2ce70", + "targetDigest": "5cf54da6cc34714fbe97e45fd61a4678662d087d7f6f6dc81cace3f34041f541" + }, + "skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md", + "sourceDigest": "09978915a4a11990cf0a9a02d9c08cc59e0b7a04c5c24b83aa560fbe4146f75b", + "targetDigest": "388e9cbc9e52bd72d9044d7e37126bd3355c4e8f1188447e2a6850fe4fc549d7" + }, + "skills/principle-minimize-reader-load/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-minimize-reader-load/SKILL.md", + "sourceDigest": "42551c5dc74cd578163c148da552db3ff67d29b78600ef216bfd311fb828fe69", + "targetDigest": "d44ded81ac7cfc194dca65c9943a55fe8a058660485049a6e607e6bdf5d70738" + }, + "skills/principle-model-the-domain/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-model-the-domain/SKILL.md", + "sourceDigest": "bbadbb9a723fac3f76e782ae45665eb94058ec10993e4bae36d4dc6b0e4eac07", + "targetDigest": "4464cc1a0a2366ff8249659c30f525dba6206947f037f07f5931238ed0e96f82" + }, + "skills/principle-never-block-on-the-human/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-never-block-on-the-human/SKILL.md", + "sourceDigest": "d82bce9ab9193249d1ec0d514202d89602f0fd3d312174d622c2dd222bae5a2e", + "targetDigest": "15d583bb6125a88dd0ff375bfdbdc1ac874027bf741cda5f6f4d3e939363051e" + }, + "skills/principle-outcome-oriented-execution/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-outcome-oriented-execution/SKILL.md", + "sourceDigest": "beedcaed75572a6a1662d47f16da203c2bd2c9dbffbbd678d4ba187d67b7e670", + "targetDigest": "f2fe80adf2da1e1e15aa1d1b465f1cb963acb05114be9032a89ae6ead06df288" + }, + "skills/principle-prove-it-works/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-prove-it-works/SKILL.md", + "sourceDigest": "ae13d287984a5864312b6286899d4ee2b60f8272b5e5e8fcc586a7c3ea51020b", + "targetDigest": "eb89aa6f3283cd73ba56d6cc01e0d4b9ec12367144988525b2c43761cbed4ada" + }, + "skills/principle-redesign-from-first-principles/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-redesign-from-first-principles/SKILL.md", + "sourceDigest": "a1c7fd0a96b40e12a74bceb0e6bb20a9666dd844dcabfb624acb27da23b929fe", + "targetDigest": "b3ac6bc1c707aa8ad1a331851ad71aeb48f179136907ab2015f34881c674d056" + }, + "skills/principle-separate-before-serializing-shared-state/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-separate-before-serializing-shared-state/SKILL.md", + "sourceDigest": "05294b40448e927c5da1c7b6f5c6b9fed3744637e3833d2d948b9154a9dd00bb", + "targetDigest": "30cc80284a90bb5d94cde5a3533a8a27755832c2c4e638f8e3b97bbf8ba94924" + }, + "skills/principle-sequence-verifiable-units/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-sequence-verifiable-units/SKILL.md", + "sourceDigest": "761390804ee36c32f5338d8db47ec54463d40ff96dbb296dfafabcd5f2593eec", + "targetDigest": "84bf426fd9c7f2bb8561baad84a06bb5b2c78534e7599bdd3fde47ce5596d707" + }, + "skills/principle-subtract-before-you-add/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-subtract-before-you-add/SKILL.md", + "sourceDigest": "a983a50e732c1ed315eba7aaf47a61329e31c4794eb1ab9d25d9945265b31e03", + "targetDigest": "65fa4e1a2a630040eb0abbcfeaf0e82241fd968443021b535f94b4b6d4b7aa0f" + }, + "skills/principle-test-behavior-not-implementation/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-test-behavior-not-implementation/SKILL.md", + "sourceDigest": "87e40efe4e486f7ea639d2ed1fc0abe6c93b8fd0e89a81218086909d2470a52e", + "targetDigest": "c6104107aa4902342d36eb5c6b7f99862eadff5ba27f367cde011ccdde4d8d17" + }, + "skills/principle-type-system-discipline/SKILL.md": { + "upstream": "pstack", + "source": "skills/principle-type-system-discipline/SKILL.md", + "sourceDigest": "c83da8031ffb4c86410030d3e261f659598fca2b7d8158fd9dc269d70a3a025e", + "targetDigest": "9397a9ce6e26e54d0e6e9f81e488f626f3daefd6903509536fbdfb0d021a274f" + }, + "skills/recall/references/history-sources.md": { + "upstream": null, + "reason": "Local portability guidance or implementation; review alongside its owning skill.", + "targetDigest": "22f657e947e100c6c1e65877029579eaf21638321e6d580246a5ea6c4492087b" + }, + "skills/recall/scripts/history.mjs": { + "upstream": null, + "reason": "Local portability guidance or implementation; review alongside its owning skill.", + "targetDigest": "fe3284380308ecd0cf232072c7bc8e59c0f56ffecc42851d1fddb2c382ab2683" + }, + "skills/recall/SKILL.md": { + "upstream": "pstack", + "source": "skills/recall/SKILL.md", + "sourceDigest": "d265b0e5b6dc5b89c0407f3387d24462c988e1990a37d5359735137908128c5a", + "targetDigest": "be67c9e064a2b6d2ec52e915c19f6507f9f389a112e9961eb93b3c793b565e70" + }, + "skills/reflect/references/divergent-reviewer.md": { + "upstream": "pstack", + "source": "skills/reflect/references/divergent-reviewer.md", + "sourceDigest": "7da0334ce282e6d8d7349c0cff14ffbd10a91a756c0b5c3ccc97291f57b8bed6", + "targetDigest": "1e3f57c622107887b6507e1c7fa599367a629ad05273322814cca87614ef5f02" + }, + "skills/reflect/references/judgment-reviewer.md": { + "upstream": "pstack", + "source": "skills/reflect/references/judgment-reviewer.md", + "sourceDigest": "917bb2ad21240a42b440aeb2a17766092e09f70d0982417b2f4caa6d74f27164", + "targetDigest": "7fec7389146c2d13fbe1e08382041420cfb43df9ac09ec54867eb6f4a7942044" + }, + "skills/reflect/references/synthesizer.md": { + "upstream": "pstack", + "source": "skills/reflect/references/synthesizer.md", + "sourceDigest": "2f76c847eb45b182dc6fbcbf7d96c8ee7e2986ecfd9e0d640fec44e3cc5a617c", + "targetDigest": "473acafc84aa4d712721d3424c81e281c9e7efdb4b3a70095840d059fb3938f0" + }, + "skills/reflect/references/tooling-reviewer.md": { + "upstream": "pstack", + "source": "skills/reflect/references/tooling-reviewer.md", + "sourceDigest": "8c74ebf7811801d634db7ab609b097d77ea00ab77bb4b258d69bd96e671cfa39", + "targetDigest": "9766d27b377ca63a2c3dc44ebd8b8ccbde47e5f7c063c19a400e3398ffa6b516" + }, + "skills/reflect/SKILL.md": { + "upstream": "pstack", + "source": "skills/reflect/SKILL.md", + "sourceDigest": "6a4d4ccaace9ce88c3e268fcbedf3cfd40e527162a5e5aad6de8edbaf5e2b83f", + "targetDigest": "5478382b85614482fdf8799c9b0113d0c12c6595ee1003aa62c91f6288b91089" + }, + "skills/setup-mstack/SKILL.md": { + "upstream": "pstack", + "source": "skills/setup-pstack/SKILL.md", + "sourceDigest": "5acdc3db4f15f3e9b81fb56eab7f17cdccde1a11a40053ed7743f7467729c8a9", + "targetDigest": "555c577bf494baafafa980ee3f647149c180a615a706ef409845cd03cd16e142" + }, + "skills/show-me-your-work/references/decision-log-template.tsv": { + "upstream": "pstack", + "source": "skills/show-me-your-work/references/decision-log-template.tsv", + "sourceDigest": "6779378006a2f5225e8c06deb84f1d913fb532041ebbb6e50d94600bfe3f4b76", + "targetDigest": "6779378006a2f5225e8c06deb84f1d913fb532041ebbb6e50d94600bfe3f4b76" + }, + "skills/show-me-your-work/scripts/log.mjs": { + "upstream": "pstack", + "source": "skills/show-me-your-work/scripts/log.sh", + "sourceDigest": "1601bc18f9aba0af3f5d6375e4f2d5dc6102334d1bca935026c0e3547cdadbbf", + "targetDigest": "1fa4184750bee4a37aedd1a9e7461f4d2bc121830b7724857fa5fbe2fb3c43f7", + "reason": "The Node implementation replaces the Bash logger for Windows portability." + }, + "skills/show-me-your-work/SKILL.md": { + "upstream": "pstack", + "source": "skills/show-me-your-work/SKILL.md", + "sourceDigest": "831e85ba3f84f38bf338cd03e6af050fea357752e25a825e334c99f59d7f2077", + "targetDigest": "d189454d45306b8b2ace4456684fc9dbaf9d3c5305e2fe3c35afed8977f1552f" + }, + "skills/swarm/SKILL.md": { + "upstream": "pstack", + "source": "skills/swarm/SKILL.md", + "sourceDigest": "d578d877c3b8b9de7163f52579c4683337a940acc0fbe4496a498f5d3b27df30", + "targetDigest": "015c31b71a020a585b628ed8308245530c008b42facedd3040b385628b3ea599" + }, + "skills/tdd/SKILL.md": { + "upstream": "pstack", + "source": "skills/tdd/SKILL.md", + "sourceDigest": "011cab0ecc04a3632121efb493ae4d60aa282a9b72c66de74dd8ad7e4313e05a", + "targetDigest": "bb077c26ad22c0171f3c06e72d288556a46a8fce34164ea7da686d78c135544c" + }, + "skills/teach/SKILL.md": { + "upstream": "pstack", + "source": "skills/teach/SKILL.md", + "sourceDigest": "bd286e5a6290673be2cd28aef7af2ec7f1fb138e6bce156ea657368c7e7fc8a4", + "targetDigest": "5a85664f8cce57429af0d8c060a20d8740579867f0976f4981032e8855cab2b5" + }, + "skills/technical-writing/SKILL.md": { + "upstream": "pstack", + "source": "skills/technical-writing/SKILL.md", + "sourceDigest": "6d6d9e625ae412ee8f36df8deb6fd9203d7c6acca776d5ee3cabbb9126f8b84c", + "targetDigest": "40dafbe83d76600dfd9417bb3861b2a73ac75010838412a10b8fa2b82b00b9a0" + }, + "skills/typescript-best-practices/references/patterns.md": { + "upstream": "pstack", + "source": "skills/typescript-best-practices/references/patterns.md", + "sourceDigest": "62e9cd32d2fa066a35d0d1349aac22dce31a48ad7d5eaf142570770cfe707384", + "targetDigest": "4f5ef6f9ab4d158910249aa375d14f939d46ec4441a9750060cef54e26691682" + }, + "skills/typescript-best-practices/SKILL.md": { + "upstream": "pstack", + "source": "skills/typescript-best-practices/SKILL.md", + "sourceDigest": "28f9e61710e205f6f3c5476f333483cff0e988f6a5b639e2faad536344b40c5e", + "targetDigest": "02b41b2cbb0ac43940b8f4df8b54c55f1aac0a68890a9379f9b006b2a8f1f4a0" + }, + "skills/unslop/SKILL.md": { + "upstream": "pstack", + "source": "skills/unslop/SKILL.md", + "sourceDigest": "c6d2572294d933a428211921069e9a248490e9b466ca59437cb4d9de248600ca", + "targetDigest": "6948335a00f9fed7a29204b9f08f74a9d9596d90989fd06f58f02d984ceab6f3" + }, + "skills/why/references/epistemics.md": { + "upstream": "pstack", + "source": "skills/why/references/epistemics.md", + "sourceDigest": "fcc0f559c072832c869a5ada72434b4849320a637ae63c09e8ee14f7ad787059", + "targetDigest": "fcc0f559c072832c869a5ada72434b4849320a637ae63c09e8ee14f7ad787059" + }, + "skills/why/references/investigator-prompt.md": { + "upstream": "pstack", + "source": "skills/why/references/investigator-prompt.md", + "sourceDigest": "6892956a7ab0b5ee6c4babaf4f793703abe89b350ea7cb9463b4a56eea1c8266", + "targetDigest": "6892956a7ab0b5ee6c4babaf4f793703abe89b350ea7cb9463b4a56eea1c8266" + }, + "skills/why/references/source-playbook.md": { + "upstream": "pstack", + "source": "skills/why/references/source-playbook.md", + "sourceDigest": "9ae9dcb9d09111c9facc68510331c5aba4aed9b1b057314ff1fc0e166ad3993a", + "targetDigest": "9ae9dcb9d09111c9facc68510331c5aba4aed9b1b057314ff1fc0e166ad3993a" + }, + "skills/why/references/sources/code-archaeology.md": { + "upstream": "pstack", + "source": "skills/why/references/sources/code-archaeology.md", + "sourceDigest": "8c14aa382f0de40829b2668d43e3f54814480bb93a486c9282ada4003f6b9dbd", + "targetDigest": "8c14aa382f0de40829b2668d43e3f54814480bb93a486c9282ada4003f6b9dbd" + }, + "skills/why/references/sources/databricks.md": { + "upstream": "pstack", + "source": "skills/why/references/sources/databricks.md", + "sourceDigest": "236ede7aab6454662a992db088a5393537a5233a4c26719c93188f9e174488f6", + "targetDigest": "236ede7aab6454662a992db088a5393537a5233a4c26719c93188f9e174488f6" + }, + "skills/why/references/sources/datadog.md": { + "upstream": "pstack", + "source": "skills/why/references/sources/datadog.md", + "sourceDigest": "ebe9c07bac28e47757ee7f16fb93b0c01d427a08c501957d047f395a2f37b7e7", + "targetDigest": "ebe9c07bac28e47757ee7f16fb93b0c01d427a08c501957d047f395a2f37b7e7" + }, + "skills/why/references/sources/incident-postmortem.md": { + "upstream": "pstack", + "source": "skills/why/references/sources/incident-postmortem.md", + "sourceDigest": "a899470f05f86029630a13a2696491cc708ca7b56a6e822a69ff6e954d6ff8cf", + "targetDigest": "a899470f05f86029630a13a2696491cc708ca7b56a6e822a69ff6e954d6ff8cf" + }, + "skills/why/references/sources/linear.md": { + "upstream": "pstack", + "source": "skills/why/references/sources/linear.md", + "sourceDigest": "01304e7f1badfcfb78babbb0edcadeb3cc8dd3f713793420cdff24ba190f907c", + "targetDigest": "01304e7f1badfcfb78babbb0edcadeb3cc8dd3f713793420cdff24ba190f907c" + }, + "skills/why/references/sources/notion.md": { + "upstream": "pstack", + "source": "skills/why/references/sources/notion.md", + "sourceDigest": "3bcfa695c49dfad4ac036d5d7181c12585f55ac04f1936198d16ab75a2788997", + "targetDigest": "3bcfa695c49dfad4ac036d5d7181c12585f55ac04f1936198d16ab75a2788997" + }, + "skills/why/references/sources/sentry.md": { + "upstream": "pstack", + "source": "skills/why/references/sources/sentry.md", + "sourceDigest": "6f292bc4fb1548e956f95e1900b9481abf5134254419f1cd9edec95428c82d73", + "targetDigest": "6f292bc4fb1548e956f95e1900b9481abf5134254419f1cd9edec95428c82d73" + }, + "skills/why/references/sources/slack.md": { + "upstream": "pstack", + "source": "skills/why/references/sources/slack.md", + "sourceDigest": "58fee61d024b3700b407e7acbed367944720ff05adfddb6954d36c88368f3ba7", + "targetDigest": "58fee61d024b3700b407e7acbed367944720ff05adfddb6954d36c88368f3ba7" + }, + "skills/why/references/synthesizer-prompt.md": { + "upstream": "pstack", + "source": "skills/why/references/synthesizer-prompt.md", + "sourceDigest": "733a3e9cf897b1aca56aaddeae01a3832e4bfed024c2f602db2d1bf807d7a73b", + "targetDigest": "733a3e9cf897b1aca56aaddeae01a3832e4bfed024c2f602db2d1bf807d7a73b" + }, + "skills/why/SKILL.md": { + "upstream": "pstack", + "source": "skills/why/SKILL.md", + "sourceDigest": "222b93789754ba6ee901835137184acdca667f7e47c523bcc85aca86675fb249", + "targetDigest": "873aedc86d1321093e9a195ac8bff1783241b309a1d33808ab74f181c4b8f22d" + }, + "skills/writing-for-agents/SKILL-MECHANICS.md": { + "upstream": "mattpocock/skills", + "source": "skills/productivity/writing-for-agents/SKILL-MECHANICS.md", + "sourceDigest": "c768e6307c7c10728c401c213f2c4ba71c542127eeb7ad2956aabd15a0fa0059", + "targetDigest": "3998b4c619f6912b24afc28d5918caf7c5f2f00d0f3aecc676f79de78d07d5df" + }, + "skills/writing-for-agents/SKILL.md": { + "upstream": "mattpocock/skills", + "source": "skills/productivity/writing-for-agents/SKILL.md", + "sourceDigest": "551adca942227b44192edba88acd4e8db911f0121ce58ad16944ccf6a896a74a", + "targetDigest": "3b50712cac5d212e449bc9e9baeed4866412b152aa30feef53fdc4a0f3791840" + }, + "tools/meta-mode/.npmignore": { + "upstream": null, + "reason": "Local portability guidance or implementation; review alongside its owning skill.", + "targetDigest": "4d56952b0fb13bf8f9b6c13a6d4c34a075bac3af447636a1df4335d7576e2f97" + }, + "tools/meta-mode/bootstrap.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/bootstrap.ts", + "sourceDigest": "ccd2ed08fd9da9d0d942e5f2491cfc74bb92d8a08fa51755e0da1ac8de287361", + "targetDigest": "d9d39f235da5512de6122f1ff28584b3a0120b170f5447a7a5868f7cf187d12a", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/bun.lock": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/bun.lock", + "sourceDigest": "667d9cf7222e9aae3fa42a4be383e657784216e9df115da02227fbc5ac7bbe46", + "targetDigest": "6413513594536785c3fd52b14934dd52d1ce179ad3ffe99d77535f5b637435a2", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/check-plan.mjs": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/check-plan.mjs", + "sourceDigest": "4f5118edf719b481983dc87dfadd5b4b5473d05aad04a7ba65191ff9c73faf1b", + "targetDigest": "ddcdb05af01b37d7513b2017d44e29c30bdaa79ca360c5913d701c16dde70907", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/orch/orch.test.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/orch/orch.test.ts", + "sourceDigest": "174b86d21ff51ccd28d1a8c938cb2e0c6fa287b209d4ca6c03932fcb1b53372e", + "targetDigest": "913d5c03ba456de547ec609458a2b14c5a3894d33385b0597ee44315252f870b", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/orch/orch.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/orch/orch.ts", + "sourceDigest": "f091687df627a0b75fabd54af58945a9cd6c7039ef0622012ac9ed60cd8ec434", + "targetDigest": "f091687df627a0b75fabd54af58945a9cd6c7039ef0622012ac9ed60cd8ec434", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/orch/store.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/orch/store.ts", + "sourceDigest": "0eae7cf69282e827b1227166f2e32cee3560f2f3e65b3f1f2faf6fb83d3b4c5b", + "targetDigest": "39394893aa8191f411a5eee33a554a6232e4568ecf11d7b43f6a21752d4c97cd", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/package.json": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/package.json", + "sourceDigest": "d1f815091209d49763775cc188e8e06ff32d5fec648bffd2272bc25a165e87c3", + "targetDigest": "c7ac7fd69bb925415cdc15cd636567304ee40f8d665986e45ed8c0ee658f6caf", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/README.md": { + "upstream": null, + "reason": "Local portability guidance or implementation; review alongside its owning skill.", + "targetDigest": "c27f30aa0fa15655a9c8dbd9df698a2c69393778bbe516ad5ebdf82ca0de71bd" + }, + "tools/meta-mode/watch-pr/cli.test.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/watch-pr/cli.test.ts", + "sourceDigest": "a35592b4c6da96caf8d47987e9cd503fab6c7774ab87d11a43f24b4b5df2a9d1", + "targetDigest": "a35592b4c6da96caf8d47987e9cd503fab6c7774ab87d11a43f24b4b5df2a9d1", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/watch-pr/cli.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/watch-pr/cli.ts", + "sourceDigest": "89c08863089181a232be782c4b2ed90a1552f44aeecb0dcb7aac2c623620e680", + "targetDigest": "89c08863089181a232be782c4b2ed90a1552f44aeecb0dcb7aac2c623620e680", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/watch-pr/fakes.test-helper.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts", + "sourceDigest": "e53e31b7f03068063f53a99a3591b052aaed66e4e2b196b3a364980f60324ab1", + "targetDigest": "e53e31b7f03068063f53a99a3591b052aaed66e4e2b196b3a364980f60324ab1", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/watch-pr/github.test.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/watch-pr/github.test.ts", + "sourceDigest": "0cbce011050522938523e36bb42a260e92b812dcd2076930fa681672ad316b24", + "targetDigest": "42e2c624bb7290761c05cb24a4b7e616c6f3c3d8af46aba5cf30ead38023786e", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/watch-pr/github.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/watch-pr/github.ts", + "sourceDigest": "d1bbbafbe8e2c703c84da2029de7f2c4011412512f8cf65a42100f46bcb92c8a", + "targetDigest": "8f44dff3af48e9872cc2f3a20cf0a6128a1f4dd3054afeabee054e51776f7ec1", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/watch-pr/policy.test.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/watch-pr/policy.test.ts", + "sourceDigest": "a9fedb25feefd1f57ef6a662f80c99018690170a3a8070773a08eaf694d1d3d9", + "targetDigest": "a9fedb25feefd1f57ef6a662f80c99018690170a3a8070773a08eaf694d1d3d9", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/watch-pr/policy.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/watch-pr/policy.ts", + "sourceDigest": "68d240505852cf23c9b2dea463342e9e61243f486b3cc86c735d140f9ee7be49", + "targetDigest": "68d240505852cf23c9b2dea463342e9e61243f486b3cc86c735d140f9ee7be49", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/watch-pr/render.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/watch-pr/render.ts", + "sourceDigest": "2b709e86628d5b8fdb61f860e60ababc342b0626c67576305442cf548283a604", + "targetDigest": "2b709e86628d5b8fdb61f860e60ababc342b0626c67576305442cf548283a604", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/watch-pr/tsconfig.json": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/watch-pr/tsconfig.json", + "sourceDigest": "1ecccc72320b086c63ce3f73c959896fd747363e63553b3b1136296ea4069e45", + "targetDigest": "1ecccc72320b086c63ce3f73c959896fd747363e63553b3b1136296ea4069e45", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/watch-pr/types.compile.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/watch-pr/types.compile.ts", + "sourceDigest": "621af4c9a664fbf699a9737efccdc248abb416df8819c9dc5f824beae2b9b1d6", + "targetDigest": "621af4c9a664fbf699a9737efccdc248abb416df8819c9dc5f824beae2b9b1d6", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/watch-pr/types.ts": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/watch-pr/types.ts", + "sourceDigest": "64ba6221e3bfe61bbb292337d199cdec6309455477280efb060203dba34e0e91", + "targetDigest": "979de5c965f10a853bac7847e043fff613e7884a93c16fc95a39766177588ef7", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/watch-pr/watch-pr": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/watch-pr/watch-pr", + "sourceDigest": "d955603be6cc0e8b8ffcec722f635192b2261410b1f2929abea94480e47eb5d4", + "targetDigest": "d955603be6cc0e8b8ffcec722f635192b2261410b1f2929abea94480e47eb5d4", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/worktree-audit.sh": { + "upstream": "pstack", + "source": "skills/poteto-mode/scripts/worktree-audit.sh", + "sourceDigest": "95b8530f3c0ccada8cabc4b0727c052fbc85c141531375aaace65c427af8aecc", + "targetDigest": "68df230b34f10558e021bb6cc9696e4653acc92645a17b35c6b953aa40837fc2", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "tools/meta-mode/worktree-audit.test.sh": { + "upstream": null, + "reason": "Local portability guidance or implementation; review alongside its owning skill.", + "targetDigest": "dfc8c11e867f5000a996e8b4aeafc630319964bb1b2946b1a626996dd4919e0f" + } + }, + "omitted": { + "mattpocock/skills:skills/engineering/codebase-design/agents/openai.yaml": { + "upstream": "mattpocock/skills", + "source": "skills/engineering/codebase-design/agents/openai.yaml", + "sourceDigest": "edebc9e4fcfe102114012575eaa9600b9b5fd08c311664f389c36e7bc717740f", + "reason": "Upstream UI metadata is specific to its skill packaging; mstack uses its shared frontmatter and per-Harness adapters." + }, + "mattpocock/skills:skills/engineering/diagnosing-bugs/agents/openai.yaml": { + "upstream": "mattpocock/skills", + "source": "skills/engineering/diagnosing-bugs/agents/openai.yaml", + "sourceDigest": "3e430dbe4334a87597488c060cb3dc3786bb00c9182877d6f5ec41f62490e90b", + "reason": "Upstream UI metadata is specific to its skill packaging; mstack uses its shared frontmatter and per-Harness adapters." + }, + "mattpocock/skills:skills/productivity/writing-for-agents/agents/openai.yaml": { + "upstream": "mattpocock/skills", + "source": "skills/productivity/writing-for-agents/agents/openai.yaml", + "sourceDigest": "eacb24b2a618cfb81dacb0416f4fdd75ddf3a8060f8ddb99aae1b1e301907e4b", + "reason": "Upstream UI metadata is specific to its skill packaging; mstack uses its shared frontmatter and per-Harness adapters." + } + } +} diff --git a/profiles/skill-sources.json b/profiles/skill-sources.json new file mode 100644 index 0000000..ff9ff43 --- /dev/null +++ b/profiles/skill-sources.json @@ -0,0 +1,37 @@ +{ + "pstack": { + "root": "skills", + "moves": { + "skills/poteto-mode/scripts": { + "target": "tools/meta-mode", + "reason": "Runtime tools are packaged separately from portable skill instructions." + }, + "skills/show-me-your-work/scripts/log.sh": { + "target": "skills/show-me-your-work/scripts/log.mjs", + "reason": "The Node implementation replaces the Bash logger for Windows portability." + } + } + }, + "mattpocock/skills": { + "skills": { + "skills/engineering/codebase-design": "codebase-design", + "skills/engineering/diagnosing-bugs": "diagnosing-bugs", + "skills/productivity/writing-for-agents": "writing-for-agents" + }, + "moves": { + "skills/engineering/codebase-design/DEEPENING.md": { + "target": "skills/codebase-design/references/deepening.md", + "reason": "The adapted dependency guidance lives beside the other design references." + }, + "skills/engineering/codebase-design/DESIGN-IT-TWICE.md": { + "target": "skills/codebase-design/references/design-alternatives.md", + "reason": "The adapted design comparison workflow lives under references." + } + }, + "omit": { + "skills/engineering/codebase-design/agents/openai.yaml": "Upstream UI metadata is specific to its skill packaging; mstack uses its shared frontmatter and per-Harness adapters.", + "skills/engineering/diagnosing-bugs/agents/openai.yaml": "Upstream UI metadata is specific to its skill packaging; mstack uses its shared frontmatter and per-Harness adapters.", + "skills/productivity/writing-for-agents/agents/openai.yaml": "Upstream UI metadata is specific to its skill packaging; mstack uses its shared frontmatter and per-Harness adapters." + } + } +} diff --git a/scripts/check-upstream.mjs b/scripts/check-upstream.mjs index 9f2ce40..edf1927 100644 --- a/scripts/check-upstream.mjs +++ b/scripts/check-upstream.mjs @@ -369,9 +369,9 @@ if (!existsSync(manifestPath)) { if (!knownTargets.has(targetName)) canonicalProblems.push(`${targetName}: stale manifest entry`); } console.log(`canonical skill bodies: ${canonicalProblems.length ? `${canonicalProblems.length} problem(s)` : "all match manifest"}`); - if (strict && canonicalProblems.length) { + if (canonicalProblems.length) { for (const problem of canonicalProblems) console.error(` ${problem}`); - artifactProblems += canonicalProblems.length; + if (strict) artifactProblems += canonicalProblems.length; } } const removed = []; diff --git a/scripts/skill-baseline-lib.mjs b/scripts/skill-baseline-lib.mjs new file mode 100644 index 0000000..630d821 --- /dev/null +++ b/scripts/skill-baseline-lib.mjs @@ -0,0 +1,192 @@ +import { createHash } from "node:crypto"; +import { execFileSync } from "node:child_process"; +import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs"; +import { join, resolve } from "node:path"; + +const textExtensions = /\.(?:cjs|css|cts|hbs|html|js|json|lock|md|mjs|sh|ts|tsx|tsv|txt|yaml|yml)$/i; +const hashPattern = /^[a-f0-9]{64}$/; + +export function inside(root, path) { + if (typeof path !== "string" || !path || path.includes("\\") || path.startsWith("/") || + path.split("/").some((part) => !part || part === "." || part === ".." || part.includes(":"))) { + throw new Error(`Invalid checkout-relative path: ${path}`); + } + let current = resolve(root); + for (const part of path.split("/")) { + current = join(current, part); + if (existsSync(current) && lstatSync(current).isSymbolicLink()) { + throw new Error(`Symbolic links are not supported in skill baselines: ${path}`); + } + } + return current; +} + +export function filesUnder(root, path, { excludeDependencies = false } = {}) { + const directory = inside(root, path); + return readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en")) + .flatMap((entry) => { + if (excludeDependencies && entry.name === "node_modules" && (entry.isDirectory() || entry.isSymbolicLink())) return []; + const child = `${path}/${entry.name}`; + if (entry.isDirectory()) return filesUnder(root, child, { excludeDependencies }); + if (!entry.isFile()) throw new Error(`Unsupported file type: ${child}`); + return [child]; + }); +} + +export function contentDigest(root, path) { + const buffer = readFileSync(inside(root, path)); + const content = textExtensions.test(path) || path.endsWith("/watch-pr") + ? buffer.toString("utf8").replace(/\r\n/g, "\n") : buffer; + return createHash("sha256").update(content).digest("hex"); +} + +function record(value) { + return value && typeof value === "object" && !Array.isArray(value); +} + +function json(root, path) { + return JSON.parse(readFileSync(inside(root, path), "utf8")); +} + +function targetFiles(root, profiles) { + const options = { excludeDependencies: true }; + const files = new Set(filesUnder(root, "skills", options)); + for (const spec of Object.values(profiles)) { + for (const move of Object.values(spec.moves ?? {})) { + const target = inside(root, move.target); + if (!existsSync(target)) continue; + const entry = lstatSync(target); + if (entry.isDirectory()) { + for (const path of filesUnder(root, move.target, options)) files.add(path); + } else if (entry.isFile()) files.add(move.target); + else throw new Error(`Unsupported file type: ${move.target}`); + } + } + return files; +} + +export function checkLocalBaseline(root, manifest) { + if (!record(manifest) || manifest.version !== 1 || !record(manifest.files) || + !record(manifest.omitted) || !record(manifest.sources)) { + throw new Error("Invalid skill manifest: expected version 1, sources, files, and omitted objects."); + } + const problems = []; + const upstreams = json(root, "profiles/upstreams.json"); + for (const [name, profile] of Object.entries(upstreams)) { + if (manifest.sources[name] !== profile.commit) problems.push(`${name}: source pin differs from profile`); + } + for (const name of Object.keys(manifest.sources)) { + if (!upstreams[name]) problems.push(`${name}: stale source pin`); + } + const actual = targetFiles(root, json(root, "profiles/skill-sources.json")); + for (const [path, entry] of Object.entries(manifest.files)) { + const target = inside(root, path); + if (!record(entry) || !hashPattern.test(entry.targetDigest ?? "")) { + problems.push(`${path}: invalid target digest`); + continue; + } + if (entry.upstream === null) { + if (typeof entry.reason !== "string" || !entry.reason.trim()) problems.push(`${path}: local addition has no reason`); + } else if (!manifest.sources[entry.upstream] || !hashPattern.test(entry.sourceDigest ?? "")) { + problems.push(`${path}: invalid upstream source baseline`); + } else { + inside(root, entry.source); + } + if (!existsSync(target)) problems.push(`${path}: target file missing`); + else if (contentDigest(root, path) !== entry.targetDigest) problems.push(`${path}: target content changed`); + actual.delete(path); + } + for (const path of actual) problems.push(`${path}: untracked skill file`); + for (const [key, entry] of Object.entries(manifest.omitted)) { + if (!record(entry) || !manifest.sources[entry.upstream] || !hashPattern.test(entry.sourceDigest ?? "") || + typeof entry.reason !== "string" || !entry.reason.trim()) problems.push(`${key}: invalid omission`); + else inside(root, entry.source); + } + return problems; +} + +function assertPinned(root, name, commit) { + const git = (args) => execFileSync("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); + if (git(["rev-parse", "HEAD"]) !== commit) throw new Error(`${name}: source checkout does not match pinned commit ${commit}`); + if (git(["status", "--porcelain=v1", "--untracked-files=all", "--", "."])) { + throw new Error(`${name}: source checkout has tracked or untracked changes`); + } +} + +export function buildBaseline(root, sourceRoots) { + const upstreams = json(root, "profiles/upstreams.json"); + const profiles = json(root, "profiles/skill-sources.json"); + const manifest = { version: 1, sources: {}, files: {}, omitted: {} }; + const owners = new Set(); + for (const [name, spec] of Object.entries(profiles)) { + const sourceRoot = sourceRoots[name]; + if (!sourceRoot) throw new Error(`Missing source checkout for ${name}`); + const commit = upstreams[name]?.commit; + if (typeof commit !== "string" || !/^[a-f0-9]{40}$/.test(commit)) throw new Error(`Invalid source pin for ${name}`); + assertPinned(sourceRoot, name, commit); + manifest.sources[name] = commit; + const skills = spec.skills ?? Object.fromEntries( + readdirSync(inside(sourceRoot, spec.root), { withFileTypes: true }).map((entry) => { + if (!entry.isDirectory()) throw new Error(`Unexpected source skill entry: ${entry.name}`); + return [`${spec.root}/${entry.name}`, upstreams[name].renames?.[entry.name] ?? entry.name]; + }), + ); + const usedMoves = new Set(); + const usedOmissions = new Set(); + for (const [sourceSkill, targetSkill] of Object.entries(skills)) { + if (owners.has(targetSkill)) throw new Error(`Multiple sources own skill ${targetSkill}`); + owners.add(targetSkill); + if (!existsSync(inside(sourceRoot, `${sourceSkill}/SKILL.md`))) throw new Error(`${sourceSkill}: missing source SKILL.md`); + for (const source of filesUnder(sourceRoot, sourceSkill)) { + const sourceDigest = contentDigest(sourceRoot, source); + const reason = spec.omit?.[source]; + if (reason !== undefined) { + if (typeof reason !== "string" || !reason.trim()) throw new Error(`${source}: omission requires a reason`); + manifest.omitted[`${name}:${source}`] = { upstream: name, source, sourceDigest, reason }; + usedOmissions.add(source); + continue; + } + let target = `skills/${targetSkill}${source.slice(sourceSkill.length)}`; + let moveReason; + const moves = Object.entries(spec.moves ?? {}).filter(([prefix]) => source === prefix || source.startsWith(`${prefix}/`)); + if (moves.length > 1) throw new Error(`${source}: overlapping move rules`); + if (moves.length) { + const [prefix, move] = moves[0]; + if (typeof move.reason !== "string" || !move.reason.trim()) throw new Error(`${source}: move requires a reason`); + inside(sourceRoot, prefix); + inside(root, move.target); + target = `${move.target}${source.slice(prefix.length)}`; + moveReason = move.reason; + usedMoves.add(prefix); + } + if (manifest.files[target]) throw new Error(`Multiple source files map to ${target}`); + if (!existsSync(inside(root, target))) throw new Error(`${source}: missing target ${target}; restore it or record an explicit move/omission`); + manifest.files[target] = { upstream: name, source, sourceDigest, targetDigest: contentDigest(root, target), ...(moveReason ? { reason: moveReason } : {}) }; + } + } + for (const path of Object.keys(spec.moves ?? {})) if (!usedMoves.has(path)) throw new Error(`${path}: stale move rule`); + for (const path of Object.keys(spec.omit ?? {})) if (!usedOmissions.has(path)) throw new Error(`${path}: stale omission rule`); + } + const expectedSkills = json(root, "profiles/skills.json").skills; + if (JSON.stringify([...owners].sort()) !== JSON.stringify([...expectedSkills].sort())) throw new Error("Skill sources do not cover the declared skill inventory"); + for (const path of targetFiles(root, profiles)) { + if (path.startsWith("skills/") && !owners.has(path.split("/")[1])) throw new Error(`${path}: no upstream skill owner`); + manifest.files[path] ??= { upstream: null, reason: "Local portability guidance or implementation; review alongside its owning skill.", targetDigest: contentDigest(root, path) }; + } + manifest.files = Object.fromEntries(Object.entries(manifest.files).sort(([a], [b]) => a.localeCompare(b, "en"))); + manifest.omitted = Object.fromEntries(Object.entries(manifest.omitted).sort(([a], [b]) => a.localeCompare(b, "en"))); + return manifest; +} + +export function baselineChanges(previous, next) { + const changes = []; + for (const group of ["sources", "files", "omitted"]) { + const oldEntries = previous?.[group] ?? {}; + for (const key of new Set([...Object.keys(oldEntries), ...Object.keys(next[group])])) { + if (JSON.stringify(oldEntries[key]) !== JSON.stringify(next[group][key])) { + changes.push(`${group}: ${key} (${!oldEntries[key] ? "added" : !next[group][key] ? "removed" : "changed"})`); + } + } + } + return changes; +} diff --git a/scripts/skill-baseline.mjs b/scripts/skill-baseline.mjs new file mode 100644 index 0000000..15be048 --- /dev/null +++ b/scripts/skill-baseline.mjs @@ -0,0 +1,82 @@ +import { spawnSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { baselineChanges, buildBaseline, checkLocalBaseline, inside } from "./skill-baseline-lib.mjs"; +import { acquireUpstreamSyncLock, assertUpstreamSyncTargetReadable, releaseUpstreamSyncLock } from "./sync-upstream-transaction.mjs"; + +const args = process.argv.slice(2); +const options = {}; +for (let index = 0; index < args.length; index++) { + const flag = args[index]; + if (["--check", "--diff", "--write", "--help"].includes(flag)) options[flag] = true; + else if (["--source", "--matt-source", "--target"].includes(flag)) { + const value = args[++index]; + if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value`); + options[flag] = value; + } else throw new Error(`Unknown argument: ${flag}`); +} +if (options["--help"]) { + console.log("Usage: node scripts/skill-baseline.mjs [--target ] [--source ] [--matt-source ] [--check | --diff | --write]\n\n" + + "--check validates every local skill file. With both sources it also verifies provenance and mappings.\n" + + "The default previews baseline changes; --diff prints upstream-to-adaptation patches for review.\n" + + "--write records the reviewed baseline. Preview, diff and write require both clean pinned source checkouts."); + process.exit(0); +} +if (["--check", "--diff", "--write"].filter((flag) => options[flag]).length > 1) throw new Error("Choose only one of --check, --diff, or --write"); +const root = resolve(options["--target"] ?? resolve(dirname(fileURLToPath(import.meta.url)), "..")); +const sourceRoots = { + pstack: options["--source"] ?? process.env.MSTACK_PSTACK_SOURCE, + "mattpocock/skills": options["--matt-source"] ?? process.env.MSTACK_MATT_SOURCE, +}; +if (!options["--check"] && !Object.values(sourceRoots).some(Boolean)) { + throw new Error("Pass --source and --matt-source to preview or refresh a baseline. Use --check for a local check or --help for usage."); +} +const lock = acquireUpstreamSyncLock(root, { recoverStale: false }); +try { + assertUpstreamSyncTargetReadable(root, lock); + const path = "profiles/skill-manifest.json"; + const previous = existsSync(inside(root, path)) ? JSON.parse(readFileSync(inside(root, path), "utf8")) : undefined; + if (options["--check"] && !previous) throw new Error(`Missing skill baseline: ${path}`); + let problems = previous ? checkLocalBaseline(root, previous) : []; + let next; + if (!options["--check"] || Object.values(sourceRoots).some(Boolean)) { + next = buildBaseline(root, sourceRoots); + const changes = baselineChanges(previous, next); + for (const change of changes) console.log(change); + if (options["--check"]) problems.push(...changes); + console.log(`${Object.keys(next.files).length} tracked files, ${Object.keys(next.omitted).length} explicit omissions, ${changes.length} baseline changes.`); + } + if (options["--diff"]) { + for (const [target, entry] of Object.entries(next.files)) { + if (entry.upstream === null) { + console.log(`\nLocal addition: ${target}\n${readFileSync(inside(root, target), "utf8")}`); + continue; + } + if (entry.sourceDigest === entry.targetDigest) continue; + console.log(`\nAdaptation: ${entry.upstream}:${entry.source} -> ${target}${entry.reason ? `\n${entry.reason}` : ""}`); + const result = spawnSync("git", ["diff", "--no-index", "--no-ext-diff", "--ignore-cr-at-eol", "--", inside(sourceRoots[entry.upstream], entry.source), inside(root, target)], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }); + if (result.error || ![0, 1].includes(result.status)) throw new Error(result.stderr || result.error?.message || "git diff failed"); + process.stdout.write(result.stdout); + } + for (const entry of Object.values(next.omitted)) console.log(`\nOmitted: ${entry.upstream}:${entry.source}\n${entry.reason}`); + } + if (options["--write"]) { + const temporary = inside(root, `profiles/.skill-manifest-${randomUUID()}.json`); + try { + writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { flag: "wx" }); + renameSync(temporary, inside(root, path)); + } finally { + if (existsSync(temporary)) unlinkSync(temporary); + } + console.log(`Wrote ${path}; review its Git diff together with the changed skills.`); + } else if (options["--check"]) { + if (problems.length) throw new Error(`Skill integrity check failed:\n${[...new Set(problems)].join("\n")}`); + console.log("All skill files match their reviewed baseline."); + } else { + console.log("No files changed. Inspect --diff before recording an intentional refresh with --write."); + } +} finally { + releaseUpstreamSyncLock(lock); +} diff --git a/scripts/skill-baseline.test.mjs b/scripts/skill-baseline.test.mjs new file mode 100644 index 0000000..7eb9980 --- /dev/null +++ b/scripts/skill-baseline.test.mjs @@ -0,0 +1,233 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; + +const cli = resolve(dirname(fileURLToPath(import.meta.url)), "skill-baseline.mjs"); + +function write(root, path, value) { + const file = join(root, path); + mkdirSync(dirname(file), { recursive: true }); + writeFileSync(file, value); +} + +function json(root, path, value) { + write(root, path, `${JSON.stringify(value, null, 2)}\n`); +} + +function commit(root) { + const git = (args) => execFileSync("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); + git(["init", "--quiet"]); + git(["add", "."]); + git(["-c", "user.name=Skill Test", "-c", "user.email=skill-test@example.invalid", "commit", "--quiet", "-m", "fixture"]); + return git(["rev-parse", "HEAD"]); +} + +function fixture(t) { + const root = mkdtempSync(join(tmpdir(), "mstack-skill-baseline-")); + t.after(() => rmSync(root, { recursive: true, force: true })); + const target = join(root, "target"); + const pstack = join(root, "pstack"); + const matt = join(root, "matt"); + write(pstack, "skills/poteto-mode/SKILL.md", "# Original workflow\n\nRead the full reference before acting.\n"); + write(pstack, "skills/poteto-mode/references/recipe.md", "# Recipe\n\nPrepare a fixture.\nRun the command.\nInspect the saved result.\n"); + write(matt, "skills/engineering/diagnosis/SKILL.md", "# Diagnosis\n\nReproduce the exact user symptom.\n"); + write(matt, "skills/engineering/diagnosis/DETAILS.md", "# Details\n\nA complete diagnostic recipe.\n"); + write(matt, "skills/engineering/diagnosis/agents/openai.yaml", "display_name: Diagnosis\n"); + const pins = { + pstack: { commit: commit(pstack), renames: { "poteto-mode": "meta-mode" } }, + "mattpocock/skills": { commit: commit(matt) }, + }; + json(target, "profiles/upstreams.json", pins); + json(target, "profiles/skills.json", { skills: ["diagnosis", "meta-mode"] }); + const mappings = { + pstack: { root: "skills" }, + "mattpocock/skills": { + skills: { "skills/engineering/diagnosis": "diagnosis" }, + moves: { "skills/engineering/diagnosis/DETAILS.md": { target: "skills/diagnosis/references/details.md", reason: "Reference organization" } }, + omit: { "skills/engineering/diagnosis/agents/openai.yaml": "Harness-specific display metadata" }, + }, + }; + json(target, "profiles/skill-sources.json", mappings); + write(target, "skills/meta-mode/SKILL.md", "# Portable workflow\n\nRead the full reference before acting.\n"); + write(target, "skills/meta-mode/references/recipe.md", "# Recipe\n\nPrepare a fixture.\nRun the command.\nInspect the saved result.\n"); + write(target, "skills/diagnosis/SKILL.md", "# Diagnosis\n\nReproduce the exact user symptom.\n"); + write(target, "skills/diagnosis/references/details.md", "# Details\n\nA complete diagnostic recipe.\n"); + write(target, "skills/meta-mode/references/harness.md", "# Local adapter\n\nDiscover the active Harness.\n"); + const run = (...args) => spawnSync(process.execPath, [cli, "--target", target, ...args], { + encoding: "utf8", + env: { ...process.env, MSTACK_PSTACK_SOURCE: "", MSTACK_MATT_SOURCE: "" }, + }); + const sources = ["--source", pstack, "--matt-source", matt]; + const record = () => { + const result = run(...sources, "--write"); + assert.equal(result.status, 0, result.stderr); + return readFileSync(join(target, "profiles/skill-manifest.json"), "utf8"); + }; + return { root, target, pstack, matt, pins, mappings, run, sources, record }; +} + +test("review previews adaptation patches and omissions without writing; explicit refresh is repeatable", (t) => { + const f = fixture(t); + const preview = f.run(...f.sources, "--diff"); + assert.equal(preview.status, 0, preview.stderr); + assert.match(preview.stdout, /-# Original workflow/); + assert.match(preview.stdout, /\+# Portable workflow/); + assert.match(preview.stdout, /Local addition: skills\/meta-mode\/references\/harness.md/); + assert.match(preview.stdout, /Omitted: mattpocock\/skills:skills\/engineering\/diagnosis\/agents\/openai.yaml/); + assert.equal(existsSync(join(f.target, "profiles/skill-manifest.json")), false); + const first = f.record(); + assert.equal(f.record(), first); + const checked = f.run(...f.sources, "--check"); + assert.equal(checked.status, 0, checked.stderr); + assert.match(checked.stdout, /5 tracked files, 1 explicit omissions, 0 baseline changes/); +}); + +test("local checks detect truncated references, deleted moved files, and untracked additions", (t) => { + const f = fixture(t); + f.record(); + write(f.target, "skills/meta-mode/references/recipe.md", "# Recipe\n"); + rmSync(join(f.target, "skills/diagnosis/references/details.md")); + write(f.target, "skills/diagnosis/new.md", "# Unreviewed instruction\n"); + const result = f.run("--check"); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /skills\/meta-mode\/references\/recipe.md: target content changed/); + assert.match(result.stderr, /skills\/diagnosis\/references\/details.md: target file missing/); + assert.match(result.stderr, /skills\/diagnosis\/new.md: untracked skill file/); +}); + +function movedFixture(t) { + const f = fixture(t); + write(f.pstack, "skills/poteto-mode/scripts/run.mjs", "console.log('upstream');\n"); + write(f.pstack, "skills/poteto-mode/logger.sh", "echo log\n"); + f.pins.pstack.commit = commit(f.pstack); + json(f.target, "profiles/upstreams.json", f.pins); + f.mappings.pstack.moves = { + "skills/poteto-mode/scripts": { target: "tools/meta-mode", reason: "Packaged runtime tools" }, + "skills/poteto-mode/logger.sh": { target: "tools/logger/log.mjs", reason: "Portable logger" }, + }; + json(f.target, "profiles/skill-sources.json", f.mappings); + write(f.target, "tools/meta-mode/run.mjs", "console.log('upstream');\n"); + write(f.target, "tools/logger/log.mjs", "console.log('log');\n"); + write(f.target, "tools/logger/unrelated.md", "Not owned by the single-file move.\n"); + f.record(); + return f; +} + +test("moved directories detect and record local additions without including a file move's siblings", (t) => { + const f = movedFixture(t); + write(f.target, "tools/meta-mode/nested/local.mjs", "console.log('review me');\n"); + const unchecked = f.run("--check"); + assert.notEqual(unchecked.status, 0); + assert.match(unchecked.stderr, /tools\/meta-mode\/nested\/local.mjs: untracked skill file/); + assert.doesNotMatch(unchecked.stderr, /unrelated.md/); + const preview = f.run(...f.sources, "--diff"); + assert.equal(preview.status, 0, preview.stderr); + assert.match(preview.stdout, /Local addition: tools\/meta-mode\/nested\/local.mjs/); + assert.match(preview.stdout, /console.log\('review me'\)/); + const recorded = JSON.parse(f.record()); + assert.equal(recorded.files["tools/meta-mode/nested/local.mjs"].upstream, null); + const checked = f.run(...f.sources, "--check"); + assert.equal(checked.status, 0, checked.stderr); + assert.match(checked.stdout, /8 tracked files, 1 explicit omissions, 0 baseline changes/); + write(f.target, "tools/meta-mode/nested/local.mjs", "console.log('changed');\n"); + const changed = f.run("--check"); + assert.notEqual(changed.status, 0); + assert.match(changed.stderr, /tools\/meta-mode\/nested\/local.mjs: target content changed/); +}); + +test("moved target inventories exclude installed dependencies but reject linked or escaping paths", (t) => { + const f = movedFixture(t); + const before = f.record(); + write(f.target, "tools/meta-mode/node_modules/pkg/dependency.mjs", "console.log('installed dependency');\n"); + const checked = f.run("--check"); + assert.equal(checked.status, 0, checked.stderr); + assert.match(checked.stdout, /All skill files match/); + assert.equal(f.record(), before); + + const outside = join(f.root, "outside"); + write(outside, "private.md", "Do not read this outside the target root.\n"); + const link = join(f.target, "tools/meta-mode/linked"); + symlinkSync(outside, link, process.platform === "win32" ? "junction" : "dir"); + const linked = f.run("--check"); + assert.notEqual(linked.status, 0); + assert.match(linked.stderr, /Unsupported file type: tools\/meta-mode\/linked/); + assert.doesNotMatch(linked.stderr, /Do not read this/); + rmSync(link); + + f.mappings.pstack.moves["skills/poteto-mode/scripts"].target = "../outside"; + json(f.target, "profiles/skill-sources.json", f.mappings); + for (const args of [["--check"], [...f.sources, "--write"]]) { + const escaped = f.run(...args); + assert.notEqual(escaped.status, 0); + assert.match(escaped.stderr, /Invalid checkout-relative path: \.\.\/outside/); + } + assert.equal(readFileSync(join(f.target, "profiles/skill-manifest.json"), "utf8"), before); +}); + +test("refresh refuses to bless a deleted source-mapped file and preserves the previous manifest", (t) => { + const f = fixture(t); + const before = f.record(); + rmSync(join(f.target, "skills/meta-mode/references/recipe.md")); + const result = f.run(...f.sources, "--write"); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /missing target skills\/meta-mode\/references\/recipe.md/); + assert.equal(readFileSync(join(f.target, "profiles/skill-manifest.json"), "utf8"), before); +}); + +test("both sources must be present, clean and pinned before refreshing", (t) => { + const f = fixture(t); + const before = f.record(); + const missing = f.run("--source", f.pstack, "--write"); + assert.notEqual(missing.status, 0); + assert.match(missing.stderr, /Missing source checkout for mattpocock\/skills/); + write(f.matt, "skills/engineering/diagnosis/DETAILS.md", "# Changed source\n"); + const dirty = f.run(...f.sources, "--write"); + assert.notEqual(dirty.status, 0); + assert.match(dirty.stderr, /mattpocock\/skills: source checkout has tracked or untracked changes/); + commit(f.matt); + const wrongPin = f.run(...f.sources, "--write"); + assert.notEqual(wrongPin.status, 0); + assert.match(wrongPin.stderr, /source checkout does not match pinned commit/); + assert.equal(readFileSync(join(f.target, "profiles/skill-manifest.json"), "utf8"), before); +}); + +test("an upstream addition needs a restored target or an explicit reviewed omission", (t) => { + const f = fixture(t); + f.record(); + const added = "skills/engineering/diagnosis/NEW.md"; + write(f.matt, added, "# New upstream workflow\n"); + f.pins["mattpocock/skills"].commit = commit(f.matt); + json(f.target, "profiles/upstreams.json", f.pins); + const result = f.run(...f.sources, "--write"); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /missing target skills\/diagnosis\/NEW.md/); + f.mappings["mattpocock/skills"].omit[added] = "Replaced by the existing local diagnosis workflow"; + json(f.target, "profiles/skill-sources.json", f.mappings); + f.record(); + const checked = f.run(...f.sources, "--check"); + assert.equal(checked.status, 0, checked.stderr); + assert.match(checked.stdout, /2 explicit omissions/); +}); + +test("checks require a valid manifest and reject path escapes before reading outside the checkout", (t) => { + const f = fixture(t); + const missing = f.run("--check"); + assert.notEqual(missing.status, 0); + assert.match(missing.stderr, /Missing skill baseline/); + const manifest = JSON.parse(f.record()); + manifest.files["../outside.md"] = manifest.files["skills/diagnosis/SKILL.md"]; + json(f.target, "profiles/skill-manifest.json", manifest); + const result = f.run("--check"); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Invalid checkout-relative path: \.\.\/outside.md/); + const badArgument = f.run("--source", "--write"); + assert.notEqual(badArgument.status, 0); + assert.match(badArgument.stderr, /--source requires a value/); + const noOptions = f.run(); + assert.notEqual(noOptions.status, 0); + assert.match(noOptions.stderr, /Use --check for a local check or --help for usage/); +}); diff --git a/skills/codebase-design/SKILL.md b/skills/codebase-design/SKILL.md index 73674ca..fb8a982 100644 --- a/skills/codebase-design/SKILL.md +++ b/skills/codebase-design/SKILL.md @@ -6,44 +6,110 @@ license: MIT # Codebase design -Design deep modules that hide substantial behavior behind a small interface. -The result should reduce what callers must know and concentrate related change -in one place. - -Use the repository's existing domain and architecture terms. The concepts below -are analytical tools, not a replacement vocabulary: - -- **Module:** code with an interface and implementation, at any useful scale. -- **Interface:** everything a caller must know, including types, invariants, - ordering, errors, configuration, and relevant performance characteristics. -- **Seam:** a place where behavior can vary without editing the caller. -- **Adapter:** an implementation selected at a seam. -- **Depth:** useful behavior provided per unit of interface callers must learn. -- **Locality:** related knowledge, change, and verification remain together. - -## Review the shape - -1. Identify callers and list what each must know or coordinate. -2. Find rules, sequencing, error translation, or lifecycle state repeated across - callers. -3. Propose the smallest interface that can own those responsibilities. -4. Place seams only where behavior actually varies or a real external - dependency requires substitution. -5. Test through the same interface callers use. - -Apply the deletion test. If deleting the abstraction removes complexity, it was -probably pass-through code. If its responsibilities spread back across many -callers, the module was earning its place. - -One production adapter alone rarely justifies a new seam. A production adapter -plus a meaningful test implementation, or two real production variants, does. -Do not expose private seams merely to make implementation details mockable. - -Accept dependencies instead of constructing remote or stateful dependencies -inside domain logic. Prefer returned results over hidden mutation when that -makes behavior easier to compose and verify. - -When deepening several shallow modules, read -[references/deepening.md](references/deepening.md). When materially different -interfaces could change the architecture, read -[references/design-alternatives.md](references/design-alternatives.md). +Design **deep modules**: a lot of behaviour behind a small interface, placed at a clean seam, testable through that interface. Use this language and these principles wherever code is being designed or restructured. The aim is leverage for callers, locality for maintainers, and testability for everyone. + +## Glossary + +Use these terms exactly: don't substitute "component," "service," "API," or "boundary." Consistent language is the whole point. + +**Module**: anything with an interface and an implementation. Deliberately scale-agnostic: a function, class, package, or tier-spanning slice. _Avoid_: unit, component, service. + +**Interface**: everything a caller must know to use the module correctly: the type signature, but also invariants, ordering constraints, error modes, required configuration, and performance characteristics. _Avoid_: API, signature (too narrow, they refer only to the type-level surface). + +**Implementation**: what's inside a module, its body of code. Distinct from **Adapter**: a thing can be a small adapter with a large implementation (a Postgres repo) or a large adapter with a small implementation (an in-memory fake). Reach for "adapter" when the seam is the topic; "implementation" otherwise. + +**Depth**: leverage at the interface. The amount of behaviour a caller (or test) can exercise per unit of interface they have to learn. A module is **deep** when a large amount of behaviour sits behind a small interface, **shallow** when the interface is nearly as complex as the implementation. + +**Seam** _(Michael Feathers)_: a place where you can alter behaviour without editing in that place; the *location* at which a module's interface lives. Where to put the seam is its own design decision, distinct from what goes behind it. _Avoid_: boundary (overloaded with DDD's bounded context). + +**Adapter**: a concrete thing that satisfies an interface at a seam. Describes *role* (what slot it fills), not substance (what's inside). + +**Leverage**: what callers get from depth. More capability per unit of interface they learn. One implementation pays back across N call sites and M tests. + +**Locality**: what maintainers get from depth. Change, bugs, knowledge, and verification concentrate in one place rather than spreading across callers. Fix once, fixed everywhere. + +## Deep vs shallow + +**Deep module** = small interface + lots of implementation: + +``` +┌─────────────────────┐ +│ Small Interface │ ← Few methods, simple params +├─────────────────────┤ +│ │ +│ Deep Implementation│ ← Complex logic hidden +│ │ +└─────────────────────┘ +``` + +**Shallow module** = large interface + little implementation (avoid): + +``` +┌─────────────────────────────────┐ +│ Large Interface │ ← Many methods, complex params +├─────────────────────────────────┤ +│ Thin Implementation │ ← Just passes through +└─────────────────────────────────┘ +``` + +When designing an interface, ask: + +- Can I reduce the number of methods? +- Can I simplify the parameters? +- Can I hide more complexity inside? + +## Principles + +- **Depth is a property of the interface, not the implementation.** A deep module can be internally composed of small, mockable, swappable parts; they just aren't part of the interface. A module can have **internal seams** (private to its implementation, used by its own tests) as well as the **external seam** at its interface. +- **The deletion test.** Imagine deleting the module. If complexity vanishes, it was a pass-through. If complexity reappears across N callers, it was earning its keep. +- **The interface is the test surface.** Callers and tests cross the same seam. If you want to test *past* the interface, the module is probably the wrong shape. +- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a seam unless something actually varies across it. + +## Designing for testability + +Good interfaces make testing natural: + +1. **Accept dependencies, don't create them.** + + ```typescript + // Testable + function processOrder(order, paymentGateway) {} + + // Hard to test + function processOrder(order) { + const gateway = new StripeGateway(); + } + ``` + +2. **Return results, don't produce side effects.** + + ```typescript + // Testable + function calculateDiscount(cart): Discount {} + + // Hard to test + function applyDiscount(cart): void { + cart.total -= discount; + } + ``` + +3. **Small surface area.** Fewer methods = fewer tests needed. Fewer params = simpler test setup. + +## Relationships + +- A **Module** has exactly one **Interface** (the surface it presents to callers and tests). +- **Depth** is a property of a **Module**, measured against its **Interface**. +- A **Seam** is where a **Module**'s **Interface** lives. +- An **Adapter** sits at a **Seam** and satisfies the **Interface**. +- **Depth** produces **Leverage** for callers and **Locality** for maintainers. + +## Rejected framings + +- **Depth as ratio of implementation-lines to interface-lines** (Ousterhout): rewards padding the implementation. We use depth-as-leverage instead. +- **"Interface" as the TypeScript `interface` keyword or a class's public methods**: too narrow: interface here includes every fact a caller must know. +- **"Boundary"**: overloaded with DDD's bounded context. Say **seam** or **interface**. + +## Going deeper + +- **Deepening a cluster given its dependencies**, see [Deepening](references/deepening.md): dependency categories, seam discipline, and replace-don't-layer testing. +- **Exploring alternative interfaces**, see [Design alternatives](references/design-alternatives.md): spin up parallel sub-agents to design the interface several radically different ways, then compare on depth, locality, and seam placement. diff --git a/skills/codebase-design/references/deepening.md b/skills/codebase-design/references/deepening.md index ec45e10..ed0c0e4 100644 --- a/skills/codebase-design/references/deepening.md +++ b/skills/codebase-design/references/deepening.md @@ -1,36 +1,37 @@ -# Deepening a module +# Deepening -Classify dependencies before moving responsibilities behind a new interface. +How to deepen a cluster of shallow modules safely, given its dependencies. Assumes the vocabulary in [SKILL.md](../SKILL.md): **module**, **interface**, **seam**, **adapter**. -## In-process dependencies +## Dependency categories -Pure computation and in-memory state can normally move inside the module. Test -the resulting behavior through its public interface. +When assessing a candidate for deepening, classify its dependencies. The category determines how the deepened module is tested across its seam. -## Local substitutes +### 1. In-process -Use a realistic local implementation such as an in-memory filesystem or local -database when it preserves the behavior under test. Keep that seam internal -unless callers genuinely choose implementations. +Pure computation, in-memory state, no I/O. Always deepenable: merge the modules and test through the new interface directly. No adapter needed. -## Owned remote systems +### 2. Local-substitutable -Place a port at the network seam. Keep domain behavior in the owning module and -inject the transport adapter. Tests may use an in-memory adapter when it models -the contract faithfully. +Dependencies that have local test stand-ins (PGLite for Postgres, in-memory filesystem). Deepenable if the stand-in exists. The deepened module is tested with the stand-in running in the test suite. The seam is internal; no port at the module's external interface. -## External services +### 3. Remote but owned (Ports & Adapters) -Wrap only the external contract the application uses. Inject that port and test -application behavior with a focused fake. Keep vendor payloads and error -translation inside the production adapter. +Your own services across a network boundary (microservices, internal APIs). Define a **port** (interface) at the seam. The deep module owns the logic; the transport is injected as an **adapter**. Tests use an in-memory adapter. Production uses an HTTP/gRPC/queue adapter. -## Migration and tests +Recommendation shape: *"Define a port at the seam, implement an HTTP adapter for production and an in-memory adapter for testing, so the logic sits in one deep module even though it's deployed across a network."* -Move one coherent responsibility at a time. Add behavior tests at the new -interface, migrate callers, then remove superseded shallow modules and tests. -Do not retain old layers merely to avoid deleting obsolete tests. +### 4. True external (Mock) -Tests should assert observable behavior and survive internal refactoring. If a -test must reach through the interface, reconsider the module shape before -adding another testing-only seam. +Third-party services (Stripe, Twilio, etc.) you don't control. The deepened module takes the external dependency as an injected port; tests provide a mock adapter. + +## Seam discipline + +- **One adapter means a hypothetical seam. Two adapters means a real one.** Don't introduce a port unless at least two adapters are justified (typically production + test). A single-adapter seam is just indirection. +- **Internal seams vs external seams.** A deep module can have internal seams (private to its implementation, used by its own tests) as well as the external seam at its interface. Don't expose internal seams through the interface just because tests use them. + +## Testing strategy: replace, don't layer + +- Old unit tests on shallow modules become waste once tests at the deepened module's interface exist; delete them. +- Write new tests at the deepened module's interface. The **interface is the test surface**. +- Tests assert on observable outcomes through the interface, not internal state. +- Tests should survive internal refactors, since they describe behaviour, not implementation. If a test has to change when the implementation changes, it's testing past the interface. diff --git a/skills/codebase-design/references/design-alternatives.md b/skills/codebase-design/references/design-alternatives.md index 8680915..0dcdd6e 100644 --- a/skills/codebase-design/references/design-alternatives.md +++ b/skills/codebase-design/references/design-alternatives.md @@ -1,24 +1,44 @@ -# Comparing interface designs - -Explore alternatives only when interface shape has lasting architectural cost. -For a small local refactor, choose the simplest adequate design directly. - -Frame the fixed constraints first: callers, invariants, dependency categories, -failure modes, and performance requirements. Then produce genuinely different -designs rather than cosmetic naming variants. Useful perspectives include: - -- The smallest interface that maximizes depth. -- An interface optimized for the most common caller. -- A more extensible interface when concrete future variants already exist. -- A ports-and-adapters design when an owned or external remote dependency sets - the seam. - -For each design, show the interface, one realistic call site, hidden -responsibilities, dependency strategy, and known tradeoffs. Compare depth, -locality, invalid states, error handling, and migration cost. Recommend one -design or a specific hybrid. - -Use parallel agents only when independent designs add useful breadth and the -task warrants multi-agent work. Give each agent file paths, constraints, and a -different design objective. Keep file ownership separate if they produce -artifacts. +# Design it twice + +When the user wants to explore alternative interfaces for a chosen deepening candidate, use this parallel sub-agent pattern. Based on "Design It Twice" (Ousterhout): your first idea is unlikely to be the best. + +Uses the vocabulary in [SKILL.md](../SKILL.md): **module**, **interface**, **seam**, **adapter**, **leverage**. + +## Process + +### 1. Frame the problem space + +Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate: + +- The constraints any new interface would need to satisfy +- The dependencies it would rely on, and which category they fall into (see [Deepening](deepening.md)) +- A rough illustrative code sketch to ground the constraints, not a proposal, just a way to make the constraints concrete + +Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel. + +### 2. Spawn sub-agents + +Use the active Harness's native delegation to assign three independent candidates, within its available concurrency. Each must produce a **radically different** interface for the deepened module. If delegation is unavailable, run separate design passes with these same briefs and disclose that they share one agent's context. Keep the passes read-only; design alternatives do not need to edit a shared checkout. + +Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from [Deepening](deepening.md), what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint: + +- Agent 1: "Minimize the interface: aim for 1–3 entry points max. Maximise leverage per entry point." +- Agent 2: "Maximise flexibility: support many use cases and extension." +- Agent 3: "Optimise for the most common caller: make the default case trivial." +- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies." + +Include both [SKILL.md](../SKILL.md) vocabulary and the repository's domain vocabulary from CONTEXT.md when it exists in the brief so each sub-agent names things consistently with the architecture language and the project's domain language. + +Each sub-agent outputs: + +1. Interface (types, methods, params, plus invariants, ordering, error modes) +2. Usage example showing how callers use it +3. What the implementation hides behind the seam +4. Dependency strategy and adapters (see [Deepening](deepening.md)) +5. Trade-offs: where leverage is high, where it's thin + +### 3. Present and compare + +Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**. + +After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated: the user wants a strong read, not a menu. diff --git a/skills/create-verification-skill/references/feature-map-example/README.md b/skills/create-verification-skill/references/feature-map-example/README.md index 35ed8f9..4865069 100644 --- a/skills/create-verification-skill/references/feature-map-example/README.md +++ b/skills/create-verification-skill/references/feature-map-example/README.md @@ -1,15 +1,16 @@ -# Feature verification map +# Notes verification map -Treat this directory as the maintained source for user-facing verification. -Start with baseline preconditions and driving conventions, then index each -feature file. +This worked example describes a fictional Notes app and its `control-notes` +driver. The commands show the precision a real feature map needs; they are +not tools shipped by mstack. Replace the app, driver, paths, and seeded data +when writing your project's map. ## Baseline preconditions -- Name the exact application URL, executable, or endpoint. -- Use disposable ports, profiles, and data directories when possible. -- Seed the smallest state required by the recipes. -- Run the doctor check before driving. +- Launch Notes at `http://127.0.0.1:4173` with a disposable data directory. +- Seed notes titled `Quarterly plan` with body `Draft budget`, and `Grocery list`. +- Put `control-notes` and the `notes` CLI on `PATH`. +- Run `control-notes doctor` and check the URL, data directory, and build revision. - Never drive an instance not owned by this verification run. ## Proof and skip reporting @@ -32,3 +33,10 @@ exactly these four H2 sections: Keep implementation details out. Record user paths, stable handles, required state, literal commands, and observable proof. + +## Features + +- [Create a note](create-note.md) covers browser and CLI creation, cancellation, + persistence, and cleanup. +- [Search notes](search.md) covers toolbar, keyboard, and CLI search with matching, + empty, and clear states. diff --git a/skills/create-verification-skill/references/feature-map-example/create-note.md b/skills/create-verification-skill/references/feature-map-example/create-note.md new file mode 100644 index 0000000..2135756 --- /dev/null +++ b/skills/create-verification-skill/references/feature-map-example/create-note.md @@ -0,0 +1,39 @@ +# Create a note + +Create note lets a user save a titled note from the browser or CLI, cancel an unfinished draft, and confirm the saved note from a second user-facing view. + +## Sub-features + +- `create-open` opens a blank editor from each browser entry point. +- `create-save` persists a title and body. +- `create-cancel` discards an unfinished browser draft. +- `create-cli` creates the same note shape from the terminal. + +## How to get to it (user POV) + +- Choose the `New note` button in the browser toolbar. +- Press `n` in the browser while focus is outside an editable field. +- Run `notes create --title --body <body>` in a terminal. + +## Driving it with control-notes + +Preconditions: + +- Notes is healthy at `http://127.0.0.1:4173`. +- No note is titled `Release checklist`. +- `control-notes doctor` reports the expected URL and disposable data directory. + +- **Open editor.** Choose `New note`. Run `control-notes browser click --role button --name "New note"`. A form named `Note editor` appears with focus in the `Title` textbox. +- **Enter content.** Type the title and body. Run `control-notes browser fill --role textbox --name "Title" --value "Release checklist"` and `control-notes browser fill --role textbox --name "Body" --value "Tag and publish"`. The `Save note` button becomes enabled. +- **Save note.** Choose `Save note`. Run `control-notes browser click --role button --name "Save note"`. A status named `Note saved` appears and the heading reads `Release checklist`. +- **Confirm persistence.** Return to the note list and reopen the note. Run `control-notes browser click --role link --name "All notes"` and `control-notes browser click --role link --name "Release checklist"`. The editor shows both saved values. +- **Cancel draft.** Open a new note, enter `Discard me`, and choose `Cancel`. Run `control-notes browser click --role button --name "New note"`, `control-notes browser fill --role textbox --name "Title" --value "Discard me"`, and `control-notes browser click --role button --name "Cancel"`. The note list returns and has no `Discard me` link. +- **CLI entry.** Create a second note. Run `control-notes cli -- notes create --title "CLI note" --body "Created from terminal" --format json`. Exit code `0` and stdout contain the new note ID and title. +- **Proof.** Reopen both saved notes from `All notes`. Run `control-notes browser snapshot --aria --path artifacts/create-note/list.aria.txt` and `control-notes browser screenshot --path artifacts/create-note/list.png`. The artifacts show `Release checklist` and `CLI note`. + +## Gotchas + +- Pressing `n` while a textbox has focus types the character instead of opening a new editor. +- Titles are trimmed on save. Assert the rendered title, not the draft input value. +- A save status alone is insufficient proof. Reopen the note from the list. +- Remove `Release checklist` and `CLI note` during fixture cleanup, but retain their proof artifacts. diff --git a/skills/create-verification-skill/references/feature-map-example/search.md b/skills/create-verification-skill/references/feature-map-example/search.md new file mode 100644 index 0000000..1f8e57d --- /dev/null +++ b/skills/create-verification-skill/references/feature-map-example/search.md @@ -0,0 +1,45 @@ +# Search notes + +Search lets a user find notes by title or body text, inspect a matching note, and distinguish no matches from an unavailable search. + +## Sub-features + +- `search-open` opens search from each supported browser entry point. +- `search-match` returns title and body matches without changing note data. +- `search-open-result` opens a result in the note editor. +- `search-empty` shows a complete empty state for a query with no matches. +- `search-clear` removes the query and restores the recent-notes view. +- `search-cli` returns the same matching notes from the terminal. + +## How to get to it (user POV) + +- Choose the `Search` button in the browser toolbar. +- Press `/` in the browser while focus is outside an editable field. +- Run `notes search <query>` in a terminal. + +## Driving it with control-notes + +Preconditions: + +- Notes is healthy at `http://127.0.0.1:4173`. +- The disposable data directory contains `Quarterly plan` with body text `Draft budget`. +- `control-notes doctor` reports the expected URL and data directory. + +- **Toolbar entry.** Choose the `Search` button. Run `control-notes browser click --role button --name "Search"`. A dialog named `Search notes` appears with focus in its searchbox. +- **Keyboard entry.** Close the dialog, focus the page, and press `/`. Run `control-notes browser press --key "/"`. The same dialog appears and the page does not insert a slash. +- **Title match.** Type `quarterly`. Run `control-notes browser fill --role searchbox --name "Search notes" --value "quarterly"`. The `Search results` list contains `Quarterly plan` and does not contain `Grocery list`. +- **Body match.** Replace the query with `budget`. Run `control-notes browser fill --role searchbox --name "Search notes" --value "budget"`. The result `Quarterly plan` remains visible with a body-match excerpt. +- **Open result.** Choose `Quarterly plan`. Run `control-notes browser click --role link --name "Quarterly plan"`. The dialog closes and the editor heading reads `Quarterly plan`. +- **Empty state.** Reopen search and enter `volcano`. Run `control-notes browser fill --role searchbox --name "Search notes" --value "volcano"`. A status named `No matching notes` appears after search completes. +- **Clear query.** Choose `Clear search`. Run `control-notes browser click --role button --name "Clear search"`. The searchbox is empty and the `Recent notes` region replaces the result list. +- **CLI match.** Search from the terminal. Run `control-notes cli -- notes search "quarterly" --format json`. Exit code `0` and stdout contain one object whose title is `Quarterly plan`. +- **CLI miss.** Search for an absent value. Run `control-notes cli -- notes search "volcano" --format json`. Exit code `0` and stdout are `[]`. +- **Proof.** Capture the populated result state. Run `control-notes browser snapshot --aria --path artifacts/search/results.aria.txt` and `control-notes browser screenshot --path artifacts/search/results.png`. Both artifacts identify Notes, the query, and `Quarterly plan`. + +## Gotchas + +- Pressing `/` while the editor or searchbox has focus inserts text instead of opening search. +- Results update after a short debounce. Wait for the results list or empty status, not a fixed sleep. +- Archived notes are excluded unless the user enables `Include archived`. +- The CLI defaults to human-readable output. Use `--format json` for stable assertions. +- Opening a result changes browser state. Reopen search before proving another query. diff --git a/skills/diagnosing-bugs/SKILL.md b/skills/diagnosing-bugs/SKILL.md index 8ac9350..58fe371 100644 --- a/skills/diagnosing-bugs/SKILL.md +++ b/skills/diagnosing-bugs/SKILL.md @@ -4,65 +4,136 @@ description: Diagnose hard, flaky, intermittent, environment-specific, or perfor license: MIT --- -# Diagnosing hard bugs +# Diagnosing bugs -Build a trustworthy feedback loop before committing to a theory. Match the -amount of investigation to the bug's cost and uncertainty. +A discipline for hard bugs. Skip phases only when explicitly justified. For a diagnosis-only request, report the verified cause and evidence. Enter the fix phases when the user has authorized implementation. -## Protect evidence +When exploring the codebase, read `CONTEXT.md` (if it exists) to get a clear mental model of the relevant modules, and check ADRs in the area you're touching. -Redact credentials, tokens, private user data, auth headers, and sensitive -infrastructure details from commands, logs, traces, screenshots, and reports. -Keep credentials in environment variables. If redaction removes the signal, -ask for a safer artifact or access path. +## Redact -## Establish the signal +This skill has you show commands, outputs and captured artifacts. **Redact every secret first**: write `<REDACTED>` in its place. Build loops against env vars, so the credential stays in the environment rather than in what you show. Captured artifacts carry auth headers: quote only the lines that carry the signal. -Create one command or repeatable interaction that reaches the reported code -path and detects the exact symptom. Run it before relying on it. Tighten it by -making setup smaller, the assertion sharper, and sources of time, randomness, -filesystem state, and network behavior controlled where practical. +If the redacted output is not enough to diagnose the bug, say so and ask the user. -For intermittent bugs, optimize for a high measured reproduction rate rather -than a perfect single run. Record attempts, failures, and conditions. See -[references/feedback-loops.md](references/feedback-loops.md) for loop choices. +## Phase 1: Build a feedback loop -If no trustworthy loop is possible, report what was attempted and request the -missing environment, a redacted capture, or authorization for temporary -instrumentation. Do not disguise speculation as a diagnosis. +**This is the skill.** Everything else is mechanical. If you have a **tight** pass/fail signal for the bug (one that goes red on _this_ bug), you will find the cause; bisection, hypothesis-testing, and instrumentation all just consume it. If you don't have one, no amount of staring at code will save you. -## Minimize and hypothesize +Spend disproportionate effort here. **Be aggressive. Be creative. Refuse to give up.** -Reduce inputs, callers, configuration, data, and steps one variable at a time. -Keep a reduction only when the failure remains. Aim for a case where every -remaining element affects reproduction. +### Ways to construct one, in roughly this order -Rank several plausible causes when uncertainty is real. Each hypothesis must -predict an observation that would support or falsify it. Share the ranked list -when domain knowledge could reorder it, but continue with the strongest safe -probe if the user is unavailable. +1. **Failing test** at whatever seam reaches the bug: unit, integration, e2e. +2. **Curl / HTTP script** against a running dev server. +3. **CLI invocation** with a fixture input, diffing stdout against a known-good snapshot. +4. **Headless browser script** (Playwright / Puppeteer) that drives the UI and asserts on DOM/console/network. +5. **Replay a captured trace.** Save a real network request / payload / event log to disk; replay it through the code path in isolation. +6. **Throwaway harness.** Spin up a minimal subset of the system (one service, mocked deps) that exercises the bug code path with a single function call. +7. **Property / fuzz loop.** If the bug is "sometimes wrong output", run 1000 random inputs and look for the failure mode. +8. **Bisection harness.** If the bug appeared between two known states (commit, dataset, version), automate "boot at state X, check, repeat" so you can `git bisect run` it. +9. **Differential loop.** Run the same input through old-version vs new-version (or two configs) and diff outputs. +10. **Human interaction loop.** Last resort. If a human must click, use the optional [Bash template](scripts/hitl-loop.template.sh) in an interactive terminal, or the active Harness's user-input tool. Follow [the interaction and capture instructions](references/feedback-loops.md). Captured observations feed back to the agent. -## Instrument deliberately +Build the right feedback loop, and the bug is 90% fixed. -Map each probe to one prediction and change one variable at a time. Prefer a -debugger or focused state inspection, then targeted logs at transitions that -distinguish hypotheses. Tag temporary instrumentation with a unique searchable -prefix. Avoid broad logging that creates noise or captures secrets. +### Tighten the loop -For performance regressions, establish a baseline with the relevant profiler, -query plan, trace, allocation data, or timing harness before changing code. See -[references/performance.md](references/performance.md). +Treat the loop as a product. Once you have _a_ loop, **tighten** it: -## Fix and prove +- Can I make it faster? (Cache setup, skip unrelated init, narrow the test scope.) +- Can I make the signal sharper? (Assert on the specific symptom, not "didn't crash".) +- Can I make it more deterministic? (Pin time, seed RNG, isolate filesystem, freeze network.) -Fix the earliest incorrect assumption, state transition, or ownership boundary -supported by evidence. Turn the minimized reproduction into a regression test -when a stable interface can express the real failure. A brittle, mock-dominated -test at the wrong level is not proof. +A 30-second flaky loop is barely better than no loop; a 2-second deterministic one is tight, a debugging superpower. -Run the focused regression and the original scenario after the fix. Then run -nearby checks in proportion to the change's risk. Remove tagged instrumentation -and temporary artifacts unless the user asked to retain a reusable diagnostic. +### Non-deterministic bugs -Report the cause, evidence that separated it from competing hypotheses, the -change, before-and-after results, and any remaining uncertainty. +The goal is not a clean repro but a **higher reproduction rate**. Loop the trigger 100×, parallelise, add stress, narrow timing windows, inject sleeps. A 50%-flake bug is debuggable; 1% is not, so keep raising the rate until it's debuggable. + +### When you genuinely cannot build a loop + +Stop and say so explicitly. List what you tried. Ask the user for: (a) access to whatever environment reproduces it, (b) a redacted captured artifact (HAR file, log dump, core dump, screen recording with timestamps), or (c) permission to add temporary production instrumentation. Do **not** proceed to hypothesise without a loop. + +### Completion criterion: a tight loop that goes red + +Phase 1 is done when the loop is **tight** and **red-capable**: you can name **one command** (a script path, a test invocation, a curl) that you have **already run at least once** (show the invocation and its output, redacted), and that is: + +- [ ] **Red-capable**: it drives the actual bug code path and asserts the **user's exact symptom**, so it can go red on this bug and green once fixed. Not "runs without erroring"; it must be able to _catch this specific bug_. +- [ ] **Deterministic**: same verdict every run (flaky bugs: a pinned, high reproduction rate, per above). +- [ ] **Fast**: seconds, not minutes. +- [ ] **Agent-runnable**: you can run it unattended; human interaction uses the template or Harness user-input tool with recorded observations. + +If you catch yourself reading code to build a theory before this command exists, **stop: jumping straight to a hypothesis is the exact failure this skill prevents.** No red-capable command, no Phase 2. + +## Phase 2: Reproduce + minimise + +Run the loop. Watch it go red as the bug appears. + +Confirm: + +- [ ] The loop produces the failure mode the **user** described, not a different failure that happens to be nearby. Wrong bug = wrong fix. +- [ ] The failure is reproducible across multiple runs (or, for non-deterministic bugs, reproducible at a high enough rate to debug against). +- [ ] You have captured the exact symptom (error message, wrong output, slow timing) so later phases can verify the fix actually addresses it. + +### Minimise + +Once it's red, shrink the repro to the **smallest scenario that still goes red**. Cut inputs, callers, config, data, and steps **one at a time**, re-running the loop after each cut, and keep only what's load-bearing for the failure. + +Why bother: a minimal repro shrinks the hypothesis space in Phase 3 (fewer moving parts left to suspect) and becomes the clean regression test in Phase 5. + +Done when **every remaining element is load-bearing**: removing any one of them makes the loop go green. + +Do not proceed until you have reproduced **and** minimised. + +## Phase 3: Hypothesise + +Generate **3–5 ranked hypotheses** before testing any of them. Single-hypothesis generation anchors on the first plausible idea. + +Each hypothesis must be **falsifiable**: state the prediction it makes. + +> Format: "If <X> is the cause, then <changing Y> will make the bug disappear / <changing Z> will make it worse." + +If you cannot state the prediction, the hypothesis is a vibe: discard or sharpen it. + +**Show the ranked list to the user before testing.** They often have domain knowledge that re-ranks instantly ("we just deployed a change to #3"), or know hypotheses they've already ruled out. Cheap checkpoint, big time saver. Don't block on it; proceed with your ranking if the user is AFK. + +## Phase 4: Instrument + +Each probe must map to a specific prediction from Phase 3. **Change one variable at a time.** + +Tool preference: + +1. **Debugger / REPL inspection** if the env supports it. One breakpoint beats ten logs. +2. **Targeted logs** at the boundaries that distinguish hypotheses. +3. Never "log everything and grep". + +**Tag every debug log** with a unique prefix, e.g. `[DEBUG-a4f2]`. Cleanup at the end becomes a single grep. Untagged logs survive; tagged logs die. + +**Perf branch.** For performance regressions, logs are usually wrong. Instead: establish a baseline measurement (timing harness, `performance.now()`, profiler, query plan), then bisect. Measure first, fix second. See [performance measurement](references/performance.md) for resource-specific tools and checks. + +## Phase 5: Fix + regression test + +Write the regression test **before the fix**, but only if there is a **correct seam** for it. + +A correct seam is one where the test exercises the **real bug pattern** as it occurs at the call site. If the only available seam is too shallow (single-caller test when the bug needs multiple callers, unit test that can't replicate the chain that triggered the bug), a regression test there gives false confidence. + +**If no correct seam exists, that itself is the finding.** Note it. The codebase architecture is preventing the bug from being locked down. Flag this for the next phase. + +If a correct seam exists: + +1. Turn the minimised repro into a failing test at that seam. +2. Watch it fail. +3. Apply the fix. +4. Watch it pass. +5. Re-run the Phase 1 feedback loop against the original (un-minimised) scenario. + +## Phase 6: Cleanup + +Required before declaring done: + +- [ ] Original repro no longer reproduces (re-run the Phase 1 loop) +- [ ] Regression test passes (or absence of seam is documented) +- [ ] All `[DEBUG-...]` instrumentation removed (`grep` the prefix) +- [ ] Throwaway prototypes deleted (or moved to a clearly-marked debug location) +- [ ] The hypothesis that turned out correct is stated in the commit / PR message, so the next debugger learns diff --git a/skills/diagnosing-bugs/references/feedback-loops.md b/skills/diagnosing-bugs/references/feedback-loops.md index 9353f15..a2ad440 100644 --- a/skills/diagnosing-bugs/references/feedback-loops.md +++ b/skills/diagnosing-bugs/references/feedback-loops.md @@ -11,6 +11,12 @@ Choose the smallest loop that reaches the real failure: 7. Automated bisection across commits, versions, configuration, or data. 8. Differential execution against a known-good implementation or state. 9. A structured human interaction with exact steps and captured observations. + If Bash and an interactive terminal are available, copy and customize + [the human interaction template](../scripts/hitl-loop.template.sh), then run + `bash <your-copy>.sh` there. The human follows prompts and shares observations; + the template echoes answers, so never enter credentials. If the active + Harness cannot offer interactive input, use its user-input tool for the same + steps and record the observations in the task log. A useful loop is specific enough to catch this bug, repeatable enough to compare changes, fast enough for several iterations, and runnable in the diff --git a/skills/diagnosing-bugs/scripts/hitl-loop.template.sh b/skills/diagnosing-bugs/scripts/hitl-loop.template.sh new file mode 100644 index 0000000..2431984 --- /dev/null +++ b/skills/diagnosing-bugs/scripts/hitl-loop.template.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Human-in-the-loop reproduction loop. +# Copy this file, edit the steps below, and run it. +# The agent runs the script; the user follows prompts in their terminal. +# +# Usage: +# bash hitl-loop.template.sh +# +# Two helpers: +# step "<instruction>" → show instruction, wait for Enter +# capture VAR "<question>" → show question, read response into VAR +# +# At the end, captured values are printed as KEY=VALUE for the agent to parse. +# +# `capture` prints its value back to the terminal, where the agent reads it, +# so capture observations, and leave signing in to the user as a `step`. + +set -euo pipefail + +step() { + printf '\n>>> %s\n' "$1" + read -r -p " [Enter when done] " _ +} + +capture() { + local var="$1" question="$2" answer + printf '\n>>> %s\n' "$question" + read -r -p " > " answer + printf -v "$var" '%s' "$answer" +} + +# --- edit below --------------------------------------------------------- + +step "Open the app at http://localhost:3000 and sign in." + +capture ERRORED "Click the 'Export' button. Did it throw an error? (y/n)" + +capture ERROR_MSG "Paste the error message (or 'none'):" + +# --- edit above --------------------------------------------------------- + +printf '\n--- Captured ---\n' +printf 'ERRORED=%s\n' "$ERRORED" +printf 'ERROR_MSG=%s\n' "$ERROR_MSG" diff --git a/skills/typescript-best-practices/references/patterns.md b/skills/typescript-best-practices/references/patterns.md index 6cc38bc..955c3d1 100644 --- a/skills/typescript-best-practices/references/patterns.md +++ b/skills/typescript-best-practices/references/patterns.md @@ -1,37 +1,85 @@ # TypeScript patterns +Code examples for each rule in `SKILL.md`. The underlying principles are language-agnostic. See the **type-system-discipline** and **boundary-discipline** principle skills. + +## Branded types + +Brand primitives so they can't be mixed up. Validate once at the boundary. Downstream code trusts the type. + +```ts +type AgentId = string & { readonly __brand: "AgentId" }; + +function parseAgentId(input: string): AgentId { + if (!isUUID(input)) throw new Error(`Invalid agent id: ${input}`); + return input as AgentId; +} + +function focusAgent(id: AgentId): void { + /* input is trusted */ +} +``` + +Match the `readonly __brand: 'X'` shape. Don't invent a new convention. + ## Discriminated unions +Model variants with a literal discriminant. Every variant shares the field name and each variant's value is unique, so impossible combos can't be represented. + ```ts +// Don't. Boolean + optionals lets contradictory states exist. +type DiffState = { loading: boolean; diff?: GitDiff; error?: string }; + +// Do. Only valid states exist. type DiffState = | { kind: "loading" } | { kind: "ready"; diff: GitDiff } | { kind: "error"; error: string }; ``` -Do not use `{ loading: boolean; diff?: GitDiff; error?: string }`, which permits -contradictory combinations. +Pick one discriminant name (`kind`, `type`, `tag`) and stick to it. + +## Constructive modeling + +Build the type from parts that are all legal instead of restricting a loose type with runtime checks. -## Branded primitives +Non-empty, via a variadic tuple: ```ts -declare const agentIdBrand: unique symbol; -type AgentId = string & { readonly [agentIdBrand]: true }; +type NonEmpty<T> = [T, ...T[]]; -function parseAgentId(input: string): AgentId { - if (!isUUID(input)) throw new Error(`Invalid agent id: ${input}`); - return input as AgentId; +// Don't: T[] plus a length check every caller must repeat +function pickWinner(entries: string[]): string { + if (entries.length === 0) throw new Error("no entries"); + return entries[Math.floor(Math.random() * entries.length)]; +} + +// Do: an empty value of the type can't exist +function pickWinner(entries: NonEmpty<string>): string { + return entries[Math.floor(Math.random() * entries.length)] ?? entries[0]; } ``` -The cast is localized after validation. Downstream functions accept `AgentId` -without repeating the check. +The fallback keeps the result total with `noUncheckedIndexedAccess`; the compiler +cannot prove that a computed index is in range even for a non-empty tuple. -## Constructive models +Where a plain `T[]` arrives, narrow once with a guard. The fact then travels in the type: ```ts -type NonEmpty<T> = readonly [T, ...T[]]; -type PairList<T> = readonly (readonly [T, T])[]; +const isNonEmpty = <T>(arr: T[]): arr is NonEmpty<T> => arr.length > 0; +``` + +Even length, as pairs: + +```ts +type Pairs<T> = [T, T][]; +``` + +A time range, as start plus duration: + +```ts +// Don't: a comment holds the invariant +type TimeRange = { start: Date; end: Date }; // start <= end + declare const durationBrand: unique symbol; type NonNegativeDurationMs = number & { readonly [durationBrand]: true }; type TimeRange = { start: Date; durationMs: NonNegativeDurationMs }; @@ -42,53 +90,245 @@ function parseDurationMs(value: number): NonNegativeDurationMs { } ``` -Use a stronger input when the loose type would force a non-null assertion: +A plain number still permits negative durations. Validate once when creating +the duration and derive the end when needed. Add a brand when callers must +distinguish the validated value from unchecked numbers. Expose useful readings +on top of the representation (`pairs.flat()`, a `rangeEnd()` helper). + +## Simplest total type + +Don't strengthen everything. Keep `T[]` when every operation on it is total: + +```ts +const sum = (xs: number[]) => xs.reduce((a, b) => a + b, 0); // [] is 0, fine +``` + +Strengthen when the loose type forces a lie at a use site. The tells are `!`, `arr[0] as T`, and a "should never happen" throw: ```ts +// Don't: partiality smuggled past the compiler +function newestSession(sessions: Session[]): Session { + return sessions.at(0)!; +} + +// Do: strengthen the input; the assertion disappears function newestSession(sessions: NonEmpty<Session>): Session { return sessions[0]; } ``` -## Parse unknown data +Weakening the result to `Session | undefined` is the other total signature. + +## `unknown` over `any` + +External data is always `unknown`. Narrow before use. ```ts +// Don't +function handle(input: any) { + return input.foo.bar; +} + +// Do +function handle(input: unknown) { + if (typeof input === "object" && input !== null && "foo" in input) { + // narrowed; compiler verifies access + } +} +``` + +External sources include RPC payloads, `JSON.parse`, `postMessage`, IPC, file contents, environment variables, database results. + +## Schemas before hand-rolled guards + +Before writing a property-by-property type guard for external data, look for the repository's runtime schema library and existing schemas. Let one schema own validation and derive the TypeScript type from it. Do not maintain a schema, a duplicate interface, and a guard that can drift apart. + +```ts +import { z } from "zod"; + +const UserSchema = z.object({ + id: z.string().uuid(), + role: z.enum(["admin", "member"]), +}); + +type User = z.infer<typeof UserSchema>; + +function parseUser(input: unknown): User { + return UserSchema.parse(input); +} +``` + +Use `safeParse` when failure is an expected branch. Use the equivalent inference helper when the repository uses another schema library. Do not add a new schema dependency for one guard. This rule prefers the schema system the codebase already trusts. + +## No `as` casts + +Every `as` is a potential runtime crash. Cast only after the type system has verified the claim. + +```ts +// Don't +const user = data as User; + +// Do. Validate fields and construct the result without an object cast. +type User = { id: string; role: "admin" | "member" }; + function parseUser(data: unknown): User { - if (typeof data !== "object" || data === null || !("id" in data)) { - throw new Error("expected a user object"); + if (typeof data !== "object" || data === null) { + throw new Error("expected object"); + } + if (!("id" in data) || typeof data.id !== "string") { + throw new Error("expected id"); + } + if (!("role" in data) || (data.role !== "admin" && data.role !== "member")) { + throw new Error("expected admin or member role"); } - if (typeof data.id !== "string") throw new Error("expected string id"); - return { id: data.id }; + return { id: data.id, role: data.role }; } ``` -Prefer a maintained schema validator when the project already uses one. +When refactoring an `as` out of existing code, identify why TypeScript can't infer: + +- Missing discriminant: add one, switch to a discriminated union. +- Overly wide source type (e.g. `Record<string, unknown>`): narrow it. +- Untyped boundary: add a parse function or schema. +- Genuinely inexpressible: use a branded type or `satisfies`. + +## Narrowing hierarchy + +From best to last-resort: + +1. **Discriminated union switch / if.** Compiler narrows automatically. +2. **`in` operator.** `"key" in obj` narrows to variants containing that key. +3. **`typeof` / `instanceof`.** For primitives and class instances. +4. **User-defined type guard.** When the above aren't enough. +5. **`as` cast.** Only after validation. + +```ts +function area(s: Shape): number { + if ("radius" in s) return Math.PI * s.radius ** 2; // narrowed to circle + return s.width * s.height; // narrowed to rect +} +``` + +## Type guards + +A guard must actually verify the claim. A lying guard is worse than `as`. + +```ts +function isCircle(s: Shape): s is Shape & { kind: "circle" } { + return s.kind === "circle"; +} +``` + +Prefer discriminant narrowing when possible. + +## Exhaustiveness -## Exhaustive variants +In default arms, assign the discriminant to a `never`-typed local. ```ts -function area(shape: Shape): number { - switch (shape.kind) { +// Value-returning switch +function area(s: Shape): number { + switch (s.kind) { case "circle": - return Math.PI * shape.radius ** 2; + return Math.PI * s.radius ** 2; case "rect": - return shape.width * shape.height; + return s.width * s.height; default: { - const exhaustive: never = shape; - return exhaustive; + const _exhaustive: never = s; + return _exhaustive; + } + } +} + +// Void switch +function handle(s: Shape): void { + switch (s.kind) { + case "circle": + drawCircle(s); + break; + case "rect": + drawRect(s); + break; + default: { + const _exhaustive: never = s; + void _exhaustive; } } } ``` -## `satisfies` and derived types +Return-style in value-returning switches, void-style in statement switches. + +## `satisfies` over `as` + +`satisfies` validates without widening literal types. ```ts -const config = { theme: "dark", columns: 3 } satisfies Config; +// Don't. Widens, loses literal types. +const config = { theme: "dark", cols: 3 } as Config; + +// Do. Validates AND preserves literal types. +const config = { theme: "dark", cols: 3 } satisfies Config; +// config.theme is "dark" (literal), not string +``` + +## Boundary validation + +Validate once where data crosses in. Trust types inside. See the **boundary-discipline** principle skill. + +- **Wire formats** (proto, JSON-RPC): follow the protocol's policy for unknown + fields. Use an option such as `ignoreUnknownFields` only when that parser + supports it and the contract permits forward-compatible extra fields. +- **Persisted JSON:** versioned blob with a try/catch around the parse. +- **Don't re-validate** deep in call chains. + +## Schema-derived types + +When a `.proto`, OpenAPI spec, GraphQL schema, or database migration already defines a shape, derive from the generated types instead of duplicating them. + +```ts +// Don't. Duplicate shape, drifts when the schema changes. +type CheckSummary = { + totalCount: number; + checks: { name: string; status: string }[]; +}; +function renderChecks(s: CheckSummary) { + /* ... */ +} + +// Do. Derive from the generated schema type. +import type { ChecksMessage } from "<generated module>"; +function renderChecks(s: Pick<ChecksMessage, "totalCount" | "checks">) { + /* ... */ +} +``` + +Reach for `Pick`, `Omit`, `Parameters`, `ReturnType`, `Awaited`, `typeof` before writing a new interface. + +## Object args + +```ts +// Positional arguments of the same type can be swapped without a type error. +moveSelection(startLineNumber, endLineNumber); + +// An object groups named options with their owning resource. +openFile(uri, { + startLineNumber: 10, + startColumn: 1, + endLineNumber: 10, + endColumn: 1, +}); -type RenderInput = Pick<GeneratedMessage, "totalCount" | "checks">; -type LoaderResult = Awaited<ReturnType<typeof loadData>>; +// Do. Order-independent, self-documenting. +openFile({ + uri, + selection: { + startLineNumber: 10, + startColumn: 1, + endLineNumber: 10, + endColumn: 1, + }, +}); ``` -Derive from the authoritative value or generated schema instead of maintaining -a parallel interface. +Skip on hot paths: per-frame render, tokenizers, parsers, anything in a tight loop where the allocation cost matters. diff --git a/skills/writing-for-agents/SKILL-MECHANICS.md b/skills/writing-for-agents/SKILL-MECHANICS.md new file mode 100644 index 0000000..5d03898 --- /dev/null +++ b/skills/writing-for-agents/SKILL-MECHANICS.md @@ -0,0 +1,37 @@ +# Skill mechanics + +Use the writing rules in [SKILL.md](SKILL.md) for the body. This reference covers +how an agent discovers a skill and how the skill reaches other instructions. + +## Frontmatter and invocation + +Every mstack skill keeps a `name` and a non-empty `description`. The name matches +its directory. The description tells the agent which task should trigger the +skill, so it costs context whenever the Harness loads its skill catalog. + +Use automatic discovery when the agent must recognize the task independently +or another workflow needs to select the skill. A human can still invoke it by +name. Describe distinct task branches once and leave the execution steps in +the body. + +For workflows the human should select, document explicit invocation in the +body. Check the active Harness's support before adding metadata that disables +automatic invocation. mstack's shared format does not assume all Harnesses can +hide a description or enforce invocation-only behavior. + +Shared references can be ordinary Markdown files linked by multiple skills. +They need no skill description when agents only reach them through those links. + +## Splitting by invocation + +Create a separate discoverable skill when it has a distinct trigger that users +actually use, or another workflow must select it independently. A new skill +adds a description to the catalog and a name for humans to remember. Keep a +branch as a linked reference when it needs neither form of independent reach. + +## Router skills + +A router gives the human one entry point for several related workflows. Name +each destination, state when to use it, and provide a resolvable link. When a +destination requires explicit human invocation in the active Harness, explain +how to invoke it; a router must not assume it can bypass that restriction. diff --git a/skills/writing-for-agents/SKILL.md b/skills/writing-for-agents/SKILL.md index c665e4e..be4243c 100644 --- a/skills/writing-for-agents/SKILL.md +++ b/skills/writing-for-agents/SKILL.md @@ -4,70 +4,79 @@ description: Write or revise instructions consumed by coding agents, including s license: MIT --- -# Writing for agents +Reference for writing any document an agent consumes: a skill, an `AGENTS.md` / `CLAUDE.md`, a doc reached by a pointer. The packaging differs; the writing does not: the same levers make each one predictable, since the agent takes the same _process_ every run rather than producing the same output. -Write instructions that change decisions reliably without spending context on -facts the agent can discover from the environment. +When the document you're writing is a skill, read [`SKILL-MECHANICS.md`](SKILL-MECHANICS.md) for frontmatter, invocation choice, and router skills. ## Context pointers -A context pointer names material that is currently out of context and states -when to load it. Skill descriptions and links from agent instructions are -context pointers. +A **context pointer** is a reference held in the agent's context that names some out-of-context material and encodes the condition for reaching it. A skill's description is one; a line in `AGENTS.md` naming a doc is the same object. The pointer's _wording_, not its target, decides when the agent reaches the material, and how reliably. A must-have target behind a weakly worded pointer is a variance bug: sharpen the wording first, and inline the material only if sharpening fails. -A useful pointer: +A pointer does two jobs: state what the material is, and list the **branches** that should trigger reaching it (a branch is a distinct case the document handles, so different runs take different paths through it). Every word of an always-loaded pointer costs on every turn, so it earns even harder pruning than the body: -- Starts with the task or object that should trigger it. -- Names each distinct branch once instead of listing synonyms. -- Says what the target contains and when it matters. -- Stays short because the pointer may be present on every turn. +- **Front-load the leading word**: the pointer is where it does its triggering work. +- **One trigger per branch.** Synonyms that rename a single branch are one branch written twice; collapse them and keep only genuinely distinct branches. +- **Cut identity the body already carries.** -Sharpen a weak pointer before moving its target into always-loaded context. +## The two loads -## Place information by need +Every document and pointer you add spends one of two budgets: -Keep ordered actions and universal constraints in the entry document. Move -details needed by only one branch into a linked reference beside that branch's -pointer. Keep a concept's definition, rules, and caveats together. +- **Context load** is the cost of always-loaded material on the agent's window: an `AGENTS.md` line, a skill description, anything sitting in context every turn, spending tokens and attention whether or not it fires. +- **Cognitive load** is the cost on the human: which documents exist and when to reach for each. The human is the index. Not a cost to minimise: it is the price of human agency; spend it where human judgement matters, remove it where it does not. -Split a document when it has become hard to navigate, when branches need -different reference material, or when later steps cause the agent to rush an -uncertain current step. A split must reduce the material a normal path loads; -otherwise it only adds another file to remember. +Material reached only through a pointer escapes context load at the price of the pointer's own line; material with no pointer at all rides entirely on cognitive load. -For a skill, prefer automatic discovery when the agent must recognize the task -without prompting or another workflow needs to route to it. Prefer explicit -invocation when the human should decide each time and can reasonably remember -the skill. A router can reduce that memory cost when several explicit skills -form one recognizable family. +## Information hierarchy -## Make completion observable +A document is built from two content types: **steps** (the ordered actions the agent performs) and **reference** (definitions, rules, facts consulted on demand). The two mix freely: all steps (a recipe), all reference (a review's rules, this skill), or both. The core decision is where each piece sits on the **information hierarchy**, a ladder ranked by how immediately the agent needs the material: -End each important step with a condition the agent can check. Prefer bounds -such as "every modified public entry point accounted for" or "the command was -run and its exit status recorded" over vague outcomes such as "understand the -system." +1. **In-file step** is the primary tier: what the agent does, in order. +2. **In-file reference** is consulted on demand. Often a legitimately flat peer-set (every rule of a review on one rung), which is a fine arrangement, not a smell. +3. **Disclosed reference** is pushed out into a separate file, reached by a context pointer, loaded only when the pointer fires. Spans a sibling file in the same folder through fully external reference that lives anywhere and any document can point at. -The bound should demand enough work to prevent premature completion without -prescribing ceremony unrelated to the task. +Push too little down and the top bloats; push too much and you hide material the agent actually needs. That tension is the whole decision. -## Use compact, stable language +**Progressive disclosure** is the move down the ladder (out of the main file and behind a pointer) so the top stays legible. Not primarily a token optimisation: it is how the hierarchy is protected. Branching is the cleanest disclosure test: inline what every branch needs, and push behind a pointer what only some branches reach. When a document has steps, in-file reference that should be disclosed buries them and turns attending to them into a coin-flip: a variance lever, not just a legibility one. -Reuse a short, established term when it carries the intended behavior more -reliably than a repeated explanation. Define project-specific terms once. -Prefer positive instructions that name the desired action. Use prohibitions -only for real guardrails, paired with the safe behavior. +**Co-location** is the within-file companion: where the ladder decides _how far down_ a piece sits, co-location decides _what sits beside it_ once there. Keep a concept's definition, rules, and caveats under one heading rather than scattered, so reading one part brings its neighbours with it. The test: the document should read like documentation written for the agent. Grouped material reads that way; scattered material does not. (Distinct from duplication: that repeats one meaning in two places; scattering fragments one meaning across many.) -## Prune +**Sprawl** is the failure mode here: a document simply too long, even when every line is live and unique. Attention thins across the excess, and every extra line is one more to keep relevant. The cure is the ladder: disclose reference behind pointers, and split by branch or sequence so each path carries only what it needs. -For each sentence, ask whether it changes likely agent behavior. Remove: +## Steps and completion criteria -- Facts available cheaply from code, configuration, directory layout, or - command help. -- Duplicate rules and stale examples. -- Generic advice the target agent already follows. -- Branch-specific detail from the common path. -- Rules added for a single incident when a narrower trigger would suffice. +Every step ends on a **completion criterion**, the condition that tells the agent the work is done. Two properties make it a lever: -Keep one source of truth for each rule. Test disputed wording against realistic -requests when the behavior matters more than editorial preference. +- **Clarity**: can the agent tell done from not-done? A vague bound ("understanding reached") invites **premature completion**: ending the step before it is genuinely done, attention slipping to _being done_. The visible steps still ahead (the **post-completion steps**) supply the pull; the criterion's clarity is the resistance. Defend in order: **sharpen the bound first** (local and cheap); only if it is irreducibly fuzzy _and_ you observe the rush, hide the later steps by splitting the sequence. Hiding only works across a real context boundary (a hand-off or a subagent dispatch; an inline call leaves the later steps in context and clears nothing). +- **Demand**: how much it requires. "Every modified model accounted for" forces thorough work where "produce a change list" does not. Demand drives **legwork** (the digging the agent does within the work, latent in the wording rather than written as its own step), and it is not step-bound: "every rule applied" binds a body of flat reference just as "every step done" binds a sequence, which is how an all-reference document still carries an exhaustiveness bar. + +The strongest criteria are both checkable and exhaustive. + +## When to split + +Splitting one document into two spends one of the two loads, so split only when the cut earns it: + +- **By sequence**: split a run of steps where the post-completion steps tempt the agent to rush the one in front of it. Keeping them out of view drives more legwork on the current task. Beware the reverse: merging sequences exposes each step's later steps to what follows, inviting premature completion. +- **By invocation**, skill-specific: see [`SKILL-MECHANICS.md`](SKILL-MECHANICS.md). + +## Leading words + +A **leading word** is a compact concept already living in the model's pretraining that the agent thinks with while running the document (_lesson_, _fog of war_, _tracer bullets_). Repeated as a token, never as a sentence, it accumulates a distributed definition and anchors a whole region of behaviour in the fewest tokens, by recruiting priors the model already holds. Coining your own works if you define it clearly, but a made-up word recruits no priors: you pay in definition tokens what a pretrained word gives free; reach for an existing word first. + +It anchors twice. In the body, _execution_: the agent reaches for the same behaviour every time the word appears, and inside flat reference it focuses attention on a class of thing to look for. In a pointer, _invocation_: when the same word lives in your prompts, your docs, and your codebase, the agent links that shared language to the material and reaches it more reliably. + +Hunt for opportunities to refactor with leading words. A triad spelled out at three sites, a pointer spending a sentence to gesture at one idea. Each is a passage begging to collapse into a single token: + +- "fast, deterministic, low-overhead" → _tight_ (a _tight_ loop). +- "a loop you believe in" → _red_, turning a fuzzy gate into a binary observable state (the loop goes _red_ on the bug, or it doesn't). + +You win twice: fewer tokens, and a sharper hook for the agent to hang its thinking on. Assume every document is carrying restatements that leading words retire. Go find them. + +**Negation** is the failure mode beside this lever: steering by prohibition drags the forbidden behaviour into context and makes it _more_ available, not less. _Don't think of an elephant_, and the elephant is all there is; the negation is a weak modifier the strongly-activated concept overruns, so the ban half-reads as an instruction to do the thing. Prompt the **positive**: state the target behaviour ("write one-line comments") so the banned one is never spoken. A prohibition earns its place only as a hard guardrail you cannot phrase positively; even then, pair it with the positive target so attention lands on what to do. + +## Pruning + +- Keep each meaning in a **single source of truth**: one authoritative place, so changing the behaviour is a one-place edit. **Duplication** (the same meaning in more than one place) costs maintenance and tokens, and inflates a meaning's prominence on the ladder past its real rank. (The accidental inverse of a leading word, which repeats a token on purpose, never the meaning.) +- The **environment** is a source of truth too (`package.json` scripts, config files, the directory layout, `--help` output), and a document that restates it is a **cache**: a copy of a lookup, earning its load only when the lookup is expensive. Cache what the agent cannot find by looking: the unwritten convention, the reason behind a choice, the gotcha no config confesses. Leave the one-file, one-command lookups to the environment, where they cannot go stale. +- Check every line for **relevance**: does it still bear on what the document does? A line loses relevance by never bearing on the task (mere exposition, or a branch that should be disclosed) or by going stale as the behaviour or world it describes changes. Shorter documents are easier to keep relevant. Without a pruning discipline the default fate is **sediment**: stale layers that settle because adding feels safe and removing feels risky, until you must core down through them to find what is still live. +- Hunt **no-ops** sentence by sentence: an instruction the model already obeys by default pays load to say nothing. The test (does it change behaviour versus the default?) is model-relative, not reader-relative: two people disagreeing about a no-op disagree about the default, and settle it by running the document, not by debate. When a sentence fails, delete the whole sentence rather than trim words from it. The test also grades leading words: a word too weak to beat the default (_be thorough_ when the agent is already thorough-ish) is a no-op, and the fix is a stronger word (_relentless_), not a different technique.