Skip to content

Commit 8bfd124

Browse files
neilgfosterclaude
andcommitted
refactor: adopt two-tier plugin layout
Align msgraph-stdlib with the template's two-tier layout. The shippable payload now lives under plugin/ and the repo root is the build/distribution repo. This adds the previously-missing .claude-plugin/marketplace.json, which is what makes `/plugin install msgraph-stdlib@neilgfoster/msgraph-stdlib` resolvable. - Move payload under plugin/: .claude-plugin/plugin.json, hooks/, skills/, src/{example,msgraph}/ (CLAUDE_PLUGIN_ROOT resolves to plugin/, so internal references are unchanged). - Add root build layer: marketplace.json (source ./plugin), pyproject.toml, CI + release workflows, PR template, CHANGELOG, CONTRIBUTING. - Add tests: marketplace->plugin manifest resolution; client describe smoke. - Update README / CLAUDE.md / DEFINITION_OF_DONE paths; ignore uv/ruff/pytest dev artifacts. Layout-only; no runtime behaviour change. Tests pass (3/3). Pre-existing ruff findings in the in-progress kernel are left for /speckit-implement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 0f03500 commit 8bfd124

20 files changed

Lines changed: 989 additions & 21 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "msgraph-stdlib",
3+
"owner": {
4+
"name": "Neil Foster",
5+
"url": "https://github.com/neilgfoster"
6+
},
7+
"plugins": [
8+
{
9+
"name": "msgraph-stdlib",
10+
"source": "./plugin",
11+
"description": "Stdlib-only, zero-backend Claude Code plugin for Microsoft Graph: read Outlook mail and author/verify native Outlook message rules. Read-only by default, with a least-privilege scope ratchet and read-only catch-set verification before any rule is installed."
12+
}
13+
]
14+
}

.github/pull_request_template.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!--
2+
PR title should be a Conventional Commit (e.g. `feat: ...`, `fix: ...`, `docs: ...`).
3+
-->
4+
5+
## What and why
6+
7+
<!-- One paragraph: what this changes and why. -->
8+
9+
## Conventions
10+
11+
- [ ] Runtime stays **stdlib-only, zero-dependency, zero-backend** (`urllib`/`json`; ruff/pytest are
12+
dev tooling only).
13+
- [ ] No secrets/tokens in the repo — they live outside it in an XDG path (`0600`).
14+
- [ ] Any new/changed skill follows `docs/AGENT-FRIENDLY.md` (description + CLI I/O are the contract).
15+
- [ ] Read-only safety model intact: `Mail.Read`-only read path, scope ratchet for writes,
16+
verify-then-install (read-only catch-set), file-to-folder (never delete).
17+
18+
## Verification
19+
20+
```sh
21+
ruff check . && ruff format --check .
22+
python3 -m pytest -q
23+
```
24+
25+
- [ ] `ruff check .` and `ruff format --check .` pass.
26+
- [ ] `python3 -m pytest -q` passes.

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
check:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-python@v5
14+
with:
15+
python-version: "3.11"
16+
- name: Install dev tooling
17+
run: pip install ruff pytest
18+
- name: Lint
19+
run: ruff check .
20+
- name: Format check
21+
run: ruff format --check .
22+
- name: Test
23+
run: python3 -m pytest -q

.github/workflows/release.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: release
2+
3+
# Cut a GitHub Release from a vX.Y.Z tag. The version in
4+
# plugin/.claude-plugin/plugin.json is the single source of truth; this workflow refuses to
5+
# release if the tag and that version disagree, then publishes notes from CHANGELOG.md.
6+
7+
on:
8+
push:
9+
tags: ["v*.*.*"]
10+
11+
permissions:
12+
contents: write
13+
14+
jobs:
15+
release:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Verify tag matches plugin.json version
21+
run: |
22+
tag="${GITHUB_REF_NAME#v}"
23+
ver=$(python3 -c "import json; print(json.load(open('plugin/.claude-plugin/plugin.json'))['version'])")
24+
if [ "$tag" != "$ver" ]; then
25+
echo "tag $GITHUB_REF_NAME does not match plugin.json version $ver" >&2
26+
exit 1
27+
fi
28+
29+
- name: Extract release notes from CHANGELOG
30+
run: |
31+
ver="${GITHUB_REF_NAME#v}"
32+
awk -v v="$ver" '
33+
$0 ~ "^## \\[" v "\\]" {grab=1; next}
34+
grab && /^## \[/ {exit}
35+
grab {print}
36+
' CHANGELOG.md > RELEASE_NOTES.md
37+
if [ ! -s RELEASE_NOTES.md ]; then
38+
echo "No CHANGELOG section for $ver." > RELEASE_NOTES.md
39+
fi
40+
41+
- name: Create GitHub Release
42+
env:
43+
GH_TOKEN: ${{ github.token }}
44+
run: gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes-file RELEASE_NOTES.md

.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ __pycache__/
22
*.pyc
33
.DS_Store
44

5+
# Local dev-tooling artifacts (ruff/pytest via uv) — never shipped.
6+
.venv/
7+
uv.lock
8+
.ruff_cache/
9+
.pytest_cache/
10+
511
# Secrets/tokens NEVER live in the repo — they belong outside it (XDG, 0600).
612
# These patterns are belt-and-braces only; the real guarantee is storing tokens
713
# at ${XDG_STATE_HOME:-~/.local/state}/<plugin>/ outside the project tree.
@@ -20,3 +26,20 @@ specs/
2026
.github/agents/speckit*
2127
.github/prompts/speckit*
2228
# --- end ---
29+
30+
# --- tredl (solo trial) ---
31+
# tredl is installed transparently for one person: the Spec Kit and tredl artifacts below stay
32+
# local, so adopting tredl leaves (almost) no footprint on the team. When the value is proven,
33+
# delete this block and commit the artifacts to graduate the team. See
34+
# docs/adr/0003-transparent-solo-by-default.md.
35+
# Caveat: if your repo already tracks a CLAUDE.md, Spec Kit appends a small SPECKIT pointer
36+
# block to it - a tracked edit a .gitignore cannot cover. Run `git checkout CLAUDE.md` to drop
37+
# it for a zero tracked footprint, or keep it.
38+
.tredl/
39+
.specify/
40+
specs/
41+
.claude/skills/speckit-*/
42+
.agents/skills/speckit-*/
43+
.github/agents/speckit*
44+
.github/prompts/speckit*
45+
# --- end tredl ---

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Changelog
2+
3+
All notable changes to this plugin are recorded here. The format follows
4+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this plugin uses
5+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html). Pre-1.0, breaking changes may land on a
6+
minor bump and are called out explicitly.
7+
8+
The `version` in `plugin/.claude-plugin/plugin.json` is the single source of truth; the release
9+
workflow refuses to publish a tag that disagrees with it.
10+
11+
## [Unreleased]
12+
13+
<!--
14+
Add notes here under Added / Changed / Fixed / Removed. On release, move them under a new
15+
## [X.Y.Z] - YYYY-MM-DD heading and bump plugin/.claude-plugin/plugin.json to match.
16+
-->

CLAUDE.md

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ to "done" is in this repo. The target is `DEFINITION_OF_DONE.md`; read it first.
1010
`msgraph-stdlib` — a Claude Code plugin that lets an agent **read Outlook mail** and **author native
1111
Outlook message rules** via Microsoft Graph, with no third-party dependencies and no backend.
1212

13-
The `src/example/` package and `skills/example-subject-verb/` skill are the **inert template
14-
reference pattern**. Your first build step is to replace them (see "Build plan" below). Keep them as
13+
The `plugin/src/example/` package and `plugin/skills/example-subject-verb/` skill are the **inert
14+
template reference pattern**. Your first build step is to replace them (see "Build plan" below). Keep them as
1515
a reference while you work; delete them once the real skills exist.
1616

1717
## Non-negotiable conventions (inherited from the template — do not relax)
@@ -46,13 +46,13 @@ This is the heart of the plugin; do not weaken it for convenience.
4646

4747
## Capabilities to build (the verbs)
4848

49-
Group into skills under `skills/<subject>-<verb>/`, all backed by the `src/msgraph/` kernel:
49+
Group into skills under `plugin/skills/<subject>-<verb>/`, all backed by the `plugin/src/msgraph/` kernel:
5050

5151
| Skill | Scope | Notes |
5252
|---|---|---|
53-
| `auth-login` | `Mail.Read` (default) or `+MailboxSettings.ReadWrite` (opt-in) | device-code flow; cache token at the XDG path; refresh via refresh-token |
53+
| `auth-login` | `Mail.Read + MailboxSettings.Read` (default read-only) or `Mail.Read + MailboxSettings.ReadWrite` (opt-in) | device-code flow; cache token at the XDG path; refresh via refresh-token |
5454
| `mail-list` / `mail-get` | `Mail.Read` | list/get messages incl. headers (`internetMessageHeaders`); concise/detailed; pagination default |
55-
| `rule-list` | `Mail.Read` *(rules are mailbox settings — confirm exact read scope during `plan`)* | enumerate existing `messageRule`s, agent-legible |
55+
| `rule-list` | `MailboxSettings.Read` *(rules are mailbox settings; included in read-only mode — resolved during `plan`)* | enumerate existing `messageRule`s, agent-legible |
5656
| `rule-verify` | `Mail.Read` | given candidate predicates (e.g. `headerContains: ["List-Unsubscribe"]`), compute and return the **read-only catch-set** — no write |
5757
| `rule-create` | `MailboxSettings.ReadWrite` | install a verified rule (predicate → move-to-folder action). Refuse unless a catch-set was verified |
5858
| `rule-remove` | `MailboxSettings.ReadWrite` | delete a rule by id (the reversibility primitive) |
@@ -63,10 +63,10 @@ Graph endpoints are simple REST over `urllib`; `messageRule` lives under
6363
## Prerequisite the human must do once (free)
6464

6565
Azure AD **app registration**: public client, device-code/public-client flow enabled, delegated
66-
permission `Mail.Read` (+ `MailboxSettings.ReadWrite` for rule authoring). Personal accounts need no
67-
admin consent. The session should read `MSGRAPH_CLIENT_ID` / `MSGRAPH_TENANT_ID` (default tenant
66+
permissions `Mail.Read` + `MailboxSettings.Read` (read-only: read mail and list rules) (+
67+
`MailboxSettings.ReadWrite` for rule authoring). Personal accounts need no admin consent. The session should read `MSGRAPH_CLIENT_ID` / `MSGRAPH_TENANT_ID` (default tenant
6868
`consumers` or `common`) from the environment — never hardcode them. Document this in
69-
`skills/auth-login` and the README. **This is not blocking** for `specify`/`clarify`/`plan`/`tasks`
69+
`plugin/skills/auth-login` and the README. **This is not blocking** for `specify`/`clarify`/`plan`/`tasks`
7070
or for offline-testable code; only live auth/integration testing needs it.
7171

7272
## Build plan (spec-first; this is the work)
@@ -80,16 +80,28 @@ or advertise it.
8080
2. `/speckit-specify` — one feature: **Graph device-code auth + Outlook mail read + message-rule
8181
CRUD with read-only catch-set verification.** Carry the safety model above as hard requirements.
8282
3. `/speckit-clarify``/speckit-plan``/speckit-tasks``/speckit-implement`.
83-
4. Replace `src/example/``src/msgraph/` (update `APP`); replace the example skill with the real
84-
ones; make `python3 -m msgraph.client describe` emit the `TOOLS` catalog.
83+
4. Replace `plugin/src/example/``plugin/src/msgraph/` (update `APP`); replace the example skill
84+
with the real ones; make `python3 -m msgraph.client describe` emit the `TOOLS` catalog.
8585
5. Validate against `DEFINITION_OF_DONE.md`. Open a PR for review.
8686

8787
## Where things are
8888

8989
| Path | Purpose |
9090
|---|---|
9191
| `DEFINITION_OF_DONE.md` | **The build target** — read first |
92-
| `.claude-plugin/plugin.json` | Plugin manifest |
93-
| `skills/<subject>-<verb>/SKILL.md` | Agent-facing commands |
94-
| `src/msgraph/client.py` | Stdlib kernel — importable AND runnable; owns the `describe` catalog |
92+
| `.claude-plugin/marketplace.json` | Marketplace entry — plugin `source` points at `./plugin` |
93+
| `plugin/.claude-plugin/plugin.json` | Plugin manifest (lives inside the shippable payload) |
94+
| `plugin/skills/<subject>-<verb>/SKILL.md` | Agent-facing commands |
95+
| `plugin/src/msgraph/client.py` | Stdlib kernel — importable AND runnable; owns the `describe` catalog |
9596
| `docs/AGENT-FRIENDLY.md` | **Required reading** — agent-tool design principles |
97+
| `pyproject.toml`, `tests/`, `.github/` | Dev tooling, tests, CI/release — never shipped in `plugin/` |
98+
99+
**Two-tier layout.** `plugin/` is the shippable payload (its own `.claude-plugin/plugin.json` +
100+
`skills/`, `src/`, `hooks/`); the repo root is the build/distribution repo. Inside the plugin,
101+
reference bundled files via `${CLAUDE_PLUGIN_ROOT}/...` — it resolves to `plugin/`.
102+
103+
<!-- SPECKIT START -->
104+
Active feature plan: `specs/001-msgraph-mail-rules/plan.md` (spec, research, data-model,
105+
contracts/tools.md, quickstart alongside it). Read it for the technical context, the stdlib-only
106+
device-code design, the `TOOLS` catalog contract, and the safety-model decisions before implementing.
107+
<!-- SPECKIT END -->

CONTRIBUTING.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Contributing
2+
3+
`msgraph-stdlib` is a standalone, public, MIT plugin. The few rules that earn their keep:
4+
5+
## Standards
6+
7+
- **Python 3.11+, stdlib only at runtime.** `urllib` for HTTP, `json` for parsing. `ruff` and
8+
`pytest` are dev tooling — they never ship in `plugin/`. No third-party runtime dependencies, no
9+
backend/server process. The constraint is the product: portable, auditable, no supply-chain surface.
10+
- **Two-tier layout.** `plugin/` is the shippable payload (its own `.claude-plugin/plugin.json` +
11+
`skills/`, `src/`, `hooks/`); the repo root is the build/distribution repo
12+
(`.claude-plugin/marketplace.json`, `pyproject.toml`, `tests/`, `.github/`, docs). Inside the
13+
plugin, reference bundled files via `${CLAUDE_PLUGIN_ROOT}/...` — it resolves to `plugin/`.
14+
- **Command naming: `<subject>-<verb>`.** Skills are named subject-then-verb (e.g. `mail-list`,
15+
`rule-verify`). Keep the convention.
16+
- **Secrets live OUTSIDE the repo** — a user-level XDG path
17+
(`${XDG_STATE_HOME:-~/.local/state}/msgraph-stdlib/`) with `0600` perms. Never committed, not even
18+
encrypted. Do not add an in-repo secrets path or a git-crypt dependency.
19+
- **Safety model is structural.** Read path requests only `Mail.Read`; rule authoring uses the
20+
separate `MailboxSettings.ReadWrite` scope; a rule is verified against a read-only catch-set before
21+
install; rule actions file to a folder and never delete. Do not weaken these for convenience.
22+
- **Agent-friendly by design.** Every skill follows `docs/AGENT-FRIENDLY.md` — read it before adding
23+
or changing a skill. The skill `description` and the CLI's inputs/outputs **are** the tool contract
24+
an agent reads.
25+
- **Conventional Commit messages and PR titles.**
26+
27+
## Verify before claiming done
28+
29+
```sh
30+
ruff check . && ruff format --check .
31+
python3 -m pytest -q
32+
```
33+
34+
CI (`.github/workflows/ci.yml`) runs exactly these; green CI is part of the Definition of Done.
35+
36+
## Cutting a release
37+
38+
SemVer, tag-driven. The version in `plugin/.claude-plugin/plugin.json` is the single source of truth.
39+
40+
1. Bump `version` in `plugin/.claude-plugin/plugin.json`.
41+
2. In `CHANGELOG.md`, move `## [Unreleased]` notes under a new `## [X.Y.Z] - YYYY-MM-DD` heading.
42+
3. `ruff check . && ruff format --check . && python3 -m pytest -q`.
43+
4. Merge, then `git tag vX.Y.Z && git push origin vX.Y.Z`. The release workflow publishes it.

DEFINITION_OF_DONE.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,12 @@ plugin is "done" for its first release when **every** box below is true. Read `C
4040

4141
- [ ] **Offline-testable.** Classification/verification/output-shaping logic is unit-tested without
4242
network (Graph HTTP boundary mockable). Tests pass with stdlib only.
43-
- [ ] **Template residue gone.** `src/example/` and `skills/example-subject-verb/` replaced by the
44-
real `src/msgraph/` and real skills; placeholders (`{{NAME}}`/`{{DESCRIPTION}}`) all filled.
43+
- [ ] **Two-tier layout.** Shippable payload lives under `plugin/` (`plugin/.claude-plugin/plugin.json`,
44+
`plugin/skills/`, `plugin/src/msgraph/`); the root carries `.claude-plugin/marketplace.json`
45+
whose plugin `source` resolves to `./plugin`.
46+
- [ ] **Template residue gone.** `plugin/src/example/` and `plugin/skills/example-subject-verb/`
47+
replaced by the real `plugin/src/msgraph/` and real skills; placeholders
48+
(`{{NAME}}`/`{{DESCRIPTION}}`) all filled.
4549
- [ ] **Docs honest.** `README.md` reflects the shipped verbs; `auth-login` documents the one-time
4650
Azure app-registration prerequisite and the `MSGRAPH_CLIENT_ID`/`MSGRAPH_TENANT_ID` env vars.
4751
- [ ] **SDD trail stays local.** `.specify/`, `specs/`, `.tredl/` remain gitignored; the public

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,17 @@ with no supply-chain surface beyond the standard library.
3232

3333
## Layout
3434

35+
Two-tier: `plugin/` is the shippable payload; the repo root is the build/distribution repo.
36+
3537
```
36-
.claude-plugin/plugin.json # manifest
37-
skills/<subject>-<verb>/SKILL.md # agent-facing commands (auth, mail-read, rule-*)
38-
src/msgraph/client.py # stdlib kernel — importable + runnable, exposes a `describe` catalog
39-
docs/AGENT-FRIENDLY.md # REQUIRED READING before adding/changing a skill
40-
DEFINITION_OF_DONE.md # what "working" means — the build target
41-
CLAUDE.md # grounding + build plan for a Claude Code session in this repo
38+
.claude-plugin/marketplace.json # marketplace entry — points at ./plugin
39+
plugin/.claude-plugin/plugin.json # plugin manifest
40+
plugin/skills/<subject>-<verb>/SKILL.md # agent-facing commands (auth, mail-read, rule-*)
41+
plugin/src/msgraph/client.py # stdlib kernel — importable + runnable, exposes a `describe` catalog
42+
docs/AGENT-FRIENDLY.md # REQUIRED READING before adding/changing a skill
43+
pyproject.toml tests/ .github/ # dev tooling, tests, CI/release (never shipped)
44+
DEFINITION_OF_DONE.md # what "working" means — the build target
45+
CLAUDE.md # grounding + build plan for a Claude Code session in this repo
4246
```
4347

4448
## Prerequisite (one-time, free)

0 commit comments

Comments
 (0)