Skip to content

feat(ownership): rebuild the ownership plugin - #9

Merged
Danrley-Ruan-Saquetti merged 43 commits into
developfrom
feat/ownership
Oct 13, 2026
Merged

Danrley-Ruan-Saquetti merged 43 commits into
developfrom
feat/ownership

Conversation

@Danrley-Ruan-Saquetti

Copy link
Copy Markdown
Member

Summary

Rebuilds the ownership plugin from its design and implementation plan (docs/superpowers/specs/2026-10-09-ownership-plugin-design.md, docs/superpowers/plans/2026-10-09-ownership-plugin.md, tasks 1–26), and makes this repository the first one it validates.

The plugin (plugins/ownership/)

  • The ownership file, version: 1: owned, generated, ignore, include and basePath, with literal, case-sensitive and disjoint paths, plus a published JSON Schema for editor support.
  • ownership validate with three checks over the whole repository — ownership/owner-coverage, ownership/codeowners-match and ownership/generated-drift — in text, json or github formats.
  • ownership explain <path>: every rule that reaches a path and where it was declared.
  • ownership init and the /ownership:init command: a skeleton, a single owner, or a file derived from CODEOWNERS.
  • A session hook that refuses a hand edit of a generated file (declared, or marked on its first line), failing open within its five-second budget.
  • plugins blocks for other plugins, and the package as a library (resolved configuration, checks, CODEOWNERS evaluation, the generated marker).
  • Requirements versioned in English under docs/requirements/, README and CHANGELOG.

The repository

  • Root Spurfile delegating to each plugin, and a CI workflow that runs the plugin's checks and validates the repository with it.
  • .ownership.yml at the root and in the plugin declare who owns every path; CODEOWNERS agrees with them.
  • CLAUDE.md and CONTRIBUTING.md point at what exists instead of references that died; the plugin is listed in the marketplace.

Fixes from the final review — generated paths that are directories or unreadable, generators that never finish (stopped after ten minutes), submodules and nested repositories in explain, YAML aliases anywhere a value is read, line-end comparison as Git would store the file, and CODEOWNERS lines with only emails in init.

Test plan

  • ./spur check (install, plugin checks, repository validation) exits 0
  • Tests: 322 total, 318 pass, 0 fail, 4 skipped
  • Lint: 0 errors (warnings only); Prettier clean
  • ownership validate on this repository: no violations found
  • CI workflow green on this PR

🤖 Generated with Claude Code

Danrley-Ruan-Saquetti and others added 30 commits October 9, 2026 16:26
How the plugin meets its requirements specification, the rules the
specification left open and how they are settled, and what changes in the
repository around it: marketplace, toolchain, CI, dogfooding, and the fix of
CLAUDE.md and CONTRIBUTING.md.

Changelog: none — a design document changes nothing for whoever installs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Changelog: none — the plan is internal to this repository

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 15-26)

Adds explain, init, the JSON Schema, the generated-guard hook, the slash
command, the public surface and distribution tests, performance, the
documentation, the root Spurfile and CI, dogfooding and the cleanup of
CLAUDE.md and CONTRIBUTING.md.

Corrects three earlier tasks found while running the plan's code: the
deleted tracked files come from git diff-files, which keeps owner
coverage inside its budget on Windows; a fixture of the drift test
carried the marker on its own first line; a regex failed the lint. The
git add of each commit step now runs from the plugin directory.

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Translates the requirements of the rebuilt plugin and folds in the rules
the original left open: the schema URL, the case-insensitive owner lookup
in CODEOWNERS, the names and shapes of entriesForPlugin and ownersOf,
init --stdout writing the summary to stderr, the init target counted as
a repository file, and OWNERSHIP_HOST_ROOT as the hook's extra root.

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Also formats the requirements tables with Prettier so format-check passes.

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every broken relation between owned, generated and ignore paths is
collected in one run; an owned containment suggests the entries that
replace the outer path.

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…GitHub does

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…he marker

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…thub formats

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Three modes — a skeleton, a single owner, the CODEOWNERS — give a plan,
and the plan is written line by line so the same tree gives the same
bytes.

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
It never overwrites one, never picks an owner nobody named, and counts
the file it creates as a repository file, so that file is born covered.

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A corpus of valid and invalid files goes through the schema and through
the loader, and both must agree on each one.

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The generated-guard hook runs before Edit, Write, MultiEdit and
NotebookEdit. It denies a file declared in generated[], a file whose
first line carries the marker, and a Write whose content carries it. It
judges the path behind a link too, and stays silent whenever it cannot
judge.

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
It creates the file only by running ownership init, asks the mode and
the owner of each path, and never touches the CODEOWNERS.

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… ships

The package exports exactly the names other plugins build on. Tests pack
it, install the tarball, import those names from the compiled code and
run the bin; copy the plugin directory alone, install it from its
lockfile and run the hook from there; and check that nothing shipped
points inside the repository that develops it.

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…th it

Changelog: none — the toolchain of this repository is not visible to whoever installs a plugin

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The root ownership file comes from ownership init --from-codeowners; the
ownership plugin project declares its own paths in a file the root
includes. CODEOWNERS loses the lines of paths that no longer exist, and
the local Claude Code settings are ignored like any per-machine file.

Changelog: none — who owns the paths of this repository is not visible to whoever installs a plugin

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Changelog: none — how this repository is developed is not visible to whoever installs a plugin

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d plugin aliases

init --from-codeowners declares only an @handle as the owner and notes
an email; generated-drift ignores the line-end style of a checkout; a
YAML alias inside a plugin block is resolved before it reaches the plugin.

Changelog: none — the plugin is not published yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Danrley-Ruan-Saquetti and others added 12 commits October 13, 2026 10:52
… be read

A declared generated path that named a directory, or a file that could not be
read, threw out of the check: Node printed a stack trace and exited with 1, the
same code as a run with findings, so CI could not tell a crash from drift. Both
are now findings on the generated entry, naming the path and the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
explain added a trailing slash to anything that is a directory on disk, but
Git lists a submodule, a nested repository and a link to a directory as one
file. The path then missed its owned entry and was reported as excluded by a
.gitignore. A path the repository lists as a file now keeps no slash, and a
link is no longer followed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An alias was resolved only inside a plugin block. A block, a plugins map, an
owned entry or a single value given by an alias failed the type checks with
"must be a map; got *name". Every value now goes through one place that
resolves the alias before its type is checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The requirements now say plainly that only the exact marker counts and that
case is the only variation; a test pins that spacing variants are not the
marker. The code already behaved this way.

Changelog: none — the behaviour does not change; only the requirement wording and a test are added

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A generator that hung kept validate, and CI with it, waiting forever with no
finding. A command still running after ten minutes is now stopped together
with every process it started, and reported as a failed command that names
it. The limit is fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hook ends itself with a timer, but a timer cannot fire during a
synchronous git call, so on a machine without a timeout command a slow or
hung git held every edit for as long as git took. git now gets the remaining
budget as its timeout; when it runs out, the hook stays silent and lets the
edit through, as it does for any defect of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntry owner

An ownership file accepts only an owner that starts with @, so init declares
the first such owner on the line and notes the others, emails included. The
requirements now state that rule.

Changelog: none — init already behaved this way; only the requirements change

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
init --from-codeowners dropped a path whose line lists only emails without a
word: it was only counted among the files without an owner. It now appears in
the commented "Left out" block and in the output, once at the level an entry
would have had, with the reason that an owner must start with @.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stripping every CR before comparing hid real drift: a file kept with CRLF on
purpose against an LF output, or a generator that starts printing CRLF where
Git keeps it. The output and the work-tree file are now hashed with git
hash-object at the declared path, so the .gitattributes and core.autocrlf
conversions decide, and an uncommitted edit is still drift. A failure of Git
to hash is a finding with the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hangs

The five-second timer could not fire while the hook blocked the event loop
on synchronous file system work (realpath, loading the ownership files,
reading the marker), so a slow or hung file system, such as a network
drive, could hold an edit indefinitely. The judgement now runs in a worker
thread; the main thread stays free, and past the budget the hook ends
silent with 0 and lets the edit through. The git deadline stays inside the
worker so a hung git does not outlive the hook.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
plural, the file:line location of an origin and the relative-to-root check
each lived in several places, so a fix to one copy had to be repeated in
the others. Each now lives in one module. isDirectory stays separate on
purpose: explain must not follow a symbolic link, init must.

Changelog: none — internal refactor that shares duplicated helpers; no behaviour changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Danrley-Ruan-Saquetti
Danrley-Ruan-Saquetti requested a review from a team as a code owner October 13, 2026 18:18
On POSIX the process group was signalled and the check returned at once.
The signal is delivered asynchronously and the shell dies before the
generator it started, so the generator could still be running when it
was reported as stopped. Wait, bounded, until the group is gone.

Changelog: none — the Unreleased entry on stopping a generator that never finishes already describes this behavior.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Danrley-Ruan-Saquetti
Danrley-Ruan-Saquetti merged commit 193e2a3 into develop Oct 13, 2026
1 check passed
@Danrley-Ruan-Saquetti
Danrley-Ruan-Saquetti deleted the feat/ownership branch October 13, 2026 18:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant