API-Docker

 view release on metacpan or  search on metacpan

.claude/agents/api-docker-doc-writer.md  view on Meta::CPAN

---
name: api-docker-doc-writer
description: "Write and maintain API::Docker POD in the house format (=attr, =method, =head1 SYNOPSIS, =seealso, woven by @Author::GETTY) and keep README.md in step. Documents the surface that exists; does not change code."
model: sonnet
allowed-tools: Read, Edit, Grep, Glob
briefing:
  skills:
    - api-docker-core
    - getty-perl-release-author-getty
    - getty-perl-core
---

You are the api-docker-doc-writer for **API::Docker**.

Document the surface as it exists. If the code and the documentation disagree, the code
wins and the disagreement is a finding you report — you do not change behavior to match
prose. The conventions above are non-negotiable — apply silently, do not restate.

## What this distribution's POD looks like

POD is interleaved with the code, each `=attr`/`=method` block directly after the
`has`/`sub` it documents, and every class ends with `=seealso`. Option lists are `=over`
blocks with one `=item * C<name> - meaning` per accepted key — mirror the method's own
`%params`/`%opts` handling, including the defaults it applies (`rm` defaults to true in
`build`, `tag` to `latest` in `pull`).

.claude/agents/api-docker-engine-worker.md  view on Meta::CPAN

You are the api-docker-engine-worker for **API::Docker**.

Your lane is the boundary between this distribution and the Docker Engine: what the
daemon accepts, what it answers, and whether this client models that faithfully.
Everything that is a Perl or packaging question — Moo structure, the socket and chunked
reader, refactoring, `cpanfile`, dist plumbing — belongs to `api-docker-worker`; hand it
over rather than drifting into it. The conventions above are non-negotiable — apply
silently, do not restate.

Coordinate via `karr`: pick tickets from the local board, record drift you find as new
tickets rather than expanding scope mid-change.

## What the client currently does not model

Verified against the rootless Podman socket on this machine, not deduced:

- **`containers->logs` returns frames, unparsed.** A container created without a TTY
  produces `01 00 00 00 00 00 00 04 "OUT\n" 02 00 00 00 00 00 00 04 "ERR\n"`, and the
  method hands those header bytes to the caller as if they were log text. With
  `Tty => \1` the same container produces `"OUT\r\n" "ERR\r\n"` and looks fine — which is
  why nothing has caught it. `exec->start` with `Detach => 0` has the same problem, and
  there is no `attach` method at all.
- **`exec->start` never surfaces the exit status.** It comes from a separate
  `GET /exec/{id}/json`, which this client does expose as `exec->inspect` — a caller has
  no way to know that from the method's POD.
- **Nothing checks `errorDetail`.** `build`, `pull` and `push` return the event list and
  leave failure detection to the caller, while the HTTP status was 200.

Fixing any of these changes a public return shape. `../p5-dist-zilla-plugin-docker-api`
consumes `images->build`, `->tag`, `->push` and `->inspect` — verify it, or file a ticket
on its board, before landing.

## Working method

Measure, don't assume. The daemon is reachable at
`unix:///run/user/1000/podman/podman.sock` (Podman, API 1.41 — there is no Docker on this
machine). `curl --unix-socket <sock> http://localhost/v1.41/...` shows the raw stream
including frame headers, which is the fastest way to confirm a wire format before writing
code against it.

Podman is a reimplementation: anything beyond the documented surface — event payload
fields, healthcheck details, error message text — is unverified until you have measured
it there, and a difference from Docker is worth writing into the `Changes` entry.

New endpoint methods follow the existing shape: options normalised into `%params`,
`list`/`inspect` wrapped into entity objects, everything else returned raw, POD with an
`=item * C<name> - meaning` per accepted key. A behavior change gets a `Changes` entry
under `{{$NEXT}}` that states what was measured.

## Verification

`prove -lr t/` for the fixture suite. For live checks against the Podman socket, set
`API_DOCKER_TEST_HOST`; only add `API_DOCKER_TEST_WRITE=1` when the task genuinely needs
containers created, and clean up what you create. Never run `images->push` against a real
registry, and never `dzil release`.

.claude/agents/api-docker-release-checker.md  view on Meta::CPAN

   POD blocks from the grep, or `Path::Tiny` in the `Images` SYNOPSIS reads as a runtime
   dependency. Known state at the time of writing: `URI` is declared and used nowhere;
   `IO::Socket::INET` is loaded by `Role::HTTP` and undeclared while its `::UNIX` sibling
   is declared; `Carp` is loaded everywhere and undeclared. Say which of those are worth
   changing; do not leave them unmentioned.
2. **`$VERSION` consistency** — `grep -rn 'our \$VERSION' lib` must return the same
   literal for all 12 modules. A module carrying a stale version, or a new module with
   none, is a release blocker. The value is the *next* release; the previous one is the
   last git tag.
3. **`dist.ini`** — `[@Author::GETTY]`, `copyright_year` current.
4. **`Changes`** — a `{{$NEXT}}` section exists and covers the user-visible changes since
   the last tag (`git log --oneline $(git describe --tags --abbrev=0 2>/dev/null)..`).
   The house standard here is a measured claim, not a summary: existing entries name the
   engine's exact error string and the observed before/after. Flag an entry that asserts
   daemon behavior without evidence.
5. **POD in sync with the code.** Every public method has `=method`, every attribute
   `=attr`, and the option lists match what the method actually forwards — the drift most
   likely to ship is an option added to a `%params` block and never documented. Check
   `README.md` against `lib/API/Docker.pm`'s SYNOPSIS too.
6. **`dzil build`** — clean, no missing files, no warnings; then `dzil test` green,
   including the generated `xt/` author and release tests (pod-syntax,
   changes_has_content).
7. **`prove -lr t/`** green with no environment set — the suite must not require a
   daemon. If every file dies with exit 2 and no plan, report a missing build dependency,
   not a test failure.
8. **No Getty-authored dependency here yet.** If one appears in `cpanfile`, it must be
   pinned to its actual released CPAN version (`cpanm --info`), never to the version in
   the sibling repo's `lib/` — that one is unreleased.

Report: ready, or a concise list of what blocks release. File blockers as karr tickets on
this repo's board.

.claude/agents/api-docker-test-writer.md  view on Meta::CPAN

  ignores the route table entirely under `API_DOCKER_TEST_HOST` — an assertion tied to
  fixture contents runs against a real daemon's data otherwise.

New fixtures are captured from a real daemon into `t/fixtures/*.json`, never hand-rolled
— and that includes wire formats. A test for the multiplexed log stream asserts against
bytes the engine actually produced, not against a frame header written from memory; the
Engine API reference above tells you what to expect, the socket tells you what is true.
Test filenames follow the existing flat, topical naming (`t/images.t`,
`t/images_push_auth.t`), one file per resource or per defect.

A test asserts intent: it must be able to fail when the logic changes. Reproduce a bug
before fixing it and leave the regression behind. Verify with `prove -lr t/`; a single
file with `prove -lv t/NN.t`.

.claude/agents/api-docker-worker.md  view on Meta::CPAN

**Where your lane ends.** You own how this distribution is built: `Role::HTTP`'s socket
handling and chunked reader, Moo composition, the entity classes, `cpanfile`, dist
plumbing. You do not own what the Docker Engine accepts or answers. If the task turns on
daemon semantics — a wire format, a query-parameter meaning, a response shape, registry
auth, API version gating — stop and hand it to `api-docker-engine-worker`, which is
briefed with the Engine API reference and you are not. Guessing at daemon behavior from
the existing Perl is exactly how a wrong assumption gets cemented; the current
`containers->logs` is the proof.

Coordinate via `karr`: pick tickets from the local board, record drift you find as new
tickets rather than expanding scope mid-change.

## Repo-specific notes — beyond the briefed skills

**This distribution has a consumer in the same workspace.**
`../p5-dist-zilla-plugin-docker-api` calls into `$docker->images` and the client
constructor. Changing a public method signature, a return type, or what `_request` hands
back is a cross-repo change: check that repo builds and tests green, or file a ticket on
its board before landing the change here.

**`our $VERSION` is repeated in all 12 `.pm` files and must stay identical.** That is the
house shape here (`[@Git::VersionManager]` allows `^lib/.*\.pm$` to be dirty in the
version-bump commit) — a new module gets the same literal as the rest. The value is the
*next* release; `dzil release` bumps it, never you by hand.

**Behavior changes get a `Changes` entry under `{{$NEXT}}`, and the entry says what was
measured.** The existing entries name the exact engine error string and what a local
registry did before and after. Match that standard: a claim about the daemon's behavior
is worth writing down only if you observed it.

POD lives next to the code (`=attr`, `=method`, `=head1`), woven by `@Author::GETTY`.
Touch a public signature, touch its POD in the same change.

## Verification

`prove -lr t/` — recursive, so a subdirectory added under `t/` later is not silently
skipped. Fixture-driven, no daemon needed, and it must stay that way.

Against a real daemon: there is no Docker on this machine, only rootless Podman at
`API_DOCKER_TEST_HOST=unix:///run/user/1000/podman/podman.sock`; add
`API_DOCKER_TEST_WRITE=1` for the mutating tests, which create and remove real
containers, images and volumes. Run them only when the task is about live behavior.

.claude/rules/api-docker-rules.md  view on Meta::CPAN

Apply to every task in this distribution unless explicitly overridden. Bias: caution over
speed on non-trivial work; use judgment on trivial tasks. Loaded automatically at launch
(same priority as `CLAUDE.md`). Subagents get their discipline from the skills
force-loaded via `briefing.skills` — this file is for the orchestrating agent.

## Engineering discipline

1. **Think before coding** — state assumptions; when uncertain, ask rather than guess.
   Push back when a simpler approach exists.
2. **Simplicity first** — minimum code that solves the problem. Nothing speculative.
3. **Surgical changes** — touch only what you must. Match existing style.
4. **Goal-driven execution** — define success criteria, loop until verified.
5. **Surface conflicts, don't average them** — pick one (more recent / more tested), flag
   the other for cleanup. Don't blend.
6. **Read before you write** — `Role::HTTP` is the single seam every resource API and
   entity class hangs off. A change to `_request`'s options, return shape or error
   handling reaches all twelve modules and the mock harness at once.
7. **Tests verify intent, not just behavior** — a test that can't fail when the logic
   changes is wrong, and a helper that normalises its input before asserting is that
   test. Reproduce a bug before fixing it; leave the regression behind.
8. **Checkpoint after every significant step** — summarize: done / verified / left.
9. **Match conventions** — conformance > taste. Surface a harmful convention; don't fork
   silently.
10. **Fail loud** — "Done" is wrong if anything was skipped. "Tests pass" is wrong if any
    were skipped — and in this repo a skip is the default failure mode, see below.
11. **A red test is a claim before it is a failure** — before changing code to turn a
    test green, say what the test asserts and whether your fix keeps that claim or
    replaces it. If the claim is wrong, fix the claim and say so.

.claude/rules/api-docker-rules.md  view on Meta::CPAN

surface, entity wrappers, request/response encoding, error handling, `cpanfile`, and
tests. Pure prose docs and `Changes` notes are not.

## Coordination — karr board (always in scope)

Ticket coordination is the orchestrating agent's job, so `karr` is always in scope —
don't invoke the `kanban-issues-karr-cli` skill first, just use it. Git-native kanban;
state lives in `refs/karr/*`; one board, this repo. Day-to-day: `karr list --compact` /
`karr board` for open work; `karr show ID` for detail; `karr create/edit/move/handoff`
for the usual flow; mutating commands auto-sync. Use it for drift to reconcile and
follow-up work that must not block the current change. Full surface: skill
`kanban-issues-karr-cli`.

**Serialize board mutations when fanning out.** Keep implementation work parallel if you
like, but collect results and loop `karr move`/`handoff`/`sync` sequentially — N landing
at once is a resource event, not a cheap command.

## Release — never without permission

`dzil build`, `dzil test` and `prove -lr t/` are fine anytime. `dzil release` and any
CPAN upload are STRICTLY forbidden without the maintainer's explicit go-ahead — even if

.claude/rules/api-docker-rules.md  view on Meta::CPAN

- **The suite is not green against a live daemon.** `t/system.t`'s `events` subtest
  asserts `ref eq 'ARRAY'` outside its `is_live()` guard; a real daemon with no events in
  the requested window returns an empty body, `_request` returns undef, and the test
  fails. It is ticketed — do not rediscover it as a new finding and do not fix it
  opportunistically.
- **`containers->logs` hands frame headers to the caller.** A container created without
  a TTY produces an 8-byte-framed stream (`01 00 00 00 00 00 00 04` + payload, stream
  type in byte 0, big-endian length in bytes 4-7) and the method returns it undecoded;
  with a TTY the stream is raw and looks correct, so hand-testing interactively hides it.
  `exec->start` shares the problem and there is no `attach` at all. Ticketed — it is a
  public return-shape change, not a passing fix.
- **Live write tests mutate the real engine.** `API_DOCKER_TEST_WRITE=1` creates and
  removes actual containers, images, networks and volumes; cleanup runs in an `END`
  block, so an interrupted run leaves them behind. Run only when the task is about live
  behavior.
- **`images->push` publishes.** With credentials it writes to a real registry under the
  maintainer's account. Never run it — nor any test that does — without explicit
  instruction.
- **Streaming endpoints block until the daemon closes.** `_request` buffers whole
  responses, so `system->events` or `containers->stats` without a bound never returns.
  Always bound the window, and wrap manual probes in `timeout`.
- **`tls` and `cert_path` are attributes with no implementation.** `Role::HTTP` never
  reads them; `tcp://` is always plaintext. Wiring TLS is new work with an ADR-grade
  decision behind it, not a repair.
- **`../p5-dist-zilla-plugin-docker-api` consumes this API.** A public signature or
  return-shape change is a cross-repo change: verify that repo, or file a ticket on its
  board before landing.

## Perl specifics — reference, don't restate

Module loading, `$VERSION`, cpanfile pinning and house style: skills `getty-perl-core`,
`getty-perl-moo`. `[@Author::GETTY]`, POD weaving, `{{$NEXT}}`: skill
`getty-perl-release-author-getty`. dist.ini mechanics: `perl-release-dist-ini`. Commit
messages: `getty-git-commit-style`. Architecture and transport invariants:
`api-docker-core`. What the daemon itself does — wire formats, response shapes, filters,
registry auth: `docker-engine-api` (briefed only into the engine-worker and the

.claude/skills/api-docker-core/SKILL.md  view on Meta::CPAN

  raw daemon response. A new list-shaped method wraps with `_wrap_list`; a new
  `tag`/`prune`/`push`-shaped one returns what the daemon said.
- **Entities hold `client` as a `weak_ref`.** `API::Docker->new->images->list`
  leaves every returned entity with `client => undef`, and the next
  `$image->remove` dies on an undefined invocant. The client must stay in a
  live variable — in library code, in examples, and in tests.
- **Query-string booleans are normalised to `1`/`0`** (`$opts{all} ? 1 : 0`);
  **JSON-body booleans are `\1`/`\0`** (see `Exec::start`), because the engine
  type-checks the body but not the query string.
- **A `params` value that is a hashref is JSON-encoded automatically.** Pass
  `filters => { dangling => ['true'] }` through unchanged; encoding it by hand
  double-encodes it.
- **Extra headers go through `headers =>`**, which strips CR/LF. Never
  concatenate a header into the request string.
- `_uri_encode` deliberately leaves `/` and `:` raw so image names survive in
  the path (`/images/raudssus/karr:user/push`).
- `sub push` and `sub kill` shadow Perl builtins inside their packages — that
  is why `namespace::clean` is loaded; always call them as methods.

## X-Registry-Auth — padded base64url, always sent

.claude/skills/docker-engine-api/SKILL.md  view on Meta::CPAN


`exec` is two calls: `POST /containers/{id}/exec` creates the instance and
returns an `Id`, `POST /exec/{id}/start` runs it. The exit status comes from
`GET /exec/{id}/json` afterwards (`ExitCode`), never from the start call.

## Other engines

Podman serves this API on a compat socket — enable with
`systemctl --user enable --now podman.socket`, reach it at
`unix://$XDG_RUNTIME_DIR/podman/podman.sock`, and it announces API 1.41.
Multi-stage builds including `target` pass through unchanged, and the frame
format above is byte-identical. It is a reimplementation, not Docker: treat
anything beyond the documented surface — event payload details, healthcheck
fields, error message text — as unverified until measured against the engine
you actually target.

Clients differ in how they *find* the daemon: `DOCKER_HOST` is the one
mechanism all of them honour. Docker contexts
(`~/.docker/config.json` `currentContext` plus
`~/.docker/contexts/meta/*/meta.json`) are resolved by the `docker` CLI and
docker-java but not by most library clients, so "works in the terminal, fails

.claude/skills/getty-git-commit-style/SKILL.md  view on Meta::CPAN

description: Use when writing or amending a commit message in a Getty repository, including a commit that spans several repos.
---

# Commit Message Style

## Format

```
<summary line — imperative, max ~72 chars>

<body — one line per change, no filler>

Co-Authored-By: Claude <Model> <noreply@anthropic.com>
```

## Rules

- **Summary line**: imperative mood ("Add", "Fix", "Rename", "Remove"), describe the primary intent
- **Body**: list every discrete change on its own line, no bullets needed, no prose explanations
- **Completeness**: every file-level change must be mentioned — don't silently lump things together
- **Brevity**: state what changed, not why it's important or how it works — the diff shows that
- **No filler**: no "This commit...", no "In this change...", no "Also..."
- **No @ symbols**: never use `@` in commit messages (e.g. write `[DBIO]` not `[@DBIO]`) — platforms like GitHub interpret `@word` as user/org mentions
- **Language**: English
- **Co-Author**: append the Co-Authored-By line whenever Claude wrote or co-wrote the changes.
  Name the model that actually did the work — read it from the session rather than
  copying a version out of an older commit or out of this file. If the harness
  prescribes its own trailers (a session link, a different spelling), use those verbatim.

## Examples

Good:
```
Rename _dbic_connect_attributes to _dbio_connect_attributes

.claude/skills/getty-perl-core/SKILL.md  view on Meta::CPAN

- **`requires 'Module::Name';`** — the version argument is optional, omit it when unpinned. Never write `'0'`.
- **A version means "or higher".** `requires 'Foo', '5.0';` already accepts 5.1 — never write `'>= 5.0'`.
- **Alphabetical order**, phase blocks (`on test => sub {...}`) at the end.

### Getty-authored dependencies — CRITICAL

Getty's `dist.ini` uses `[@Author::GETTY]`, which sets `$VERSION` in the repo to the **next, unreleased** version (`0.402` while CPAN is at `0.401`). The repo is ALWAYS one ahead of CPAN.

1. **Pin what the code actually needs, not the number that happened to be in front of you.** Opening a sibling repo, reading its `$VERSION` and pinning that — while depending on nothing that version introduced — is the mistake this section exists...
2. **Check `cpanm --info Module::Name`** for the released version. If the released one carries what you use, that is the pin.
3. **Pinning the next, unreleased version is correct when the change spans both repos** — the sibling gained what this code calls, or a release is being prepared and the distributions are tested together from their working trees. It commits you to ...
4. **Pin every Getty-authored distribution.** Not stale, not omitted — current.
5. **Re-check on upgrade.**

```bash
cpanm --info Module::Name | tail -1
# → GETTY/Module-Name-1.234.tar.gz  ← pin to 1.234
```

Getty-authored (non-exhaustive): `Langertha`, `IO::K8s`, `Kubernetes::REST`, `WWW::Crawl4AI`, `Net::Async::Crawl4AI`, `Net::Async::WebSearch`, `Catalyst::Plugin::ChainedURI`, `Locale::Simple`, `DBIO::*`, `WWW::Zitadel`, `WWW::PayPal`, `WWW::Chain`.

## Changelog (the Changes file)

Every distribution ships a `Changes` file with a `{{$NEXT}}` token at the top (Dist::Zilla's `[NextRelease]` fills it at release time).

- **Add a bullet under `{{$NEXT}}` in the SAME commit as any user-facing change** — new bindings, behaviour changes, bug fixes, deprecations. If a CPAN consumer would notice, it belongs there.
- **Match the existing style:** two-space indent, `  - ` bullets, wrap near 78 columns, present-tense imperative ("New binding X", "Fix Y on macOS").
- **Skip pure dev-tooling noise** — skill hardlinks, editor config, internal CI refactors. A CI fix that unbreaks the build for everyone IS worth a line.
- **Never hand-edit the version line or timestamp** — `[NextRelease]` owns those.

## Forbidden

❌ `require Foo` inside a method to "speed up startup" · ❌ a Getty repo's `$VERSION` as a cpanfile requirement · ❌ `'0'` or `'>= x'` as a version argument · ❌ `default => sub {...}` for a non-trivial attribute default · ❌ 4-space indent ...

## When in doubt

.claude/skills/getty-perl-moo/SKILL.md  view on Meta::CPAN

);
```

**Use `Sub::HandlesVia` over `MooX::HandlesVia`** – the latter documents that triggers/coercions don't fire on delegated mutations. Load `Sub::HandlesVia` *after* `use Moo`.

---

## Pattern 7 – Method Modifiers

```perl
before calc => sub { die "x<0" if $_[1] < 0 };         # validate, can't change return

around calc => sub {
  my ($orig, $self, $x) = @_;
  return $self->$orig($x) + 1;                        # can change return value
};

after calc => sub { ... };                               # side-effects, logging
```

**Rules:** `before`/`after` cannot alter return value; `around` can. Always forward `@_` correctly in `around`. Multiple modifiers from multiple roles stack – order is composition-order-sensitive.

---

## Pattern 8 – Attribute Options Cheatsheet

.claude/skills/getty-perl-release-author-getty/SKILL.md  view on Meta::CPAN

Every file the bundle processes carries `# ABSTRACT: <one line>` directly under
`package`, before any `use`; executables in `bin/` carry it under the shebang.
PodWeaver turns it into the NAME section. Outside a bundle-managed distribution
the line does nothing — do not scatter it into files Dist::Zilla never sees.

## @Author::GETTY Options

### Feature Toggles (Boolean)
- `no_cpan` - Skip UploadToCPAN; also defaults `version_finder` to `:MainModule`
- `no_podweaver` - Skip PodWeaver
- `no_changes` - Skip NextRelease
- `no_installrelease` - Skip InstallRelease
- `no_makemaker` - Skip MakeMaker
- `xs` - Use ModuleBuildTiny (for pure-Perl XS without Alien deps)
- `deprecated` - Add Deprecated plugin
- `adoptme` - Add x_adoptme metadata
- `no_github` - Skip GithubMeta and GitHub::CreateRelease, use Repository instead. Auto-set to 1 when `.git/config` has no github.com remote; set `no_github = 0` to force GitHub plugins on anyway
- `no_github_release` - Skip only GitHub::CreateRelease. Same auto-detection; when active, `dzil release` creates a GitHub Release and attaches the tarball, which needs `~/.github-identity` (login + token)
- `gitea` - Treat the remote host as Gitea/Forgejo (repository/bugtracker/homepage via GiteaMeta). Only needed for self-hosted instances — codeberg.org and the author's own are auto-detected. No effect when a GitHub remote exists
- `include_readme` - Ship README.md (excluded from the tarball by default)
- `no_install` - Resulting distribution can't be installed

.claude/skills/getty-perl-release-author-getty/SKILL.md  view on Meta::CPAN


**Auto-generated sections (do NOT write manually):**
NAME, VERSION, AUTHOR, SUPPORT, CONTRIBUTING, COPYRIGHT

## Versioning Convention — CRITICAL

**The version in the repository is always the NEXT release version, not the current one.**

Before a release, the files already contain the upcoming version:
- `dist.ini` or module `$VERSION` = e.g. `1.005`
- `Changes` has `{{$NEXT}}` as the placeholder for unreleased changes
- The currently released version on CPAN is `1.004`

After `dzil release` runs:
1. `{{$NEXT}}` in Changes is replaced with `1.005` + release date
2. The version is bumped to `1.006` (or next AutoVersion value)
3. A Git tag `v1.005` is created

**Do NOT treat the version in dist.ini as the released version.** If the user asks "what version is released?", check CPAN or git tags — not the current `$VERSION` in the files.

**Do NOT bump the version manually before a release** — `dzil release` handles this automatically.

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

### Cross-board dependencies

`--depends-on` is board-local. When work here cannot proceed until something is
fixed in *another repository*, that link is a cross-board dependency:

```bash
# in the other repository -- raise the card and record where it came from
karr create "Fix the API" --escalated-from home#5

# here -- record what you are waiting for, block, release the claim, leave
karr edit 5 --add-needs other-repo#7 --block "needs other-repo#7: API change first" --release

# any time -- what is this board waiting on, and is it done yet?
karr needs
karr needs --board other-repo=/srv/other-repo     # where that board is on THIS machine
karr needs --resolve                              # drop settled links, unblock what is free
```

A reference is `BOARD#ID`: the other board's **name** and a task id. Never a
path -- the card is shared state and two clones of the same fleet have
different directories. karr turns the name into a directory from

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

karr enable                                  # allow them again
karr disable --json                          # {"foundation":{"enabled":0,"reason":"…"}}
```

Board-level opt-out from `karr-foundation`. Unlike the per-machine `.karr` file
the flag is board state (`foundation.enabled` in `refs/karr/config`), so it syncs
with the board and every foundation instance on every machine honours it. A
disabled board is skipped whole: no drain, no auto-block, no agent run — the
flag wins over `karr-foundation --command`, the config's `default_command`, the
`.karr` `command` and `claude: true`, and `--force` does not override it. Nothing
else changes: the board stays fully usable by hand (`karr list`, `karr pick`,
`karr move`, …). Use it for a repository whose backlog is parked rather than
abandoned.

`karr disable` without `--reason` clears any previously stored reason. The same
state is readable and writable through `karr config`:

```bash
karr config get foundation.enabled           # -> 0 or 1
karr config set foundation.enabled false     # true/false, yes/no, on/off, 1/0
karr config set foundation.reason "why"

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

access to Git config or agent skill directories.

### Sync

```bash
karr sync
karr sync --pull
karr sync --push
```

Use this when you want explicit control over board ref exchange with the remote
instead of relying only on the implicit pull/push behavior of mutating
commands.

`karr sync` also carries `refs/karr-foundation/*` — karr-foundation's shared
chain, run logs, question mailbox and design documents — in the same run,
after the board and never on its own. One command on purpose: a separate one
would be a second thing to remember, and a coordination namespace nobody
synced fails quietly. Mutating commands still sync the board only, so this
costs nothing outside an explicitly typed `karr sync`, and a repository
holding nothing under `refs/karr-foundation/` pushes nothing there. Deletions

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

The board lives in `refs/karr/*`. `materialize` writes a file view of it for
grepping or for kanban-md to read; `import` reads such a directory back in.
The `tasks/` directory is always gitignored and is never the source of truth —
losing it costs nothing, editing it costs nothing until you `import`.
`materialize` refuses to write over paths the project itself tracks in git,
which is what `--force` overrides.

### Repair an old board

```bash
karr repair                                  # report what would change
karr repair --yes                            # migrate
```

Boards written by karr 0.402 or earlier stored UTF-8 double-encoded. Such a
board is detected on read and repaired on the fly, so nothing is broken in the
meantime; this migrates the stored refs once so the workaround stops being
needed. A board created by a later version needs nothing here and says so.

The same command also raises a `started` stamp that precedes its own card's
`created` up to that `created` — karr wrote `started` as a bare date until

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

```

Stores and retrieves helper payloads in Git refs outside protected namespaces
such as `refs/karr/*`, branches, and tags. Use this for shared planning blobs,
agent scratch data, or similar workflow artifacts that should sync through Git
without becoming task cards.

The arguments after the ref are joined with a single space, so they are a
one-line payload. A document goes in on stdin instead — with no content
argument at all, `karr set-refs REF < file` stores the file verbatim and
`karr get-refs REF > file` gives it back unchanged.

### Activity log

```bash
karr log                                     # last 20 entries
karr log --agent swift-fox                   # filter by agent
karr log --task 5                            # filter by task
karr log --last 50 --json                    # more entries, JSON
karr log --compact                           # one line per entry, no padding
```

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

1. **Need a board?** → `karr init`
2. **New work item?** → `karr create "Title" --priority high`
3. **What's on the board?** → `karr board` or `karr list`
4. **Starting work?** → `karr pick --claim NAME --move in-progress`
5. **Done with task, hand to review?** → `karr handoff ID --claim NAME --note "reason"`
6. **Done with task, close it?** → `karr edit ID --release && karr move ID done`
7. **Blocked?** → `karr edit ID --block "reason"`
8. **Need details?** → `karr show ID`
9. **Soft-delete?** → `karr archive ID`
10. **Board snapshot for agent context?** → `karr context --write-to AGENTS.md`
11. **Check/change config?** → `karr config` / `karr config set KEY VALUE`
12. **Install agent skills?** → `karr skill install`
13. **Need a full board snapshot?** → `karr backup` / `karr restore --yes`
14. **Need shared non-task workflow data?** → `karr set-refs` / `karr get-refs`
15. **Board should never be drained by an automation host?** → `karr disable --reason "why"`
16. **Need to remove the board completely?** → `karr destroy --yes`
17. **Overview of every board under a directory?** → `karr dashboard`

## Multi-agent workflow

```bash

CLAUDE.md  view on Meta::CPAN

10. **Pin every Getty-authored dependency** to its latest released CPAN
    version in `cpanfile`.

11. **The version in `lib/API/Docker.pm` is the NEXT release.** What's
    currently on CPAN is the previous tag. `dzil release` bumps the
    version automatically — never bump it by hand before a release. The
    same literal is repeated in all twelve `lib/**/*.pm` files and must
    stay in sync.

12. **`{{$NEXT}}` in `Changes` is the placeholder for the upcoming
    release.** Add entries under it as you change behavior; `dzil
    release` replaces it with the version + timestamp.

## What this distribution is

A pure-Perl client for the Docker Engine API. No LWP, no shell-outs —
HTTP/1.1 (incl. chunked) is spoken directly over the daemon's Unix
socket (default) or a TCP endpoint. Any engine serving that API works;
Podman needs nothing but `DOCKER_HOST`.

The synchronous `_request` core lives in

CLAUDE.md  view on Meta::CPAN

is reused by `../p5-dist-zilla-plugin-docker-api`).

Ticket coordination runs on the repo's `karr` board (`karr board`).

## When changing behavior

- Add a `Changes` entry under `{{$NEXT}}`, and say what was measured.
- Update the POD on the affected class. POD lives next to the code
  (`=method`, `=attr`, `=head1 SYNOPSIS` ...) and is woven by the
  `@Author::GETTY` bundle.
- If you change a public method signature or a return shape, check that
  callers in the workspace (notably `../p5-dist-zilla-plugin-docker-api`)
  still build and test green.

Changes  view on Meta::CPAN

    `RUN exit 7` answers `200 OK` and ends the stream with
    `{"errorDetail":{"message":"building at STEP \"RUN exit 7\": while
    running runtime: exit status 7\n"},"error":"..."}`, the flat `error`
    key carrying the same text. Callers that never scanned for it -- the
    documented shape until now -- reported a broken build as a success.
    The exception is an `API::Docker::Error::Stream`, a new class whose
    whole purpose is that the progress output is not lost with the
    return value: `$err->events` is the complete event list, error event
    included. It overloads stringification and produces exactly what the
    plain croak it replaces produced, reason plus Carp's ` at FILE line
    N.` suffix, so existing eval-and-inspect-$@ code needs no change;
    `$@ =~ s/...//` on it yields a plain string as it would for any
    overloaded object. The trigger is the `errorDetail` key alone, never
    the word "error" in payload text.
    Which of the three actually takes that route depends on the engine,
    and Podman is not Docker here -- measured on the same socket, all
    three cases: only `build` answers 200 with the failure in the
    stream. A pull of a missing repository answers `403 Forbidden` with
    `{"message":"denied: requested access to the resource is denied"}`,
    a missing tag answers `404 Not Found` with `{"message":"manifest
    unknown: manifest unknown"}`, and a push to an unreachable registry

Changes  view on Meta::CPAN

    `API::Docker::Role::HTTP::stream_frames`.
    `exec->start` also gained POD saying where the exit status actually
    comes from -- `exec->inspect($id)->{ExitCode}`, a separate call --
    which the method's documentation never mentioned.
  - `images->build`, `->pull` and `->push` now always return an ArrayRef
    of events. `_request` used to try `decode_json` on the whole body
    first and only fall back to line-by-line parsing, so a stream that
    carried exactly one JSON object came back as a HashRef while a
    multi-event stream came back as an ArrayRef, and every caller had to
    check `ref` before iterating. Measured on Podman: `POST /build?q=1`
    emits exactly one object, which is the case that used to change
    shape. The ordinary single-JSON-object endpoints (`/version`,
    `/containers/{id}/json`, ...) are untouched and still return a
    HashRef -- the streaming behaviour is now requested explicitly with
    the new `ndjson => 1` transport option rather than guessed from the
    body. The option is named for the format and not `stream`, which is
    already a query parameter of `/events` and
    `/containers/{id}/stats`.
    `system->events` takes the same option. It was reaching an ArrayRef
    only through the implicit fallback that has now gone, so without it
    the endpoint would have quietly started returning an undecoded

LICENSE  view on Meta::CPAN

 Copyright (C) 1989 Free Software Foundation, Inc.
                    <https://fsf.org/>

 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The license agreements of most software companies try to keep users
at the mercy of those companies.  By contrast, our General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users.  The
General Public License applies to the Free Software Foundation's
software and to any other program whose authors commit to using it.
You can use it for your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Specifically, the General Public License is designed to make
sure that you have the freedom to give away or sell copies of free
software, that you receive source code or can get it if you want it,
that you can change the software or use pieces of it in new free
programs; and that you know you can do these things.

  To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

  For example, if you distribute copies of a such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have.  You must make sure that they, too, receive or can get the

LICENSE  view on Meta::CPAN

General Public License and to the absence of any warranty; and give any
other recipients of the Program a copy of this General Public License
along with the Program.  You may charge a fee for the physical act of
transferring a copy.

  2. You may modify your copy or copies of the Program or any portion of
it, and copy and distribute such modifications under the terms of Paragraph
1 above, provided that you also do the following:

    a) cause the modified files to carry prominent notices stating that
    you changed the files and the date of any change; and

    b) cause the whole of any work that you distribute or publish, that
    in whole or in part contains the Program or any part thereof, either
    with or without modifications, to be licensed at no charge to all
    third parties under the terms of this General Public License (except
    that you may choose to grant warranty protection to some or all
    third parties, at your option).

    c) If the modified program normally reads commands interactively when
    run, you must cause it, when started running for such interactive use
    in the simplest and most usual way, to print or display an
    announcement including an appropriate copyright notice and a notice
    that there is no warranty (or else, saying that you provide a
    warranty) and that users may redistribute the program under these
    conditions, and telling the user how to view a copy of this General
    Public License.

    d) You may charge a fee for the physical act of transferring a
    copy, and you may at your option offer warranty protection in
    exchange for a fee.

Mere aggregation of another independent work with the Program (or its
derivative) on a volume of a storage or distribution medium does not bring
the other work under the scope of these terms.

  3. You may copy and distribute the Program (or a portion or derivative of
it, under Paragraph 2) in object code or executable form under the terms of
Paragraphs 1 and 2 above provided that you also do one of the following:

    a) accompany it with the complete corresponding machine-readable

LICENSE  view on Meta::CPAN

YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

                     END OF TERMS AND CONDITIONS

        Appendix: How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to humanity, the best way to achieve this is to make it
free software which everyone can redistribute and change under these
terms.

  To do so, attach the following notices to the program.  It is safest to
attach them to the start of each source file to most effectively convey
the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.

    <one line to give the program's name and a brief idea of what it does.>
    Copyright (C) 19yy  <name of author>

LICENSE  view on Meta::CPAN


1. You may make and give away verbatim copies of the source form of the
Standard Version of this Package without restriction, provided that you
duplicate all of the original copyright notices and associated disclaimers.

2. You may apply bug fixes, portability fixes and other modifications
derived from the Public Domain or from the Copyright Holder.  A Package
modified in such a way shall still be considered the Standard Version.

3. You may otherwise modify your copy of this Package in any way, provided
that you insert a prominent notice in each changed file stating how and
when you changed that file, and provided that you do at least ONE of the
following:

    a) place your modifications in the Public Domain or otherwise make them
    Freely Available, such as by posting said modifications to Usenet or
    an equivalent medium, or placing the modifications on a major archive
    site such as uunet.uu.net, or by allowing the Copyright Holder to include
    your modifications in the Standard Version of the Package.

    b) use the modified Package only within your corporation or organization.

LICENSE  view on Meta::CPAN

neither be construed as a distribution of this Package nor shall it
fall under the restrictions of Paragraphs 3 and 4, provided that you do
not represent such an executable image as a Standard Version of this
Package.

7. C subroutines (or comparably compiled subroutines in other
languages) supplied by you and linked into this Package in order to
emulate subroutines and variables of the language defined by this
Package shall not be considered part of this Package, but are the
equivalent of input as in Paragraph 6, provided these subroutines do
not change the language in any way that would cause it to fail the
regression tests for the language.

8. Aggregation of this Package with a commercial distribution is always
permitted provided that the use of this Package is embedded; that is,
when no overt attempt is made to make this Package's interfaces visible
to the end user of the commercial distribution.  Such use shall not be
construed as a distribution of this Package.

9. The name of the Copyright Holder may not be used to endorse or promote
products derived from this software without specific prior written permission.

MANIFEST  view on Meta::CPAN

t/fixtures/images_pull_stream.ndjson
t/fixtures/networks_list.json
t/fixtures/system_events_stream.ndjson
t/fixtures/system_info.json
t/fixtures/system_version.json
t/fixtures/volumes_list.json
t/images.t
t/images_push_auth.t
t/lib/Test/API/Docker/Mock.pm
t/networks.t
t/release-changes_has_content.t
t/role_http.t
t/stream_error.t
t/stream_frames.t
t/streaming_shape.t
t/system.t
t/tls.t
t/version.t
t/volumes.t

META.json  view on Meta::CPAN

               "Dist::Zilla::Plugin::Git::Commit" : {
                  "add_files_in" : [],
                  "commit_msg" : "v%V%n%n%c",
                  "signoff" : 0
               },
               "Dist::Zilla::Role::Git::DirtyFiles" : {
                  "allow_dirty" : [
                     "Changes"
                  ],
                  "allow_dirty_match" : [],
                  "changelog" : "Changes"
               },
               "Dist::Zilla::Role::Git::Repo" : {
                  "git_version" : "2.47.3",
                  "repo_root" : "."
               },
               "Dist::Zilla::Role::Git::StringFormatter" : {
                  "time_zone" : "local"
               }
            },
            "name" : "@Author::GETTY/@Git::VersionManager/release snapshot",
            "version" : "2.052"
         },
         {
            "class" : "Dist::Zilla::Plugin::Git::Tag",
            "config" : {
               "Dist::Zilla::Plugin::Git::Tag" : {
                  "branch" : null,
                  "changelog" : "Changes",
                  "signed" : 0,
                  "tag" : "0.003",
                  "tag_format" : "%v",
                  "tag_message" : "v%V"
               },
               "Dist::Zilla::Role::Git::Repo" : {
                  "git_version" : "2.47.3",
                  "repo_root" : "."
               },
               "Dist::Zilla::Role::Git::StringFormatter" : {

META.json  view on Meta::CPAN

               "Dist::Zilla::Role::Git::DirtyFiles" : {
                  "allow_dirty" : [
                     "Build.PL",
                     "Changes",
                     "Makefile.PL"
                  ],
                  "allow_dirty_match" : [
                     "(?^:^bin/)",
                     "(?^:^lib/.*\\.pm$)"
                  ],
                  "changelog" : "Changes"
               },
               "Dist::Zilla::Role::Git::Repo" : {
                  "git_version" : "2.47.3",
                  "repo_root" : "."
               },
               "Dist::Zilla::Role::Git::StringFormatter" : {
                  "time_zone" : "local"
               }
            },
            "name" : "@Author::GETTY/@Git::VersionManager/post-release commit",

META.yml  view on Meta::CPAN

      class: Dist::Zilla::Plugin::Git::Commit
      config:
        Dist::Zilla::Plugin::Git::Commit:
          add_files_in: []
          commit_msg: v%V%n%n%c
          signoff: 0
        Dist::Zilla::Role::Git::DirtyFiles:
          allow_dirty:
            - Changes
          allow_dirty_match: []
          changelog: Changes
        Dist::Zilla::Role::Git::Repo:
          git_version: 2.47.3
          repo_root: .
        Dist::Zilla::Role::Git::StringFormatter:
          time_zone: local
      name: '@Author::GETTY/@Git::VersionManager/release snapshot'
      version: '2.052'
    -
      class: Dist::Zilla::Plugin::Git::Tag
      config:
        Dist::Zilla::Plugin::Git::Tag:
          branch: ~
          changelog: Changes
          signed: 0
          tag: '0.003'
          tag_format: '%v'
          tag_message: v%V
        Dist::Zilla::Role::Git::Repo:
          git_version: 2.47.3
          repo_root: .
        Dist::Zilla::Role::Git::StringFormatter:
          time_zone: local
      name: '@Author::GETTY/@Git::VersionManager/Git::Tag'

META.yml  view on Meta::CPAN

          commit_msg: 'increment $VERSION after %v release'
          signoff: 0
        Dist::Zilla::Role::Git::DirtyFiles:
          allow_dirty:
            - Build.PL
            - Changes
            - Makefile.PL
          allow_dirty_match:
            - (?^:^bin/)
            - (?^:^lib/.*\.pm$)
          changelog: Changes
        Dist::Zilla::Role::Git::Repo:
          git_version: 2.47.3
          repo_root: .
        Dist::Zilla::Role::Git::StringFormatter:
          time_zone: local
      name: '@Author::GETTY/@Git::VersionManager/post-release commit'
      version: '2.052'
    -
      class: Dist::Zilla::Plugin::Git::Push
      config:

lib/API/Docker.pm  view on Meta::CPAN


=head2 Podman

Podman ships a Docker-compatible API service. Enable its rootless socket and
point L</host> at it:

    systemctl --user enable --now podman.socket
    export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"

The socket announces API version 1.41, which L</negotiate_version> picks up
like any other daemon. Multi-stage builds are passed through unchanged,
C<target> included, down to skipping the stages the target does not depend on.

=head2 Socket discovery

L</host> resolves in two steps and no more: C<$ENV{DOCKER_HOST}>, then
C<unix:///var/run/docker.sock>. It deliberately does B<not> read Docker
contexts. C<currentContext> in F<~/.docker/config.json> and the matching
F<~/.docker/contexts/meta/*/meta.json> are ignored, so if you switch daemons
with C<docker context use>, that choice is not picked up here. Set
C<DOCKER_HOST> explicitly instead.

lib/API/Docker.pm  view on Meta::CPAN

Docker daemon connection URL. Used as default for L</host> if not explicitly set.

Examples: C<unix:///var/run/docker.sock>, C<tcp://localhost:2375>

Also the supported way to reach a non-Docker engine such as Podman:
C<unix://$XDG_RUNTIME_DIR/podman/podman.sock>. See L</CONTAINER ENGINES>.

=item C<DOCKER_CERT_PATH>

Path to TLS certificates directory. Used as default for L</cert_path>, which
nothing reads -- this client has no TLS support at all. Setting it changes
nothing, and having it set (as machines running the C<docker> CLI usually do)
breaks nothing.

=back

=head1 SEE ALSO

=over

=item * L<API::Docker::Role::HTTP> - HTTP transport implementation

lib/API/Docker/API/Images.pm  view on Meta::CPAN


    my $events = eval { $images->build(context => $tar, t => 'myapp:latest') };
    if (my $err = $@) {
        warn "$err";               # the reason, with Carp's location suffix
        for my $event (@{ $err->events }) {   # the build output up to the failure
            print $event->{stream} if defined $event->{stream};
        }
    }

The exception stringifies to what a plain C<croak> would have produced, so
existing C<eval>-and-inspect-C<$@> code needs no change.

Options:

=over

=item * C<context> - Tar archive bytes (required)

=item * C<dockerfile> - Path to Dockerfile within the archive (default: C<Dockerfile>)

=item * C<t> - Tag for the image (e.g. C<name:tag>)

lib/API/Docker/Error/Stream.pm  view on Meta::CPAN

purely so the progress output is not lost with the failure: the complete event
list, error event included, is available through L</events>.

=head2 It is still the string it replaces

Everything else in this distribution croaks plain strings, and callers rely on
that. This class overloads stringification (with C<< fallback => 1 >>, so
comparison, concatenation, C<sprintf> and matching all work through it) and
produces exactly what C<croak> would have died with: the reason, followed by
Carp's own C< at FILE line N.> location suffix. Code written against the old
behaviour keeps working unchanged:

    eval { $docker->images->build(...) };
    if ($@) {
        (my $reason = $@) =~ s/\s+at\s+\S+\s+line\s+\d+\.?//g;   # still works
        die "no good: $@";                                        # still works
        warn $@ if $@ =~ /exit status/;                           # still works
    }

Note that a substitution B<on> C<$@> replaces the object in that scalar with a
plain string, as it would with any overloaded object, so take a copy first if

t/images_push_auth.t  view on Meta::CPAN

subtest 'identitytoken auth' => sub {
    my $auth = { identitytoken => 'tok-123', serveraddress => 'ghcr.io' };
    my $hdr = API::Docker::API::Images::_build_registry_auth_header($auth);
    is_deeply(decode_json(b64url_decode($hdr)), $auth,
        'identitytoken roundtrips');
};

subtest 'pre-encoded base64-like string passes through' => sub {
    my $pre = 'eyJ1IjoibWUifQ';
    is API::Docker::API::Images::_build_registry_auth_header($pre), $pre,
        'string passed through unchanged';
};

subtest 'push() sends X-Registry-Auth via _request' => sub {
    require API::Docker;
    my $docker = API::Docker->new(
        host        => 'unix:///dev/null',
        api_version => '1.47',
    );

    my $captured;

t/release-changes_has_content.t  view on Meta::CPAN

BEGIN {
  unless ($ENV{RELEASE_TESTING}) {
    print qq{1..0 # SKIP these tests are for release candidate testing\n};
    exit
  }
}

use Test::More tests => 2;

note 'Checking Changes';
my $changes_file = 'Changes';
my $newver = '0.003';
my $trial_token = '-TRIAL';
my $encoding = 'UTF-8';

SKIP: {
    ok(-e $changes_file, "$changes_file file exists")
        or skip 'Changes is missing', 1;

    ok(_get_changes($newver), "$changes_file has content for $newver");
}

done_testing;

sub _get_changes
{
    my $newver = shift;

    # parse changelog to find commit message
    open(my $fh, '<', $changes_file) or die "cannot open $changes_file: $!";
    my $changelog = join('', <$fh>);
    if ($encoding) {
        require Encode;
        $changelog = Encode::decode($encoding, $changelog, Encode::FB_CROAK());
    }
    close $fh;

    my @content =
        grep { /^$newver(?:$trial_token)?(?:\s+|$)/ ... /^\S/ } # from newver to un-indented
        split /\n/, $changelog;
    shift @content; # drop the version line

    # drop unindented last line and trailing blank lines
    pop @content while ( @content && $content[-1] =~ /^(?:\S|\s*$)/ );

    # return number of non-blank lines
    return scalar @content;
}

t/stream_error.t  view on Meta::CPAN

};

# ---------------------------------------------------------------------------
subtest 'the stringification contract' => sub {
  my $t = transport(load_fixture_raw('images_build_error_stream.ndjson'));
  eval { $t->images->build(context => 'tar-bytes') };
  my $err = $@;

  # Everything else in this distribution croaks strings and consumers rely on
  # it -- ../p5-dist-zilla-plugin-docker-api strips Carp's location tail off
  # $@ with a substitution. All of this has to keep working unchanged.
  like "$err", qr/ at \S+ line \d+\.?/,
    'carries Carp\'s location suffix, exactly as the plain croak it replaces';
  like $err, qr/exit status 7/, 'matches a regex without an explicit stringify';
  ok $err, 'boolean-true, so if ($@) still detects it';
  is $err . '', "$err", 'concatenation goes through the overload';
  is sprintf('%s', $err), "$err", 'so does sprintf %s';
  ok $err eq "$err", 'and string comparison';

  is $err->message . $err->location, "$err",
    'message and location are separable, and together they are the string';



( run in 3.213 seconds using v1.01-cache-2.11-cpan-0fb53d1c279 )