diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 3b89c77..c4afdc6 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "engineering-method", - "version": "0.3.0", + "version": "0.3.1", "description": "Risk-proportionate engineering workflows for Codex, Claude and OpenCode.", "author": { "name": "Peter Hense" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 1c50795..1044061 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "engineering-method", - "version": "0.3.0", + "version": "0.3.1", "description": "Risk-proportionate engineering workflows for Codex, Claude and OpenCode.", "author": { "name": "Peter Hense" diff --git a/BACKLOG.md b/BACKLOG.md index 235b723..f9c1ec7 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -157,6 +157,9 @@ Keep stable IDs unchanged. Order groups unblocker-first, then priority and depen - ✅ `EM-007` **P2** Publish stable version 0.1.0 and update public documentation - Depends on: `EM-005` - Notes: Published v0.1.0 at e9f5130 as stable Latest with checked ZIP and SHA256SUMS. 308 tests, validators, provenance, reproducible packaging, both clean installs, downloaded bytes and GitHub/main plus fixed-tag installs verified. Prior rc.1 unchanged; no issue migration. + +- ⭕ `EM-021` **P2** Hold every skill to the published skill authoring limits (release 0.3.1) + - Notes: Core 4300085 adds tests/test_skill_authoring.py; contents list in lifecycle-handoffs.md. Release 0.3.1 in preparation. - ⭕ `EM-019` **P3** Verify a Gemini host adapter in Method Core - Notes: Deferred from EM-018; shared adapter ownership is phense/method-core. No Gemini support claimed until fresh native discovery and behavior checks pass. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63d2a50..622e9b1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,6 +76,7 @@ alone do not prevent sensitive Git history from being published. ## Release policy +`v0.3.1` adds the skill authoring limits test from Method Core `4300085`. `v0.3.0` separates writing into Writing Method and bundles shared support from Method Core, with [split evidence](docs/verification/EM-018-method-split.md). `v0.2.1` refines the PDF template and rendered-output checks documented in diff --git a/README.md b/README.md index ca88752..9607425 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,11 @@ local checks first, reuse unaffected evidence, focused review of fixes, and an effort brake before testing or delegation grows beyond the task. Subscription limits and model-effort permissions remain binding across quota resets. -**[Version 0.3.0](https://github.com/phense/engineering-method/releases/tag/v0.3.0) -separates writing into its own plugin and bundles shared support from a pinned -Method Core.** See the [release notes](docs/releases/0.3.0.md). Earlier scopes: +**[Version 0.3.1](https://github.com/phense/engineering-method/releases/tag/v0.3.1) +adds a test that holds every skill to the published skill authoring limits.** +See the [release notes](docs/releases/0.3.1.md). Earlier scopes: +[0.3.0](docs/releases/0.3.0.md) separated writing into its own plugin and +bundled shared support from a pinned Method Core, [0.2.1](docs/releases/0.2.1.md) refined local PDF export, [0.2.0](docs/releases/0.2.0.md) added documentation and playbook work, and [0.1.1](docs/releases/0.1.1.md) added native OpenCode integration. The historical 0.1.0 baseline has independently accepted @@ -35,7 +37,7 @@ routing evaluation. ## Related methods and shared core -Version 0.3.0 is the current stable release; 0.2.1 was the last release that bundled the writing skills. +Version 0.3.1 is the current stable release; 0.2.1 was the last release that bundled the writing skills. Version 0.3.0 separates writing into [Writing Method](https://github.com/phense/writing-method). [Method Core](https://github.com/phense/method-core) owns the bundled host adapters, state/continuity tools, verification/review/isolation support and generic playbooks. Each repository keeps its own Issues and pull requests; the common topic is `agent-methods`. diff --git a/docs/installation.md b/docs/installation.md index 9ed3ae6..a536076 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -8,7 +8,7 @@ installation success does not establish live model behavior on every host. For OpenCode, follow [the source-linked installation](opencode.md), available since 0.1.1. Its native discovery and bounded DeepSeek test were checked with OpenCode 1.18.30; see the [0.1.1 release scope](releases/0.1.1.md) and the -[0.3.0 release notes](releases/0.3.0.md). +[0.3.1 release notes](releases/0.3.1.md). ## Split into three repositories @@ -38,11 +38,11 @@ claude plugin install engineering-method@engineering-method claude plugin list --json ``` -For stable version 0.3.0 on either host, clone the release tag and register the +For stable version 0.3.1 on either host, clone the release tag and register the absolute checkout path instead of the GitHub shorthand: ```sh -git clone --branch v0.3.0 --depth 1 https://github.com/phense/engineering-method.git +git clone --branch v0.3.1 --depth 1 https://github.com/phense/engineering-method.git cd engineering-method em_plugin="$(pwd)" codex plugin marketplace add "$em_plugin" @@ -55,11 +55,11 @@ Choose one source per marketplace name. If switching an existing local marketplace to GitHub, uninstall this plugin and remove its marketplace using the commands below, then register the new source. Preserve your project files. -Alternatively, download `engineering-method-0.3.0.zip` and -`SHA256SUMS` from the [0.3.0 release](https://github.com/phense/engineering-method/releases/tag/v0.3.0). +Alternatively, download `engineering-method-0.3.1.zip` and +`SHA256SUMS` from the [0.3.1 release](https://github.com/phense/engineering-method/releases/tag/v0.3.1). In the download directory, run `shasum -a 256 -c SHA256SUMS`, extract the archive, and register the extracted `engineering-method` directory as above. -The plugin manifests and stable Git tag identify version `0.3.0`. The older +The plugin manifests and stable Git tag identify version `0.3.1`. The older `v0.1.0-rc.1` pre-release remains available for historical reference. Claude also supports a session-only load: diff --git a/docs/opencode.md b/docs/opencode.md index 617dfcd..2a0581a 100644 --- a/docs/opencode.md +++ b/docs/opencode.md @@ -1,6 +1,6 @@ # OpenCode integration -Version 0.3.0 bundles 19 engineering/shared skills. Writing Method +Version 0.3.1 bundles 19 engineering/shared skills. Writing Method bundles its own 18 writing/shared skills and a distinct `writing-method.js` loader. Both use the same adapter from Method Core. Earlier release evidence under `docs/verification/` describes the combined historical packages and is not @@ -16,7 +16,7 @@ macOS. Use version 0.1.1 or newer; the earlier v0.1.0 release predates this integration. Clone a retained checkout and install from it: ```sh -git clone --branch v0.3.0 --depth 1 https://github.com/phense/engineering-method.git +git clone --branch v0.3.1 --depth 1 https://github.com/phense/engineering-method.git cd engineering-method ``` diff --git a/docs/releases/0.3.1.md b/docs/releases/0.3.1.md new file mode 100644 index 0000000..adafdd3 --- /dev/null +++ b/docs/releases/0.3.1.md @@ -0,0 +1,51 @@ +# Engineering Method 0.3.1 + +Released 2026-11-13. A maintenance release: every skill is now held to the +published skill authoring limits by a test. + +## Changes + +- Syncs [Method Core](https://github.com/phense/method-core) revision `4300085` + (70 files). It adds `tests/test_skill_authoring.py`, which checks each + `skills/*/SKILL.md`: a name of at most 64 lowercase letters, digits and + hyphens without a reserved word; a description of at most 1,024 characters in + the third person without XML tags; portable frontmatter keys; a body under + 500 lines; forward slashes in links; and a contents list at the top of every + reference file above 100 lines. +- Adds a contents list to `shared/policies/lifecycle-handoffs.md` and, through + the core, to the Claude and Codex platform adapters. +- No skill name, description or behaviour changes. All 19 skills already met + the name, description and length limits. + +## Verification and limits + +The complete Python suite passes 347 tests on macOS. `scripts/validate-plugin` +and `sync-core --check` pass. The committed-source package is built twice for +reproducibility. + +The test proves that the limits are kept. It does not measure whether a host +selects a skill for a plainly worded request. One unscripted probe on Claude +Code during this work loaded `systematic-debugging` for a failing test without +being told to, and loaded no specification skill for a new cross-module +feature request; that single run is an observation, not a rate. The paid native +routing and lifecycle evaluations were not rerun; the evidence limits of +[0.3.0](0.3.0.md) remain. + +The downloadable package is `engineering-method-0.3.1.zip`; verify it with the +attached `SHA256SUMS`. + +## Install or update + +```sh +claude plugin marketplace update engineering-method +claude plugin update engineering-method@engineering-method +``` + +```sh +codex plugin remove engineering-method@engineering-method +codex plugin add engineering-method@engineering-method +``` + +Restart the host session afterwards. See +[Installation and maintenance](../installation.md) for a fixed-tag install and +[OpenCode installation](../opencode.md) for the source-linked loader. diff --git a/method-core.lock.json b/method-core.lock.json index 72b5225..4e248b3 100644 --- a/method-core.lock.json +++ b/method-core.lock.json @@ -138,12 +138,12 @@ { "mode": "100644", "path": "shared/platform/claude.md", - "sha256": "6534484a16e086ee7328ecfec82a747363cd857661319ce9bc65d2932633925a" + "sha256": "be35b70b8fc0990da7975b3df4bd7007ca590d6288c2a2f98d46ef1deb88d4ad" }, { "mode": "100644", "path": "shared/platform/codex.md", - "sha256": "b9b2f32eff905f183cfb1f685ddf126363e52847e46bdb160f0f24ead9008e2a" + "sha256": "be2002849b7e7dc20da86e9574a8fc0150208f1dfa61de27eb9034b822ebcaee" }, { "mode": "100644", @@ -340,6 +340,11 @@ "path": "tests/test_platform_adapters.py", "sha256": "0e2970719f66384db7fd33db57959ce2c44770801eca86e0721f688dcd922dd9" }, + { + "mode": "100644", + "path": "tests/test_skill_authoring.py", + "sha256": "f0b6f864491cd226840ea36580f6873f43bc1d2c675517714f0090255493bc6b" + }, { "mode": "100644", "path": "third-party/core-sources.lock.json", @@ -347,7 +352,7 @@ } ], "repository": "https://github.com/phense/method-core.git", - "revision": "087476ccedf03342604e110768d9b528b5ae00ba", + "revision": "4300085b164686cc359a98cf7b3ee965cbd886b0", "schema_version": 1, "version": "0.1.0" } diff --git a/method.json b/method.json index b7db23d..3a9ee8c 100644 --- a/method.json +++ b/method.json @@ -1,7 +1,7 @@ { "schema_version": 1, "name": "engineering-method", - "version": "0.3.0", + "version": "0.3.1", "method_skills": [ "architecture-modeling", "openspec-apply", diff --git a/pyproject.toml b/pyproject.toml index f9d965a..26c5501 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "engineering-method" -version = "0.3.0" +version = "0.3.1" description = "Risk-proportionate engineering workflows for Codex, Claude and OpenCode." requires-python = ">=3.11" license = { text = "MIT" } diff --git a/shared/platform/claude.md b/shared/platform/claude.md index da12e6f..6196abb 100644 --- a/shared/platform/claude.md +++ b/shared/platform/claude.md @@ -4,6 +4,14 @@ Shared Method Core host mapping. Shared skills keep semantic names; resolve them against this adapter in Claude Code. The conventional plugin-root `skills/` directory is the same tree exposed by the Codex manifest. +## Contents + +- Host mapping (the JSON block below) +- Capability resolution +- Isolation and continuity +- Verified CLI interfaces +- Method integration + ```json { "host": "claude", diff --git a/shared/platform/codex.md b/shared/platform/codex.md index 28010bd..c853a1a 100644 --- a/shared/platform/codex.md +++ b/shared/platform/codex.md @@ -4,6 +4,14 @@ Shared Method Core host mapping. Shared skills keep semantic names; resolve those names against this adapter when executing in Codex. Both host manifests expose the same plugin-root `skills/` tree. +## Contents + +- Host mapping (the JSON block below) +- Capability resolution +- Isolation and continuity +- Verified CLI interfaces +- Method integration + ```json { "host": "codex", diff --git a/shared/policies/lifecycle-handoffs.md b/shared/policies/lifecycle-handoffs.md index 193a56b..60a27e6 100644 --- a/shared/policies/lifecycle-handoffs.md +++ b/shared/policies/lifecycle-handoffs.md @@ -2,6 +2,15 @@ Every phase consumes current artifacts from disk and names one next phase. +## Contents + +One JSON block with these keys: + +- `nodes` +- `executors` +- `edges` +- `retry_transitions` + ```json { "nodes": [ diff --git a/tests/test_skill_authoring.py b/tests/test_skill_authoring.py new file mode 100644 index 0000000..d1ae6f0 --- /dev/null +++ b/tests/test_skill_authoring.py @@ -0,0 +1,120 @@ +"""Skill files follow the published skill authoring limits. + +The limits come from Anthropic's skill authoring guidance: a name of at most 64 +lowercase letters, digits and hyphens without a reserved word; a description of +at most 1,024 characters in the third person without XML tags; a SKILL.md body +under 500 lines; forward slashes in paths; and a contents list at the top of +every reference file longer than 100 lines, so that a partial read still shows +its scope. The frontmatter keys are the ones every host accepts. +""" + +from __future__ import annotations + +import re +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +NAME = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*") +RESERVED = ("anthropic", "claude") +PORTABLE_KEYS = {"name", "description", "license", "compatibility", "metadata", "allowed-tools"} +MAX_NAME = 64 +MAX_DESCRIPTION = 1024 +MAX_BODY_LINES = 500 +CONTENTS_ABOVE_LINES = 100 +CONTENTS_WITHIN_LINES = 40 +FRONTMATTER = re.compile(r"\A---\n(.*?)\n---\n", re.S) +LINK = re.compile(r"\[[^\]]*\]\(#\s]+)[^)]*\)") +CONTENTS = re.compile(r"^#{1,3}\s*(Contents|Table of contents)\s*$", re.I | re.M) +NOT_THIRD_PERSON = re.compile(r"\b(I|we|you|your|my|our)\b") + + +def skill_files() -> list[Path]: + return sorted(ROOT.glob("skills/*/SKILL.md")) + + +def split(path: Path) -> tuple[dict[str, str], str]: + text = path.read_text(encoding="utf-8") + match = FRONTMATTER.match(text) + if match is None: + return {}, text + fields: dict[str, str] = {} + for line in match.group(1).splitlines(): + if line[:1] in (" ", "\t", "#") or ":" not in line: + continue + key, _, value = line.partition(":") + value = value.strip() + if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'": + value = value[1:-1] + fields[key.strip()] = value + return fields, text[match.end():] + + +def linked_markdown(path: Path) -> set[Path]: + found = set() + for target in LINK.findall(path.read_text(encoding="utf-8", errors="ignore")): + if "://" in target or target.startswith("mailto:"): + continue + resolved = (path.parent / target).resolve() + if resolved.suffix == ".md" and resolved.is_file() and ROOT in resolved.parents: + found.add(resolved) + return found + + +def reference_files() -> set[Path]: + """Markdown a skill can be sent to: the shared and skill directories, and links two levels deep.""" + found = {p.resolve() for base in ("shared", "skills") for p in (ROOT / base).rglob("*.md")} + for skill in skill_files(): + first = linked_markdown(skill) + found |= first + for path in first: + found |= linked_markdown(path) + return {p for p in found if p.name != "SKILL.md"} + + +class SkillAuthoringTests(unittest.TestCase): + def test_there_are_skills_to_check(self) -> None: + self.assertTrue(skill_files()) + + def test_frontmatter_names_the_skill_within_the_limits(self) -> None: + for path in skill_files(): + with self.subTest(skill=path.parent.name): + fields, _ = split(path) + self.assertTrue(set(fields) <= PORTABLE_KEYS, sorted(set(fields) - PORTABLE_KEYS)) + name = fields.get("name", "") + self.assertEqual(path.parent.name, name) + self.assertLessEqual(len(name), MAX_NAME) + self.assertRegex(name, rf"\A{NAME.pattern}\Z") + self.assertFalse([word for word in RESERVED if word in name]) + + def test_description_is_bounded_plain_and_in_the_third_person(self) -> None: + for path in skill_files(): + with self.subTest(skill=path.parent.name): + description = split(path)[0].get("description", "") + self.assertTrue(description.strip()) + self.assertLessEqual(len(description), MAX_DESCRIPTION) + self.assertNotRegex(description, r"[<>]") + self.assertEqual([], NOT_THIRD_PERSON.findall(description)) + + def test_body_stays_under_the_line_limit(self) -> None: + for path in skill_files(): + with self.subTest(skill=path.parent.name): + self.assertLess(len(split(path)[1].splitlines()), MAX_BODY_LINES) + + def test_links_use_forward_slashes(self) -> None: + for path in skill_files(): + with self.subTest(skill=path.parent.name): + targets = LINK.findall(path.read_text(encoding="utf-8")) + self.assertEqual([], [target for target in targets if "\\" in target]) + + def test_long_reference_files_open_with_a_contents_list(self) -> None: + missing = [] + for path in sorted(reference_files()): + lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() + if len(lines) > CONTENTS_ABOVE_LINES and not CONTENTS.search("\n".join(lines[:CONTENTS_WITHIN_LINES])): + missing.append(f"{path.relative_to(ROOT)} ({len(lines)} lines)") + self.assertEqual([], missing) + + +if __name__ == "__main__": + unittest.main()