Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
3 changes: 3 additions & 0 deletions BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<!-- engineering-method:backlog {"schema_version":1,"id":"EM-021","title":"Hold every skill to the published skill authoring limits (release 0.3.1)","status":"open","priority":"P2","parent_id":null,"depends_on":[],"notes":"Core 4300085 adds tests/test_skill_authoring.py; contents list in lifecycle-handoffs.md. Release 0.3.1 in preparation.","updated_at":"2026-11-13T18:08:00Z"} -->
- ⭕ `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.
<!-- engineering-method:backlog {"schema_version":1,"id":"EM-019","title":"Verify a Gemini host adapter in Method Core","status":"open","priority":"P3","parent_id":null,"depends_on":[],"notes":"Deferred from EM-018; shared adapter ownership is phense/method-core. No Gemini support claimed until fresh native discovery and behavior checks pass.","updated_at":"2026-11-13T10:39:43Z"} -->
- ⭕ `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.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`.

Expand Down
12 changes: 6 additions & 6 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"
Expand All @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions docs/opencode.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
```

Expand Down
51 changes: 51 additions & 0 deletions docs/releases/0.3.1.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 8 additions & 3 deletions method-core.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -340,14 +340,19 @@
"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",
"sha256": "91a39dfde56856e141cc29795bea8e151dd49a6bd3efcdc042d523dc01df2db8"
}
],
"repository": "https://github.com/phense/method-core.git",
"revision": "087476ccedf03342604e110768d9b528b5ae00ba",
"revision": "4300085b164686cc359a98cf7b3ee965cbd886b0",
"schema_version": 1,
"version": "0.1.0"
}
2 changes: 1 addition & 1 deletion method.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"schema_version": 1,
"name": "engineering-method",
"version": "0.3.0",
"version": "0.3.1",
"method_skills": [
"architecture-modeling",
"openspec-apply",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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" }
Expand Down
8 changes: 8 additions & 0 deletions shared/platform/claude.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions shared/platform/codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions shared/policies/lifecycle-handoffs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
120 changes: 120 additions & 0 deletions tests/test_skill_authoring.py
Original file line number Diff line number Diff line change
@@ -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()
Loading