Files
pad/plugin
xarmian d8627968d0 feat(plugin): pad Claude Code plugin — nested layout, monitors, skills (PLAN-2469 Phase 2) (TASK-2534) (#1085)
* feat(plugin): assemble Phase-0 scaffold v3 into a nested plugin layout — PLAN-2469 Phase 2 (TASK-2534)

Drops scaffold v3 (plugin.json, marketplace.json, monitors.json, the pad/
status/capture/onboard skills) in under plugin/, verbatim, rather than at
repo root. Repo root already embeds skills/pad/SKILL.md into the binary
as the surface-agnostic `pad agent install` source (embed.go); loading
the plugin flat at repo root would register BOTH that copy and the
plugin's Claude-Code-only trimmed copy as separate skills in the same
session (confirmed empirically via --plugin-dir) and would ship the
whole repo as the marketplace install payload (confirmed via a real
marketplace add + install + cache inspection). Nesting under plugin/
with marketplace.json's source: "./plugin" scopes both discovery and
the install payload to exactly the plugin's own files, with zero
content changes to the scaffold itself.

The stale minimal .claude-plugin/plugin.json at repo root is removed —
superseded by the real one at plugin/.claude-plugin/plugin.json; repo
root's .claude-plugin/ now holds only marketplace.json, which is what
`/plugin marketplace add PerpetualSoftware/pad` needs to find there.

`claude plugin validate --strict` passes clean on both the repo root
(marketplace) and plugin/ (plugin manifest) independently.

* fix(plugin): round-1 review fixes — invocation reality, degradation ordering, confirm-by-default, license (TASK-2534)

Six findings from codex round 1 + dispatcher's shallow pass:

- pad/SKILL.md's "How This Works" claimed a literal `/pad <anything>`
  command, but under plugin namespacing this skill registers as
  `pad:pad`, not a bare `/pad`. Rewrote to state the true DR-1 model
  (contextual trigger by description, not typed) and defined the
  document's own `/pad <anything>` notation as shorthand for "what the
  user said," not literal syntax. Fixes the earlier /pad:pad cosmetic
  note at its root and the two "onboarding" sections that repeated the
  same wrong dual-syntax framing.

- status/SKILL.md and capture/SKILL.md invoked `pad` before stating
  their missing-CLI fallback; reordered so the failure modes are
  handled as part of the same instruction, not an afterthought.
  monitors.json's command had no guard at all — a padless machine
  would hit a raw spawn error, not silence. Added a `command -v pad`
  shell guard (verified live, both branches: silent when pad is
  absent, invokes correctly when present) and re-verified the two
  original degradation legs (no .pad.toml, unreachable padd) through
  the exact guarded command string, not just the bare binary.

- `pad bootstrap` hard-exits ("no workspace linked...") when unlinked
  (cmd/pad/main.go's getWorkspace()), but the skill assumed it always
  succeeds and the onboarding NL routing tried to load the onboard
  PLAYBOOK before a workspace — and therefore that playbook — could
  exist. Added the unlinked-workspace branch to Context Loading and
  taught onboarding routing to run `pad workspace init` first in that
  case, matching what skills/onboard/SKILL.md already did correctly.

- capture/SKILL.md's "no ceremony" immediate-create contradicted the
  main skill's "always confirm before creating/modifying." Made
  confirm-first the default, with an explicit opt-out for workspaces
  whose conventions declare autonomous capture.

- Two `open "$IMG"` examples were macOS-only; added the Linux
  (xdg-open) alternative and a describe-the-path fallback.

- plugin.json declared MIT; the repo is Apache-2.0 (LICENSE,
  web/package.json). Corrected.

`claude plugin validate --strict` clean on both the repo root
(marketplace) and plugin/ (plugin manifest) after the fixes.

* fix(plugin): make DR-4's no-write default unmistakable (TASK-2534)

Second independent dogfood run found the etiquette text underdetermined
behavior: one run correctly parked a context nudge, another interpreted
"fold it in" as license to run pad item comments and post a real reply
to the watched item — a write action from a context notification, which
DR-4 never intended and the plugin's noise-discipline promise depends on
never happening. Rewrote the section: read-only one-line park-and-summarize
is now the explicit default for every watch notification and any addressed-
to-you event the model isn't certain about; writing to Pad in reaction to a
notification is called out as prohibited by name (no more "fold it in"
euphemism), with a single narrow exception (an assignment/ask explicitly
addressed to the session's user, where acting is unambiguously expected)
and an explicit "when in doubt, park" tiebreaker.

claude plugin validate --strict clean on both repo root (marketplace) and
plugin/ (plugin manifest).

* fix(plugin): loop the pad-missing monitor guard instead of a one-shot sleep (TASK-2534)

Verifying the finding-2 guard through Claude Code's REAL monitor spawn
path (--bg --plugin-dir, not a manual `sh -c` test) surfaced a real gap
the manual test couldn't: the runner does NOT relaunch a monitor command
after it exits. The round-1 guard (`command -v pad && exec ... || sleep
3600`) is a single check-then-sleep-then-exit — once that one hour
elapses, the process exits and, per this observed behavior, is never
restarted, leaving the session permanently monitor-less for the rest of
its life on a machine where pad wasn't yet on PATH at session start.
That silently breaks DOC-2479's "sleep-retry hourly" contract, which the
Go binary's own `pad watch --stream --for-session` loop honors correctly
for its two conditions (no .pad.toml, padd unreachable) via an actual
internal retry loop — this shell guard needs the same shape for its one
condition (pad binary absent).

Wrapped the check in `while ! command -v pad; do sleep 3600; done` so it
keeps re-checking indefinitely instead of dying after one interval, then
falls through to the same `exec pad watch --stream --for-session` once
pad becomes resolvable. Verified live through the real spawn path with a
shortened sleep interval on a throwaway plugin copy: a fresh sleep child
appears every cycle (confirms the loop keeps running, not one-shot), then
re-verified the actual committed command (real sleep 3600) resolves a
live `pad watch --stream --for-session` process when pad is present and
sits in a healthy, silent, no-error `sleep 3600` loop when it's absent —
both through claude --bg --plugin-dir, not manual shell invocation.

claude plugin validate --strict clean on both repo root (marketplace)
and plugin/ (plugin manifest).

* fix(plugin): probe watch capability, not just binary existence, in the monitor guard (TASK-2534)

Codex round 2, P1: the round-1 fix looped on `command -v pad`, but an
old pad binary predating `pad watch` resolves on PATH fine and then
dies non-zero the moment `exec pad watch --stream --for-session` runs
— and the previous commit already established the runner never
relaunches an exited monitor command, so that death is permanent for
the session. `command -v` only proves the binary exists, not that it
has the subcommand this guard needs.

Changed the loop condition to `pad watch --help`, which probes the
actual capability in one call: covers both missing-binary (no such
command) and old-binary-without-watch (unknown subcommand, non-zero
exit) with a single condition. Verified first that `--help` is safe to
call as a probe — it succeeds (exit 0) with no `.pad.toml` anywhere
and padd unreachable, so it can't deadlock a legitimate unlinked-
project or padd-down user behind the guard.

claude plugin validate --strict clean on both repo root (marketplace)
and plugin/ (plugin manifest).

* fix(plugin): round-2 doc fixes — capture routing, confirm scope, invocation wording, CLI accuracy (TASK-2534)

Findings 2-8 from codex round 2:

- capture/SKILL.md: no unlinked-workspace branch — would fail opaquely
  on `pad collection list` without .pad.toml. Mirrors the same routing
  fix the main skill already has: run `pad workspace init` first.

- pad/SKILL.md's notification write-exception didn't say whether the
  confirm-always principle (Key Principles #3) still applies inside it.
  It does — the exception only lifts never-write, not confirm-first —
  now stated explicitly where the exception lives, same rule as
  capture's autonomous-capture opt-out.

- The `/pad` shorthand disclaimer scoped itself to "the playbook-routing
  and examples sections below," missing Context Loading's "on every
  `/pad invocation`" a few lines above it. Widened to cover the whole
  document. plugin.json's own description repeated the bare "/pad
  conversational surface" claim; reworded to name the real typed
  shortcuts instead, and synced marketplace.json's plugin-entry
  description to match (was already a paraphrase, now identical).

- Ideation example passed both `--content` and `--stdin` to the same
  `pad item create` call — verified --stdin wins and hangs on ReadAll
  with no piped input. Dropped --stdin, kept --content.

- Plan-retro example was missing the mandatory `--comment` this same
  doc teaches elsewhere. Key Principles #2's status enumeration listed
  "blocked" as a task status — verified against
  internal/collections/templates.go: the tasks schema's actual options
  are open/in-progress/done/cancelled, no "blocked". Fixed both.

- Two role-board pointers claimed `pad server open` lands on
  `/{workspace}/roles` directly; verified cmd/pad/cmd_server.go's open
  command only appends the workspace slug, never a sub-path. Reworded
  both to describe navigating to Roles from the opened UI rather than
  asserting an unverified deep-link.

claude plugin validate --strict clean on both repo root (marketplace)
and plugin/ (plugin manifest).

* fix(plugin): safe (non-hanging) unlinked-workspace self-heal, onboarding offer in status, ask-events wording (TASK-2534)

Codex round 3, findings 1-3:

- The routing added in rounds 1-2 told the skill to run bare
  `pad workspace init` whenever bootstrap reported "no workspace
  linked." Verified live this can HANG INDEFINITELY: with pad
  configured (mode/URL set) but no admin account created yet /
  session not authenticated, `pad workspace init` calls a
  browser-based setup/login flow with no TTY guard and no
  non-interactive fallback — unlike `pad init`, which has an explicit
  `!canPromptForConfig()` check at the same step and fails fast with a
  headless-bootstrap hint instead. Bootstrap's own two failure
  signatures ("Pad is not configured" vs "no workspace linked") don't
  fully disambiguate this either — verified live that "no workspace
  linked" fires even when the deeper problem is "no admin account
  exists yet," which is exactly the state that hangs.

  Fixed by adding a `pad auth whoami` preflight (verified live: fast,
  safe, never blocks, in all three states — unconfigured, configured-
  but-unauthenticated, and fully set up) before ever attempting
  `pad workspace init`. Only self-heal when whoami reports a real
  user; otherwise tell the user to run `pad init` themselves in an
  interactive terminal (Claude Code: suggest `! pad init`). Applied
  consistently across pad/SKILL.md's Context Loading and Onboarding
  routing, onboard/SKILL.md, and capture/SKILL.md's unlinked-workspace
  branch (round 2 addition, same hang risk).

- status/SKILL.md ran `pad project dashboard --format json` but never
  checked its `needs_onboarding` field (verified present on that
  response too, not just bootstrap's — internal/server/handlers_
  dashboard.go's NeedsOnboarding), so the mandated onboarding offer
  couldn't fire via `/pad:status`. Added the same offer wording the
  main skill uses.

- monitors.json's description claimed the stream delivers "asks for
  your role" alongside assignment; verified against
  internal/watchevents/watchevents.go: KindAsk is contract-reserved
  with no Phase 1 producer. Reworded to describe what ships today,
  noting ask-events as reserved-not-yet-emitted.

Filed docapp TASK-2537 for the fourth finding (embed-source
skills/pad/SKILL.md has the same --content/--stdin bug and has
drifted from these corrections) — selective port, out of this diff's
scope per the two skills' by-design framing divergence.

claude plugin validate --strict clean on both repo root (marketplace)
and plugin/ (plugin manifest).

* fix(plugin): --full on body-promising queries, capture loads always-on conventions, onboard's already-set-up path (TASK-2534)

Codex round 4, findings 1-3:

- Every `pad item list conventions|playbooks --format json` example whose
  surrounding prose promises "bodies" was missing `--full` — verified
  against cmd/pad/cmd_item.go: the flag exists specifically because JSON
  output defaults to a token-light summary shape with no content.
  Without it, all 8 of these calls (the Context Loading bootstrap-field
  description at :41, the 7 trigger-query examples under "Before
  Performing Work") would return exactly what the surrounding text says
  they're loading bodies to avoid: metadata with no content. Added
  --full to all 8, not just the block the finding cited — the "Template"
  lines carry the identical "pull their bodies" promise as the
  "Concrete examples" lines a few lines below them, so leaving one set
  fixed and the other not would just move the inconsistency.

  Swept the rest of the file's `item list ... --format json` examples
  (Ideation load-context, Status Check role queue, Daily Standup) —
  left those alone; their surrounding prose asks for enumeration/counts
  ("3 items in your queue", "Yesterday/Today/Blockers"), not body
  content. The one exception found: Retrospective's task-load line
  promises "what shipped, what was deferred, lessons learned" — that
  needs actual task content to synthesize, so it got --full too, with
  a one-line note explaining why (the other fixed lines didn't need
  one; their promise was already explicit in the surrounding prose).

- capture/SKILL.md never loaded conventions at all. Per the dispatcher's
  ruling: low ceremony means skipping bootstrap's dashboard/playbook/
  role weight, not skipping mandatory project rules (trigger=always
  conventions exist precisely to be small and always-applied). Capture
  now runs the always-on conventions query (with --full) before
  creating and applies whatever it returns; the autonomous-capture
  exception now checks the SAME load instead of running its own
  separate (previously --full-less) query. One sentence documents the
  tradeoff as deliberate: always-on rules yes, full bootstrap no.

- onboard/SKILL.md only defined the needs_onboarding=true branch. Added
  the false branch: say the workspace is already set up, summarize what
  exists (collection/convention counts from the bootstrap payload
  already in hand), and offer extend/audit instead of re-running
  first-time setup — same confirm-first rule as everywhere else.

claude plugin validate --strict clean on both repo root (marketplace)
and plugin/ (plugin manifest).
2026-08-12 23:49:47 -04:00
..