Commit Graph

49 Commits

Author SHA1 Message Date
xarmian 402f79e016 feat(store,server,web): collection kernel traits — de-hardcode conventions/playbooks slugs (TASK-2657, BUG-2702) (#1171)
Implements SPEC-5 §Collection traits (approved v1.1) — the first unit of
PLAN-2656 phase 0. Three kernel behaviors were keyed on the literal collection
slugs "conventions" and "playbooks": what the agent bootstrap loads, which
items route by invocation slug, and which items export as portable artifacts.
Collections now DECLARE those behaviors and the kernel resolves them from the
declarations.

Fixes the KERNEL half of BUG-2702, which stays open for the rest (see below).
A slug is not a stable identifier —
UpdateCollection re-slugs on any name change, and renaming a collection is a
documented onboarding step (TASK-1510) — so renaming either collection silently
detached all three behaviors from it, with the items still present and no error
anywhere. Measured on origin/main before the fix: conventions and
convention_index dropped 1 -> 0, playbooks 1 -> 0, and GET /playbooks/{slug}
went 200 -> 404, so `/pad ship` stopped resolving with no sign the playbook
still existed. Both halves are locked by regression tests observed failing on
unfixed code.

BUG-2702 is NOT fully closed here, deliberately. Every kernel behavior follows
the trait, and library activation on the MCP dispatcher and CLI was converted
too — but the pack's own dedicated web routes (/conventions, /playbooks list and
detail, /library) still address their collection by literal slug and render
empty after a rename. Filed as BUG-2705 with the route paths and the likely fix
shape; 2702 closes when that lands. Degradation there is bounded: no data loss,
and the collection stays usable at its own /[collection] route and in the
sidebar.

SPEC-5 was amended to v1.1 BEFORE any code, per the spec tree's own discipline:
bootstrap_include becomes a LIST of {mode, filter, key} because v1.0 could not
express convention_index at all; the conventions filter is now normative and
includes status=active, which v1.0's shorthand omitted and which the
implementation does enforce (implementing v1.0 literally would have leaked
draft conventions into every agent's boot payload); v1 filters are field-
equality maps with query/1 named as the widening path, since SPEC-2 is phase 1
and PLAN-2656 forbids growing toward it; and invocation_field is constrained to
the literal `invocation_slug`, because any other field name falls outside the
partial unique indexes in migrations/054 and pgmigrations/033 that are the real
uniqueness guard.

Traits get their own column rather than a key inside the schema JSON. The
schema column is overwritten wholesale on update and every client rebuilds it
fields-only, so a traits key stored there is destroyed by one ordinary
collection edit — measured during this task, not assumed. Trait authority
cannot rest on a value an unrelated UI save deletes. UpdateCollection writes
traits only when explicitly supplied, so pre-existing clients leave them alone;
an explicit "{}" still clears.

Bootstrap keeps its three payload keys as first-party views fed from the
declarations, and gains a generic bootstrap_includes array for any other
declared key — so the boot surface is genuinely generic rather than three
hardcoded payloads, and no consumer breaks.

Existing workspaces are backfilled slug-keyed in both dialects, guarded on
traits='{}' so a re-run cannot clobber a workspace's own declarations. The
backfill inherits today's blind spot (a workspace that renamed the collection
before upgrading is not reached) but cannot do worse than the status quo, which
is itself slug-keyed; from the backfill forward the hazard is structurally gone.

Malformed declarations are refused at create and update rather than stored:
an unparseable blob degrades to "declares nothing", which is silently the wrong
behavior instead of a loud error (SPEC-0 L6).

Web groups agent-facing collections by bootstrap_include presence, replacing a
hardcoded two-slug array repeated at five call sites.

Not done, deliberately: no MCP catalog change (traits are first-party kernel
declarations, no agent needs to set them, and the separate column means
pad_collection.update passes through harmlessly — no ToolSurfaceVersion bump);
bootstrap's collections[] projection does not carry traits (PLAN-1410 trimmed
that payload and nothing consumes them there); prefix.go's NormalizeSlug is
untouched (a pure function with no workspace context, and de-hardcoding two of
its six slugs would make it less coherent, not more).

Eight Codex review rounds found nineteen real defects, all fixed here. The
serious one:
bootstrap_include filter keys FAIL OPEN. The item store's field-filter path
drops any key its sanitizer rejects, removing the predicate rather than matching
nothing, so a declaration filtering on `"stat us"` would narrow nothing and ship
every convention — drafts included — to every agent at boot, defeating the
status=active guarantee this change makes normative. Filter keys are now
validated against the store's own sanitizer shape and pinned by a cross-package
agreement test, since models cannot import store and a future divergence would
silently reopen it. SPEC-5 amended to v1.2 with the rule and its fail-open /
fail-closed asymmetry. Also fixed: an unknown declared artifact_kind reached
artifact.Encode and surfaced as a 500 (now a 400 at the export boundary, since
SPEC-5 permits unknown kinds as legal non-round-tripping declarations); and
workspace import validated traits as JSON only, so an archive could persist a
declaration that degrades to "declares nothing" (now validated, degrading to
"{}" with a warning rather than refusing an import that may be the only copy).

Later rounds found more, and several were defects this change itself created.
A hidden collection could SHADOW a visible one: resolution used to name exactly
one collection, so with several declaring, resolving across all of them and
rejecting afterwards on visibility made a visible playbook unreachable behind a
hidden one — candidates are now filtered by visibility before selection, in both
playbook resolution and artifact import. Importing a pre-traits archive produced
an INERT workspace: the migration backfill cannot reach rows inserted long after
it ran, so conventions/playbooks arrived declaring nothing, and canonical
declarations are now inferred from the slug when a collection declares none
(never overriding declarations that survived the round trip). The generic
include path had no L4 boot budget and is now capped with an overflow count.
Trait parsing claimed to be strict but json.Decoder ignores trailing bytes, so
`{...} garbage` parsed cleanly. First-party payload keys are now mode-pinned,
since their projections have fixed shapes and declaring the other mode would be
silently ignored. Duplicate artifact_kind / invocation_field declarations are
refused at the collection API, and a conflicting archive warns on import.

Agent-facing text was updated with the rest, not after it: SKILL.md,
instructions.md and the MCP catalog said the literal slugs, which is exactly the
artifact an agent acts on. ToolSurfaceVersion 0.24 -> 0.25 for the
pad_library.activate behaviour change.

Trait uniqueness is a documented BEST-EFFORT gate, not an invariant, by lead
ruling. The gate reads then writes without a lock, import bypasses it, and a
rename can mint a duplicate without touching that path. The database-level
enforcement (partial unique indexes on the extracted traits) cannot ship first:
existing deployments can already hold duplicates via rename-then-reseed, so the
index would fail the migration on precisely the databases that most need
repairing. TASK-2710 carries the de-duplication pass and the indexes; SPEC-5
v1.3 records the deferral and the reason. L6's requirement that conflicts fail
loud is met by the refusal plus the warning — the mechanism is deferred, the
principle is not.

Gates: build · make lint 0 issues · go test ./internal/... · make test-pg ·
svelte-check 0 errors · vitest 99 files / 1734 tests. Mutation-verified across
four matrices, 20 mutations, 19 caught; the survivor is a seeding path whose
trait-vs-slug difference is unreachable today (SeedCollectionsFromTemplate
creates any missing template collection before it seeds items), recorded on the
task trail rather than papered over with a test that proves nothing.

Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V
2026-08-20 13:26:05 -04:00
xarmian 7332a7f9f8 feat(collections): add spec workspace template — spec-driven development (IDEA-2527) (#1081)
* refactor(collections): extract tasksCollection/ideasCollection helpers

Pulls Tasks and Ideas out of Defaults() into standalone functions,
mirroring the existing docsCollection extraction. Seeded schema is
byte-identical; this just lets a template compose Tasks/Ideas without
also getting Plans, which the upcoming spec template (IDEA-2527) needs.

* docs(collections): generalize decompose playbook to plan-or-spec

The shared `decompose` library playbook was worded plan-only
(target description, pre-flight checks, body-analysis step). Broadens
the wording to also recognize a spec's `## Implementation plan` /
`## Acceptance criteria` sections as decomposition sources, ahead of
the spec template (IDEA-2527) reusing this playbook. Title is
unchanged (looked up by exact string elsewhere); wording is additive
so startup/scrum/product, which have no Specs collection, are
unaffected.

* feat(collections): add spec workspace template (IDEA-2527)

New "spec" template positions Pad as a spec-driven-development
platform: Specs (SPEC, draft→in-review→approved→implemented→
superseded, version+area fields, content_template skeleton) replaces
Plans as the parenting artifact — idea/bug → spec → tasks → PRs.
Deliberately no Plans collection; implementation-plan material lives
in the spec body's optional "## Implementation plan" section instead
(cheaper than maintaining two overlapping artifacts).

Ships:
  - SpecConventionTriggers/SpecPlaybookTriggers, extending the
    software trigger vocab with on-spec-draft/approve/change
  - Four seed conventions gating implementation, PR review, and
    spec-edit discipline on the spec lifecycle
  - Three full-prose playbooks: `/pad spec` (draft-first interview
    with IDEA/BUG graduation), `/pad verify` (walk acceptance
    criteria against the diff/behavior), `/pad extract-specs`
    (brownfield extraction with a subsystem-map human checkpoint and
    provenance-marked observed-behavior specs)
  - Reuses `decompose` (generalized in the previous commit) and
    `ship` (unchanged) as the remaining two seed playbooks

Registered in templates.go; adds the three new playbook bodies to
TestInvocationFramingStaysNLCanonical's scanned surfaces. Dedicated
tests in templates_sdd_test.go cover registration, Specs schema
shape, extended (not replaced) trigger vocab, the four conventions,
and the five playbooks.

Positioning/marketing page descoped from this PR — recon found no
marketing-page infrastructure in this repo to extend (the root route
redirects straight to /console); tracked separately.

* fix(collections): gate spec graduation, fix strict-parser arg contract

Codex round 1, findings 1-2:

- `/pad spec` accepted ANY ref matching the generic ref pattern
  (e.g. TASK-7) and would enter graduation mode, terminalizing an
  unrelated item's status. Dispatch now resolves the ref, checks its
  collection is actually Ideas-like or Bugs-like before graduating,
  and otherwise falls back to using it as recon context for a
  new-topic draft with no status flip.
- `target` was documented/declared optional, but the strict CLI/MCP
  parser only fills REQUIRED args positionally (internal/server/
  handlers_playbooks.go) — `pad playbook run spec "<topic>"` silently
  failed to bind it. Made `target` required, matching the `plan`
  playbook's `topic` precedent. `extract-specs`'s `target` stays
  optional by design (bare invocation is a supported flow); its
  Arguments docs now show the strict-path key=value form instead of
  implying positional works.

Adds a regression test (TestSpecPlaybookTargetArgumentRequirement)
pinning target's required-ness for both playbooks.

* docs: sync decompose's structured arg metadata + SKILL.md to plan-or-spec

Codex round 1, finding 3: the decompose playbook body was generalized
to plan-or-spec in an earlier commit, but its structured
`arguments` JSON metadata (the queryable contract) and
skills/pad/SKILL.md's Decomposition entry were left plan-only —
exactly the drift the body's own comment says these two surfaces
must not have.

* fix(collections): treat unedited Implementation-plan placeholder as absent

Codex round 1, finding 4: the spec content_template always ships a
populated "## Implementation plan" section, which meant decompose's
"no implementation plan -> fall back to acceptance criteria" path
never triggered for skeleton-created specs — it would treat the
placeholder's angle-bracket instruction text as a real task
candidate. The skeleton placeholder now tells the author to delete
the section if unused, and decompose's source-analysis step treats
an unedited placeholder the same as a missing section.

* docs(collections): drop phantom TASK-2528 reference

Codex round 1, finding 5: TASK-2528 doesn't exist in the docapp
workspace. The tasksCollection/ideasCollection extraction comments
now cite IDEA-2527 only.

* docs: add spec-target routing example to SKILL.md Planning section

Codex round 2, finding 1: the Planning section's decomposition routing
example only showed a plan target ("break plan 2 into tasks" → PLAN-2).
Adds a spec-target example alongside it, consistent with the
Decomposition entry further down (already generalized to plan-or-spec)
and the decompose playbook body/arguments.

* fix(collections): generalize ship playbook target to plan-or-spec

Codex round 3, P1: the spec template seeds ShipPlaybook() unchanged,
but its target contract documented only PLAN-ref | TASK-ref. Decompose's
step-7 report tells the user to run `/pad ship <target-ref>` on the
source ref, which in a spec workspace is SPEC-N — so the seeded
idea->spec->tasks->ship handoff broke at the last step.

Generalizes target's wording across all three surfaces (the Arguments
line, the argument-parsing PLAN-ref bullet, and the arguments-JSON
description) to PLAN-ref | SPEC-ref | TASK-ref, mirroring the same
additive pattern already used for decompose: a spec is a parenting
artifact with identical expansion mechanics to a plan (same
--parent-child wiring), so the change is inert for startup/scrum/product,
which have no Specs collection. No test pins the exact argument text,
so existing structural tests (TestStartupTemplateShipsShipPlaybook,
TestPlaybookLibrary_ShipBodyShared) pass unchanged.

* fix(collections): generalize ship's remaining plan-only mentions

Codex round 3 follow-up: two plan-only mentions left over from the
target-contract generalization.

- Commit-message template's "Parent: PLAN-XXX." -> "Parent: PLAN-XXX /
  SPEC-XXX.", plain aliasing matching everything else already
  generalized.
- Step 11's parent-closing guidance keeps the existing plan sentence
  as-is and adds the spec case as a judgment-trigger pointer rather
  than a parallel unconditional flip: a spec's terminal status is
  gated by verification (that's what the `verify` playbook is for),
  so ship tells the agent to run `/pad verify SPEC-XXX` instead of
  flipping the spec's status directly — it moves the spec to
  `implemented` only once the acceptance criteria actually hold.

* fix(collections): ship's PR-body guidance cites specs and their criteria

Codex round 4, P1: ship's PR-context generation still said "parent
plan" and templated the PR body under <PLAN-REF> only — so
`/pad ship SPEC-N` produced a PR that never cited the governing spec,
directly violating the spec template's own seeded on-pr-create
convention ("PRs cite the spec and which criteria they satisfy").

Generalizes the PR-body template to <PARENT-REF> (PLAN-ref or
SPEC-ref) and adds explicit guidance: when the parent is a spec, the
PR must also list which acceptance criteria it satisfies (e.g.
"Implements TASK-12 under SPEC-4, satisfies AC-1, AC-2") — this is
what makes /pad verify fast later, since the reviewer walks the cited
criteria instead of re-deriving intent. Also generalized step 1's
"check the parent plan's content" to plan-or-spec, since a spec
parent's acceptance criteria are exactly what step 8 needs to cite.

* fix(collections): verify gates the implemented flip on spec approval

Codex round 4, P1: /pad verify only excluded draft specs from the
flip-to-implemented, so it could promote an in-review or superseded
spec straight to implemented on the strength of passing acceptance
criteria alone — bypassing the workspace's own approval lifecycle.

Verification still runs and reports AC results regardless of status
(useful information either way), but the Resolve step's all-pass path
now branches on status: approved -> offer the flip (unchanged);
already implemented -> report the re-verify confirms it still holds,
nothing to flip; in-review -> report the pass but tell the user
approval isn't done yet, point at finishing review; superseded ->
report the pass but point at whatever spec replaced this one, since
that's the one that should be verified and implemented going forward.

* fix(collections): decompose treats placeholder ACs as absent too

Codex round 4, P2: the AC-fallback path treated unedited skeleton
placeholders (AC-1: <a statement...>) as real task candidates, so a
fresh untouched spec could decompose into bogus tasks. Extends the
same placeholder-as-absent rule already applied to the
Implementation-plan section: an AC-N line still holding the unedited
angle-bracket instruction text isn't a real criterion and doesn't get
a task proposed for it. If every AC-N is still a placeholder, there's
nothing to decompose from either source — decompose stops and tells
the user the spec has no real acceptance criteria yet.

* fix(collections): unify AC placeholder idiom, cover bare-ellipsis form

Codex round 5, P2: the seeded skeleton's AC-2 used a bare-ellipsis
placeholder ("AC-2: ...") while AC-1 used angle brackets and the
decompose placeholder rule only named the angle-bracket form — a
literal-minded agent could propose a bogus task for an untouched AC-2.

Two one-line fixes: the skeleton's AC-2 now uses the same
angle-bracket idiom as AC-1 ("AC-2: <the next verifiable criterion>"),
so the seeded skeleton has one placeholder style; decompose's
placeholder rule now also names bare ellipsis ("AC-N: ...") as a
placeholder form, as belt-and-suspenders for user-typed shorthand
beyond just the seeded skeleton.

No test pinned the AC-2 text, so no test changes needed.

* fix(collections): spec's circulate-for-review branch actually sets in-review

Codex round 6, P2: the "circulate for review" branch said to leave the
spec at in-review and stop, but the create command always uses
--status draft and no update followed it on that path — so the spec
silently stayed draft forever. Since round 4's fix gates verify's
implemented-flip on approval status, an item stuck at draft (never
even reaching in-review) is a stuck workflow, not just a label
mismatch.

Adds the explicit `pad item update <new-spec-ref> --status in-review
--comment ...` step to the circulate branch, parallel to the
approved-outright branch's existing update command, keeping the
audit-comment habit consistent with the rest of the body.

* fix(collections): add resume mode so circulate-for-review specs converge

Codex round 7, P1: the circulate branch stopped the playbook before
the graduation step, and nothing ever completed it — a later
`pad item update SPEC-N --status approved` was a bare status flip with
no agent step attached, so the source IDEA/BUG never got terminalized.
Worse, the advertised rerun path was broken: `/pad spec SPEC-N`
dispatched as non-graduation (a spec isn't Ideas/Bugs-like per the
round-1 gate) and would have created a SECOND spec instead of
resuming the first.

Three coordinated edits:

- New dispatch mode: a target resolving to the specs collection
  itself enters resume mode, never creates anything. Branches on the
  spec's status — in-review is the normal resume case (confirm
  approval, complete any pending graduation, offer decompose);
  draft/approved/implemented/superseded get the sensible remainder
  (offer the original choice again, report already-resolved state, or
  point at the successor spec).
- The circulate branch now records a "Graduation pending approval:
  <source-ref>" comment on the new spec when in graduation mode, so
  resume mode has something mechanical to find rather than relying on
  re-deriving intent from the Context section.
- The circulate branch's stop text now tells the user how the loop
  closes: rerun `/pad spec SPEC-N` when review is done.

Updated the `target` argument docs (body + arguments JSON) to name the
third accepted form. templates_sdd_test.go doesn't assert argument
description text, so no test changes needed.

* fix(collections): restructure graduation as one idempotent reconcile rule

Codex round 8, two P1s: the approved/implemented branch of Resume
never checked the pending-graduation marker (so a plain manual
`--status approved` never graduated the source), and the circulate
branch's marker write sat after "stop here" — a skippable step three
rounds of findings kept landing on. Scattering graduation state and
handling across branches was the actual bug; restructuring so the
class can't recur, not another branch-local patch.

- The graduation-link comment now gets written unconditionally at
  spec-creation time (step 5, graduation mode), before any
  approve/circulate branching — no ordering problem, no skippable
  step, exists on every path including a crash before either branch
  completes.
- One Reconcile rule, stated once in the Resume section: on every
  resume that reaches it (draft/superseded stop earlier and skip it;
  in-review-not-yet-approved also skips it), if the spec is now
  approved or implemented and its comments name a graduation source
  that's still open, complete the graduation — idempotent, so it's
  safe to run on every resume regardless of how approval happened
  (through this playbook, a crash-recovery rerun, or a bare manual
  status flip outside the playbook entirely).
- Step 6 (approve-outright path) is now just an invocation of
  Reconcile rather than parallel instructions — graduation mechanics
  described exactly once, referenced from both call sites.

Dispatch and Arguments text re-read coherent after the restructure;
no changes needed there beyond what round 7 already added.

* fix(collections): graduation idempotent by source, custom collection, promise wording

Codex round 9, five findings — the last substantive round before
remaining crash-window-shaped gaps become documented limitations
rather than more branches:

1. (High) Rerunning /pad spec IDEA-x mid-flight created a second
   spec — graduation wasn't idempotent by SOURCE, only by spec ref.
   Pre-flight step 2 (graduation mode) now checks, before drafting,
   whether the source's trail already shows a "Graduating into
   <spec-ref>" comment, or whether a search of the specs collection
   finds a spec whose Context names this source. Either match means
   this run is really a resume — switch to resume mode on the found
   spec instead of creating.
2. (High) A crash between create and the marker comment left an
   unlinked spec. Step 5 now writes markers on BOTH sides (source and
   new spec) immediately after create, back-to-back, shrinking the
   window. Documents the actual recovery mechanism instead of
   pretending atomicity: the skeleton's Context section always names
   the source, so finding 1's recon check catches even a marker-less
   spec on the next run.
3. (Medium) The resume-mode dispatch check tested only the literal
   "specs" collection, breaking for a custom `collection` argument.
   Now tests against the resolved `collection` argument (checking
   `pad collection list` if renamed), consistent with the existing
   ideas/bugs check.
4. (Medium) The opening promise ("nothing gets created until the
   user approves") contradicted the circulate path, which creates an
   in-review item. Reworded to match actual behavior: nothing is
   created until the user chooses approve-or-circulate; the draft is
   always presented in chat first.
5. (Note) The superseded branch skipped Reconcile unconditionally,
   stranding any pending graduation. Now checks the marker before
   stopping: if the source is still open, the pending graduation
   transfers to the successor spec (if findable) via the same marker
   comment, so the successor's own future Reconcile picks it up.

* fix(collections): make graduation's recovery claims actually true

Codex round 10, four sentence-scale edits closing the gap between
what the prose claimed and what it actually did:

1. (High) Pre-flight step 2's "recon check is the actual recovery"
   claim was false for a marker-less spec after a crash: the search
   found the spec, but Reconcile still needed marker comments that
   were never written, so it would no-op and strand the source. Now
   the discovery path repairs — writes both sides' markers right then
   if missing — before proceeding to resume, so the recovery claim
   holds by construction instead of by accident.
2. (High) Transferring a pending graduation to an already-approved-or-
   implemented successor (superseded branch) wrote the marker but
   never re-triggered anything to act on it. Now runs Reconcile on the
   successor immediately in that case (idempotent, source ref already
   in hand) instead of waiting on a rerun that might never come.
3. (Medium) "Skip straight to Resume below" bypassed Resume's own
   pre-flight (loading the spec's comments), which Reconcile depends
   on. Now explicit: run Resume's pre-flight first.
4. (Medium/borderline) "Never creates a second spec" overstated the
   guarantee for a SPEC-ref passed with a mismatched --collection.
   Softened to "never creates a duplicate within the resolved specs
   collection" everywhere the claim appears (Arguments prose, Dispatch,
   pre-flight, and the arguments JSON description) — consistent
   scoped truth in every location rather than a strong claim in one
   place and a weaker one elsewhere.

* fix(collections): enforce the Context-citation premise, walk the chain

Codex round 11, two findings:

1. (High) The whole crash-recovery mechanism (pre-flight step 2's
   search, step 5's recovery claim) depends on a graduated spec's
   Context section naming its source — but nothing enforced that; the
   skeleton's own Context hint says "(if any)" since most specs
   aren't graduated, and step 3 never mandated the citation for the
   ones that are. Added the explicit rule to step 3: in graduation
   mode, Context MUST cite the source ref by ID (e.g. "Grew from
   IDEA-12"), stated with the reason — it's the search key crash
   recovery depends on. Reinforced in step 5's and pre-flight step
   2's claim text: attributed the guarantee to the playbook's own
   mandate, not to "the skeleton," which doesn't itself enforce
   anything.
2. (Medium) Transferring a pending graduation to a successor that is
   itself superseded parked the marker somewhere no rerun would ever
   look — chains of supersession weren't walked. The superseded
   branch now walks to the LIVE HEAD of the chain (bounded, ~10 hops)
   before transferring or reconciling; a loop or dead-end mid-chain is
   treated the same as no successor found, rather than guessed at or
   walked forever.

* fix(collections): don't silently pick a branch in the supersession chain

Codex round 12, the last: the chain-walk from round 11 silently picked
one live head when supersession branches (more than one spec claims to
supersede the same spec). One clause, grouped with the existing
loop/dead-end stop rule: if more than one spec claims to supersede the
same spec at any point in the walk, stop and ask the user which is
canonical before transferring — don't pick silently.
2026-08-12 14:20:02 -04:00
xarmian faf9b3734a feat(web): default new collections to Board — schema-aware (IDEA-2274, IDEA-2287) (#1015)
* feat(web): default new collections to Board view (IDEA-2274)

Board becomes the baseline default view for new collections; existing
collections keep their stored default_view (no migration).

- Frontend fallback (settingsDefaults, collection-page defaultMode,
  shareView coerce, initial viewMode) -> board
- Create/Edit collection modals default -> board
- Backend template seeds (defaults.go, templates*.go) list -> board for
  ideas/plans/docs/hiring/interviewing collections (tasks was already board)
- CLI `pad collection create` and MCP mapCollectionCreate defaults -> board
- Curated create-modal presets with deliberate list curation (Meeting
  Notes, Decisions, OKRs) intentionally left as list
- Pin the three list-keyboard-nav pane E2E tests to ?view=list

Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra

* fix(web): board default reaches public share page + ItemDetail fallback (Codex round 1)

Codex review found the public share route (s/[token]) derives its owner
default view via a separate `?? 'list'` fallback that bypassed the
coerceSettings change, so settings-less/legacy collections rendered List
on public share pages. Align it (and the pre-init selectedBase) to board.
Also align ItemDetail's inline CollectionSettings fallback (default_view
is unused there, but keep it consistent with settingsDefaults).

Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra

* fix(collections): group Contacts board by relationship, not status (Codex round 2)

Contacts has no `status` field, so defaulting it to Board grouped by the
default `status` rendered every card in a single Uncategorized lane. Set
BoardGroupBy=relationship so the board shows real lanes. All other
board-defaulted seed collections have a status field or an explicit
board_group_by (verified: Companies/Conventions/Playbooks/Docs have status).

Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra

* fix(web): always serialize ?view= so a List URL survives a board default (Codex round 3)

buildCollectionUrlParams treated List as the implicit URL view and omitted
it. With Board now a possible collection default (IDEA-2274), a List
selection on a board-default collection produced a URL that, when copied or
opened without the sender's localStorage, resolved back to Board. Always
serialize the view mode; add a covering unit test. Verified the pane E2E
suite (URL-equality assertions) stays green.

Claude-Session: https://claude.ai/code/session_01EZ6yr6pAUFb1uffan912ra
2026-07-23 13:33:01 -04:00
xarmian db1472123e test(collections): drift-guard for NL-canonical playbook invocation (TASK-1862) (#752)
Backstops PLAN-1858 / IDEA-1846 against regression: a denylist test that
fails when agent-facing copy reframes `/pad <slug>` as THE invocation form.

internal/collections/invocation_framing_test.go scans SKILL.md, the MCP
server instructions, and the four rendered seeded playbook bodies (plan /
decompose / onboard / ship) for canonical-framing phrasings ("maps directly
to /pad", "invoke via /pad", "say /pad …", "directly invokable as /pad",
"canonical /pad", "dispatches /pad", "Playbooks available: /pad"). It also:
- asserts the NL-canonical principle stays stated in SKILL.md (so the scan
  can't pass vacuously after a deletion), and
- self-checks that each banned regex matches its own representative example
  (so a typo can't silently neuter a pattern).

Deliberately a low-false-positive backstop, not a generator: labeled
shortcuts ("/pad ship in Claude Code") and slug-routing examples are fine.
The companion convention CONVE-1863 (created as workspace data) covers the
surfaces this test doesn't mechanically scan (MCP catalog/prompt Go consts,
the web UI).

Parent: PLAN-1858 (final task).

Claude-Session: https://claude.ai/code/session_01KmxkPxLksjf1pmrZDpsnTJ
2026-06-21 15:36:47 -04:00
xarmian a2827ecb33 feat(playbooks): NL-canonical invocation in seeded bodies + CLAUDE.md (TASK-1861) (#751)
De-hardcode `/pad <slug>` as the primary invocation form across user/agent-
facing copy:

- Seeded playbook bodies (plan / decompose / onboard): the cross-references
  and recap lines now lead with intent / the playbook name, with `/pad`
  labeled as the Claude-Code shortcut where shown.
- CLAUDE.md: the invocation_slug description, the library section (now also
  reflecting the `▶ <slug>` chip from TASK-1860), the onboarding sections,
  and the needs_onboarding nudge quote (updated to the shipped NL-canonical
  active offer from PLAN-1847).

Left Go `//` dev comments as shorthand — not user-facing, and out of scope
for the drift-guard (TASK-1862 scopes to body strings + markdown).

Parent: PLAN-1858.

Claude-Session: https://claude.ai/code/session_01KmxkPxLksjf1pmrZDpsnTJ
2026-06-21 15:31:25 -04:00
xarmian fa8064e2b9 feat(onboard): capture workspace intent at creation, warm the onboard run (TASK-1855) (#746)
Intent-as-seed for the onboarding bridge. The workspace `description` column
already existed end-to-end but nothing captured or surfaced it:

- Web: CreateWorkspaceModal gains an optional "What are you tracking?"
  textarea (create-only), sent as `description` on create.
- Bootstrap: AgentBootstrapWorkspace now carries `description` (omitempty,
  additive) so the onboard playbook can read the user's stated intent.
- Onboard playbook: pre-flight reads workspace.description; B1 reflects it
  back ("You mentioned this is for X — let's build around that") instead of
  opening cold with "what is this project?", falling back when absent.

Net effect: a user who types one line at creation gets an onboard interview
that starts warm instead of from zero.

Parent: PLAN-1847 (Phase 3).

Claude-Session: https://claude.ai/code/session_01KmxkPxLksjf1pmrZDpsnTJ
2026-06-21 12:25:46 -04:00
xarmian 619465a24b feat(onboard): suggest an independent AI code reviewer (model != implementer) (TASK-1645) (#654)
Encode the independent-reviewer principle (a reviewer model different from the
implementer catches more than self-review) as opt-in onboarding guidance — no
tool-specific operational lore (that stays ours).

- New generic library convention "Independent AI code review" (quality,
  on-pr-create, nice-to-have): states the principle; names review tools as
  examples (a review CLI, claude review, a GitHub bot) without operational depth.
- /pad onboard build (B3) + audit (A3) steps: the agent notes its own model
  (the implementer), probes for / asks about a review tool, and when a
  different-model reviewer is available, proposes activating the convention,
  naming the detected tool (e.g. codex) as the concrete suggestion. Skips when
  the only reviewer would be the same model. Never blocks.

Parent: PLAN-1628.
2026-05-29 18:38:42 -04:00
xarmian 24707fc2ad chore(templates): make seeded ship playbook review loop tool-neutral (TASK-1644) (#653)
The seeded ship playbook ships into every new workspace, so it must not carry
our local Codex operational lore. Strip the Codex name + the < /dev/null /
--full-auto / stdin-wedge details PR #646 added → a tool-neutral review loop
("use whatever synchronous review tool you have"), with a pointer that /pad
onboard can wire up an independent reviewer. Our Codex specifics stay only in
this workspace's PLAYB-1405 + the personal ship-tasks skill.
2026-05-29 18:35:10 -04:00
xarmian d2bcbd3b9b chore(deps,docs): bump x/image v0.41.0 + sync seed ship-playbook codex guidance (#646)
* chore(deps,docs): bump x/image to v0.41.0 (GO-2026-5031/5032) + sync seed ship-playbook codex guidance

- golang.org/x/image v0.39.0 → v0.41.0: clears GO-2026-5031/5032 (reachable
  via attachment image decode). go mod tidy. govulncheck clean.
- templates_startup_ship.go (seeded ship playbook for new workspaces): drop
  the deprecated `codex exec --full-auto`, add `< /dev/null` + a codex-specific
  note that open stdin causes the zero-output "wedge" (not prompt length), and
  the stdin-first rule-out in the wedge/safety notes. Matches the ship-tasks
  skill + PLAYB-1405.

* fix(docs): show review prompt as positional arg in seed ship-playbook example per Codex review (round 1)

codex exec reads the prompt from stdin when none is passed as an argument, so
the `-o <file> < /dev/null` example without a prompt would review nothing. Show
the prompt positional and note the gotcha.
2026-05-29 16:37:55 -04:00
xarmian 6433cc51ea feat(mcp): pad_library catalog tool + ToolSurfaceVersion 0.5 (TASK-1563) (#615)
* feat(mcp): pad_library catalog tool + ToolSurfaceVersion 0.5 (TASK-1563)

MCP catalog wiring for PLAN-1560 (`pad_library` MCP tool + matching CLI
surface). Closes IDEA-1514 — pure-MCP agents (notably the /pad onboard
playbook from PLAN-1496) can now browse and activate library entries
without shelling out.

## New tool

`pad_library` joins the v0.5 catalog as the ninth resource × action tool.
Three actions, all passThrough to the `pad library` CLI:

- `list`     — Browse conventions + playbooks. Defaults to summary mode
               for playbooks (compact bodies via the ?summary=true
               endpoint flag); conventions always carry full content.
               Optional type / category / full inputs.
- `get`      — Full body of one entry by exact title. Conventions-first
               precedence mirrors `activate`.
- `activate` — Create a workspace item from a library entry by title.

`Workspace: true` on the tool — list/get ignore it; activate validates
and uses it. The schema-level declaration gives activate automatic
pad_set_workspace session-default resolution (same precedent as
pad_meta's mixed-workspace actions).

## Dispatcher extensions

- `dispatchLibraryList` forwards `category` to BOTH endpoints and
  passes `summary=true` to the playbook endpoint by default (unless
  input.full=true). MCP-default summary mode keeps agent context
  budgets tight; CLI default already aligned in TASK-1562.
- `library get` added to the routeTable as a clean GET to
  /api/v1/library/entry with `title` mapped to the query string.
  Cleaner than another explicit dispatcher case — matches
  playbook list / playbook show shape.

## Version bump

ToolSurfaceVersion bumped from 0.4 → 0.5. Pure addition; no existing
tool/action/param/bootstrap shapes changed. Backwards-compatible for
any v0.4 consumer that doesn't enumerate the new tool. Documented in
version.go with the same comment-block structure as prior bumps.

## Test coverage

- catalog_readonly_test.go — pad_library added to the want{} map; three
  library action → cmdPath entries in expected{}; library list / get /
  activate added to liveCmdhelpDoc stubs.
- dispatch_http_project_test.go — 4-case table test (defaults, category,
  full=true, category+full) pins category/summary query-param forwarding;
  library get routing test confirms the routeTable entry resolves.

## Live MCP verification

- `initialize` handshake advertises padToolSurface.version=0.5.
- `pad_meta version` returns tool_surface_version=0.5.
- `pad_library list type=playbooks category=agent-workflows` returns
  4 playbooks in summary mode (content stripped, summary populated,
  invocation_slug + arguments present).
- `pad_library get title='Ship tasks'` returns
  {type: playbook, playbook: {…, content (9512 chars), invocation_slug: ship}}.

Parent: PLAN-1560. Unblocks TASK-1564 (cleanups).

* fix(onboard): update playbook body to use pad_library MCP tool per Codex review (round 1)

Codex P2 on PR #615: the /pad onboard playbook body in
internal/collections/playbook_library_onboard.go still told MCP-only
agents that the library catalog was "not yet exposed as an MCP tool"
and to work from memory — directly contradicting the pad_library tool
this PR just landed and breaking the main advertised consumer of the
new surface.

Updated step B3 (conventions) to mention both surfaces side-by-side
(`pad library list --type conventions` / `pad_library` with
`action: list, type: conventions`), and rewrote step B5 (playbooks)
the same way so the activate path doesn't drift either.

Pre-PLAN-1560 IDEA-1514 reference removed from the body — the idea
is now closed.

No test pins the playbook body content; `make check` passes; the
playbook seed still validates against the playbooks collection schema
since trigger/scope/invocation_slug/arguments are unchanged.

Closes the onboard-side scope of TASK-1564 (stale dispatch_http_slice4
hint + CHANGELOG still pending there).
2026-05-21 19:18:19 -04:00
xarmian 2df6edeaab feat(server): library endpoints gain ?category, ?summary, /library/entry (TASK-1561) (#612)
Extends the convention + playbook library HTTP layer to support the shape
the upcoming `pad_library` MCP tool and the updated `pad library` CLI need:

- `GET /api/v1/convention-library?category=X` — server-side filter,
  case-sensitive exact match. Unknown categories return an empty slice,
  not 404.
- `GET /api/v1/playbook-library?category=X&summary=true` — same filter
  plus a new summary mode that strips Content and injects Summary
  (first non-heading paragraph, ~240 char cap). Web UI and existing
  consumers omit the flag and see the legacy full-body shape. Summary
  mode deep-copies category slices so a request never mutates the
  package-level library data — TestPlaybookLibrary_SummaryDoesNotMutate
  Global pins this.
- `GET /api/v1/library/entry?title=X` — NEW. Returns one matched entry
  in a `{type, convention|playbook}` envelope. Conventions-first
  precedence mirrors the dispatcher's `library activate` so a title
  resolves to the same kind in both surfaces. 400 on missing title,
  404 on no match.

Hoisted `playbookSummary` to `collections.PlaybookSummary` so the
bootstrap handler and the new library endpoints share one algorithm.
Bootstrap continues to call it for every playbook entry it returns.

Adds 12 handler tests + the existing bootstrap-summary test stays
green after the move. Lint clean on touched packages; `make check`
gate is blocked by a pre-existing gofmt issue in
internal/store/workspace_members.go captured as BUG-1565.

Parent: PLAN-1560. Unblocks TASK-1562 (CLI) and TASK-1563 (MCP catalog).
2026-05-21 13:08:02 -04:00
xarmian 0930743304 feat: retire IDEA-1 seed pattern + 'pad onboard' cobra + surface blank in init (TASK-1501/1502/1503) (#577)
* feat: retire IDEA-1 seed pattern + 'pad onboard' cobra + surface blank in init (TASK-1501,1502,1503)

PLAN-1496's legacy-onboarding teardown:

TASK-1501 (remove seed items + update banner):
- internal/collections/templates_onboarding.go (and the _product/_scrum
  siblings) deleted — these generated the IDEA-1/PLAN-2/TASK-3/DOC-4 +
  BACK-1/SPRINT-2/BUG-3/DOC-4 + FEAT-1/FB-2/ROAD-3/DOC-4 first-person
  seeds. The /pad onboard playbook (TASK-1499 / TASK-1500) is the
  replacement.
- startup/scrum/product templates: SeedItems lines removed.
- post-init banner in printOnboardingHints: now points at "/pad onboard"
  in one line, then web UI link, then dashboard hint. The "use pad to
  get IDEA-1 / BACK-1 / FEAT-1" branch is gone.

TASK-1502 (retire cobra + OnboardingPrimaryRef plumbing):
- OnboardingPrimaryRef struct field on WorkspaceTemplate removed. The
  dashboard's banner auto-discovers seeds via item_number=1 +
  source="template" + created_by="system", so the field was redundant
  even before retirement.
- onboardingPrimaryRef() helper in cmd/pad/main.go removed.
- 'pad onboard' Cobra subcommand removed (~160 lines). It scanned the
  project directory for build/test/CI markers and seeded library
  conventions — useful behavior but CLI-only, unreachable from
  MCP-only agents. The /pad onboard PLAYBOOK now covers it.
- internal/cli/detect.go and workspace_context_detect.go stay; still
  used by the web-side workspace-context save path.

TASK-1503 (Blank in interactive picker):
- The picker already surfaces Blank because templates_picker.go iterates
  GroupTemplatesByCategory, and the IDEA-1479 Blank template entry lives
  in CategoryCustom. Verified the output renders correctly with the
  TASK-1498 description + icon update.
- 'pad workspace init --help' Long now mentions Blank explicitly +
  points users at /pad onboard. Helps discoverability without restructuring
  the picker.

Test changes (delete or rewrite tests that exercised the retired pattern):
- internal/collections/templates_test.go: six tests deleted (StartupOnboardingItemsOrderAndShape,
  ScrumOnboardingItemsOrderAndShape, ProductOnboardingItemsOrderAndShape,
  Startup/ScrumProduct/TemplatesDeclareOnboardingPrimaryRef). New
  TestSoftwareTemplatesShipNoSeedItems replaces them with the inverse
  invariant: software templates ship zero seed items.
- internal/server/handlers_dashboard_test.go: three IDEA-1/BACK-1/FEAT-1
  expectation tests collapsed into TestDashboardOnboardingSeed_NilForAllTemplates,
  which asserts the auto-discovery finds no seed because seeds no longer
  ship. (Hiring + EmptyWorkspace tests untouched — they already expect
  nil for unrelated reasons.)
- internal/store/items_test.go: TestSeedCollectionsFromTemplate{Startup,Scrum,Product}RefSequence
  and TestOnboardingFlow_FullWalkthrough_{Startup,Scrum,Product} deleted;
  these locked the IDEA-1 ref-sequence + walkthrough behavior. Unused
  helpers (findItemByTitle, extractStatus, safeFields, setItemStatus,
  countItemsInCollection) deleted alongside them.
- internal/mcp/resources_test.go: TestReadItem_PreservesIDEAOneOnboardingBodyVerbatim
  → TestReadItem_PreservesBodyVerbatim. Property is the same (resource
  pipeline doesn't mangle markdown), but the fixture is now synthetic
  markdown instead of the IDEA-1 seed.

Note: handlers_dashboard.go still has the auto-discovery code path
(onboardingPrimaryCollectionSlugs map + the loop that probes for
item_number=1 + source="template"). It's now dead code — no item
will ever match the criteria after this PR. Left in place for a
follow-up cleanup pass to keep this PR focused.

Parent: PLAN-1496.

* docs: replace 'pad workspace onboard' references with /pad onboard (Codex round 1)

P2 finding on PR #577: README + CLAUDE.md still advertise the
'pad workspace onboard' subcommand in four places (README §Onboard
agents to a new codebase, README §3 Teach your agents the rules,
README CLI Reference, CLAUDE.md CLI). After this branch lands, those
instructions return "unknown command."

Replaced each with guidance pointing at /pad onboard (the playbook,
auto-seeded into every workspace). The library-list commands still
work and stay where they are.

Parent: PLAN-1496.

* docs: replace 'use pad to get IDEA-1' guidance with /pad onboard (Codex round 2)

P1 finding on PR #577: README.md:33-39 and CLAUDE.md:111-117 still
told users to 'use pad to get IDEA-1' after the post-init banner.
Since this branch deletes templates_onboarding.go and stops seeding
IDEA-1/PLAN-2/TASK-3/DOC-4, the quickstart instructions in both
top-level docs pointed at items that no longer exist.

Replaced each with /pad onboard guidance (the playbook is auto-seeded
into every new workspace by TASK-1500). CLAUDE.md's CLI reference
gets a one-line historical note explaining the pre-PLAN-1496 IDEA-1
pattern so readers reviewing older code/blame have context.

Parent: PLAN-1496.

* docs(skill): retire 'use pad to get IDEA-1' guidance in agent skill (Codex round 3)

P1 finding on PR #577: skills/pad/SKILL.md:175 still taught agents
that '"use pad to get IDEA-1"' should dispatch to 'pad item show IDEA-1'.
This branch deletes the seed items, so any agent following the
shipped skill in a fresh workspace would try to fetch a missing ref
instead of running /pad onboard.

Updated the routing entry to dispatch the legacy phrasing (kept as a
recognized intent so older docs/conversations still work) to the
/pad onboard playbook. Explicit "do NOT try to fetch IDEA-1
directly" to short-circuit the previously-trained behavior.

A broader skill cleanup — removing the standalone Onboarding
workflow section and adding the bootstrap nudge rendering — is
TASK-1505's scope. This PR's update is the minimal change needed to
unbreak the agent-facing routing.

Parent: PLAN-1496.

* docs(skill): add library-activation caveat to onboard routing entry (round 4)

P2 finding on PR #577: the routing entry said /pad onboard is
'always invokable because every workspace auto-seeds it.' True for
newly-created workspaces, but pre-existing workspaces (created before
PLAN-1496 lands) won't have it. Auto-upgrade is intentionally not
wired into SeedCollectionsFromTemplate for empty-template-name paths.

Mirrored the same activation-fallback caveat /pad plan and
/pad decompose carry: 'activate via library if the bootstrap's
playbooks array lacks invocation_slug=onboard, status=active.'

Parent: PLAN-1496.
2026-05-17 13:40:15 -04:00
xarmian 507793e565 feat(playbooks): author canonical /pad onboard library playbook (TASK-1499) (#576)
* feat(playbooks): author canonical /pad onboard library playbook (TASK-1499)

The fourth invokable library playbook (alongside ship/plan/decompose).
This is the workspace bootstrap interview the agent runs to turn a
freshly-created workspace into one whose collections, conventions,
playbooks, and roles actually match the user's project.

Files:
- internal/collections/playbook_library_onboard.go (new):
  - onboardPlaybookBody — surface-agnostic instruction set teaching
    the agent to ADAPT seeded artifacts, not curate from the library.
    Mode-aware: build (blank workspace), audit (templated workspace),
    revisit (already-onboarded), defaults (escape hatch). The body
    explicitly tells the agent to use pad_item/pad_collection/pad_role
    MCP actions OR pad CLI — never assumes a shell. Lean on the
    TASK-1510/1511/1512 mutation primitives shipped earlier in
    PLAN-1496.
  - onboardPlaybookArguments — mode (enum), defaults (flag),
    skip-codebase (flag). Mirrors the body's ## Arguments section
    for the strict CLI parser.
  - OnboardPlaybook() — LibraryPlaybook constructor.
- internal/collections/playbook_library.go: register OnboardPlaybook()
  in the agent-workflows category alongside ship/plan/decompose.
- internal/collections/playbook_library_test.go:
  - TestPlaybookLibrary_InvokableEntriesPresent now expects 4
    invokable entries (was 3) and includes onboard in wantSlugs.
  - New TestOnboardPlaybook_Contract locks the design contract:
    invocation_slug=onboard, trigger=manual (compatible with the
    blank template's minimal vocab), mode/defaults/skip-codebase
    argument shape, and presence of the "ADAPT, DON'T CURATE"
    posture in the body.

Design notes captured at top of playbook_library_onboard.go:
  1. Adapt, don't curate — library entries are starting points,
     rewrite using the project's actual commands.
  2. Surface-agnostic — describe intent, not specific CLI commands;
     pure MCP users must follow the same flow.
  3. Mode-aware — blank/audit/revisit/defaults paths.
  4. Confirmation before mutation.
  5. Self-removing nudge — the playbook produces user-created items
     which clear the bootstrap onboarding flag (TASK-1504, separate).

Parent: PLAN-1496. Unblocked by TASK-1497 + TASK-1510/1511/1512.

* fix: auto-seed onboard playbook + correct CLI form in body (Codex round 1)

Addresses two PR #576 findings:

1. P1 — folding TASK-1500 into this PR: without auto-seed, the
   library entry alone makes /pad onboard manually-activatable but
   not invokable on day one. Codex correctly flagged that the PR as
   originally drafted shipped a half-feature.

   Wiring (PLAN-1496 / TASK-1500):
   - OnboardSeedPlaybook() in playbook_library_onboard.go returns
     the playbook as a SeedPlaybook with status=active,
     trigger=manual, scope=all, invocation_slug=onboard,
     arguments=onboardPlaybookArguments. Body + args are shared
     with the library entry (same pattern ShipPlaybook uses for
     ship) so they cannot drift.
   - SeedCollectionsFromTemplate appends OnboardSeedPlaybook to
     EVERY workspace created with a non-empty templateName —
     blank, startup, scrum, product, hiring, interviewing, demo.
     The empty-templateName path is preserved as the explicit
     backward-compat escape hatch (tests + direct API callers
     that want a bare workspace with zero items). cmd/pad/init.go
     always supplies a non-empty template (interactive picker or
     defaultTemplateName), so real user-facing workspace creation
     always lands in the seeded branch.

   Tests:
   - TestSeedFromTemplateAlwaysIncludesOnboardPlaybook walks all
     six real templates and confirms the onboard playbook is
     seeded into each.
   - TestSeedWithEmptyTemplateNameSkipsOnboard locks the
     escape-hatch invariant.
   - TestSeedFromBlankTemplate updated: blank workspace now ships
     exactly one item (the onboard playbook) instead of zero,
     because that's TASK-1500's whole point.

2. P2 — the body referenced 'pad library list-conventions', which
   doesn't exist. Corrected to 'pad library list --type conventions'
   (the actual CLI form), with a parenthetical pointing MCP users
   at pad_meta.action: bootstrap for the same data.

This PR now covers both TASK-1499 (author playbook) and TASK-1500
(auto-seed) — combining them because Codex's P1 made it clear they
ship together or not at all.

* docs: correct MCP library-browse fallback in onboard body (Codex round 2)

P2 finding on PR #576: the body told MCP-only users to read the
convention library via 'pad_meta.action: bootstrap'. Bootstrap
returns workspace STATE (collections, conventions, playbooks
actually present in the workspace), not the global library
catalog. So MCP users following that instruction would see only
what's already activated, not what they could activate.

The honest answer is that there is no MCP library-browse surface
today. Updated the body to say so explicitly: if the agent has a
shell, use 'pad library list'; if not, work from domain knowledge
and have the user paste any library bodies they want as starting
text.

Captured the underlying gap as IDEA-1514 (Expose library catalog
via MCP) and linked from the playbook body. Three options outlined
there: new pad_library tool, pad_meta.action: library, or embed in
bootstrap.

Parent: PLAN-1496.
2026-05-17 11:52:20 -04:00
xarmian cc0b1c0bf3 feat(templates): finalize 'blank' template with minimal-vocab seeds for /pad onboard (TASK-1498) (#575)
* feat(templates): finalize 'blank' template for /pad onboard flow (TASK-1498)

A blank template entry was already present in templates.go (drafted
for IDEA-1479) but its seeded trigger/scope vocabularies leaked the
software domain — on-commit, on-pr-create, on-implement, etc., baked
into a template whose whole point is being domain-agnostic. The
/pad onboard playbook (PLAN-1496 / TASK-1499) needs a true blank
starting point so the interview can broaden vocabulary to match the
project's actual domain, whatever it is.

This commit:

- Replaces the software-flavored seed with minimal vocab: trigger=
  always for conventions, trigger=manual for playbooks, scope=all
  on both. The constants live in templates_blank.go so future tweaks
  to the seed surface have a focused diff. The agent broadens via
  pad collection update (TASK-1510) during onboarding.
- Updates the template's description and icon to point at the
  onboard flow ("Empty workspace — run /pad onboard to build it out",
  sparkles instead of memo).
- Adds an in-place comment explaining the design choice so the next
  reader doesn't re-leak software triggers into the seed.
- New test: TestBlankTemplateUsesMinimalVocabularies locks the
  minimal-seed posture; any regression that adds domain-flavored
  triggers fails this test and triggers a fresh design conversation.

Pre-existing IDEA-1479 tests (Shape, ExcludesSoftwareCollections,
AppearsInPicker) still pass — the contract they describe is
preserved (2 system collections only, no user-facing leaks, Custom
group placement).

Parent: PLAN-1496.

* fix(test): blank-vocab assertions use literal slices, not the vars they came from (round 1)

P3 finding on PR #575: TestBlankTemplateUsesMinimalVocabularies
compared template output to BlankConventionTriggers /
BlankPlaybookTriggers — the same vars used to build the template.
Widening either var would silently widen the "minimal" definition
and the test would still pass, defeating the drift-guard intent.

Switched to literal expected slices. Now any change to the var that
adds a domain trigger fails the test loudly.
2026-05-17 07:52:58 -04:00
xarmian f5579300fb feat(cli,mcp): expose 'collection update' via CLI and MCP catalog (TASK-1510) (#572)
* feat(cli,mcp): expose 'collection update' via CLI and MCP catalog (TASK-1510)

The HTTP handler at handlers_collections.go::handleUpdateCollection
already supported PATCHing a collection's name, icon, description,
prefix, schema, settings, and sort_order (plus field-value migrations).
The CLI and MCP surfaces never exposed it, so agents couldn't rename
collections, swap icons, or reshape schemas — a hard blocker for the
adaptive /pad onboard playbook (TASK-1499) which needs to rewrite
seeded collections to match each project's actual vocabulary.

This wires both agent-facing surfaces to the existing handler:

- cmd/pad: new 'pad collection update <slug>' Cobra subcommand with
  --name / --icon / --description / --prefix / --schema / --fields /
  --sort-order flags. Only flags explicitly set are sent (uses
  cmd.Flags().Changed); --schema and --fields reuse the existing
  collectionSchemaJSONFromFlags helper so DSL parity stays.

- internal/mcp/catalog_collection: add 'update' action plus the
  slug, prefix, and sort_order params on padCollectionTool.

- internal/mcp/dispatch_http_routes: new mapCollectionUpdate handles
  the schema-object-vs-string coercion. The catalog declares schema
  as a JSON object for MCP ergonomics, but
  models.CollectionUpdate.Schema is *string — and its UnmarshalJSON
  only flexes settings, not schema. The mapper re-marshals object
  input to its JSON-string form before sending, symmetric to what
  the CLI does via collectionSchemaJSONFromFlags.

Tests cover canonical body, schema-object-to-string coercion
(round-trip through CollectionUpdate.UnmarshalJSON), schema-string
pass-through, empty-field omission, and required-arg validation.
catalog_readonly_test bijection + liveCmdhelpDoc fake updated.

Parent: PLAN-1496.

* fix(mcp): collection update — clear-on-empty + fields DSL parity per Codex review (round 1)

Addresses two P2 findings on PR #572:

1. The catalog advertises `icon=""` / `description=""` / `prefix=""`
   as clear-the-field, and the CLI flag help says the same, but the
   HTTP mapper filtered empty strings via `v != ""` — leaving MCP HTTP
   callers unable to clear fields the CLI can. Switched to key-presence
   semantics for the four string fields so explicit empty strings
   round-trip to the store (which honors *string("") as "clear").

2. The catalog advertises `fields OR schema` as mutually exclusive
   (mirroring `pad collection create`), but the mapper only consumed
   `schema`. An MCP HTTP request with `fields=...` produced a `{}` PATCH
   body silently. Extracted the DSL parser to a shared package
   (internal/collections/dsl.go::ParseFieldsDSL + FieldsDSLToSchemaJSON)
   so the CLI and the mapper share one parser; mapper now resolves
   fields-or-schema with the same mutual-exclusion guard the CLI has.

Tests added in dispatch_http_routes_extras_test.go:
- TestMapCollectionUpdate_EmptyStringClearsField
- TestMapCollectionUpdate_AcceptsFieldsDSL (round-trips through
  models.CollectionSchema to confirm the parsed shape)
- TestMapCollectionUpdate_RejectsFieldsAndSchemaTogether

cmd/pad/main.go's parseFieldsDSL becomes a one-line alias for
collections.ParseFieldsDSL so the CLI's behavior stays identical.

Parent: PLAN-1496, fixing PR #572 / TASK-1510.

* fix(mcp): collection update — use encodeSchemaForBody + normalize empty schema (round 2)

Addresses two more findings from Codex round 2 on PR #572:

1. P2: mapCollectionUpdate bypassed encodeSchemaForBody, so structured
   schemas didn't get label backfill and string schemas weren't
   validated before PATCH — diverged from collection create + CLI.
   Now reuses encodeSchemaForBody (the same encoder collection create
   uses at dispatch_http_routes.go:418), getting label-backfill via
   the Title-Case-of-key heuristic and shape validation for free.

2. P3: schema=null or schema="" plus a real fields=... update tripped
   the mutual-exclusion check. Now normalizes empty inputs as absent
   BEFORE checking exclusivity, matching the relaxed handling
   collection create has for optional empty params.

Tests:
- Renamed TestMapCollectionUpdate_PassesSchemaStringVerbatim to
  TestMapCollectionUpdate_AcceptsSchemaString — the new property is
  round-trip parity + label backfill, not verbatim pass-through.
- New TestMapCollectionUpdate_EmptySchemaDoesNotBlockFields covers
  both nil and empty-string schema combined with a real fields value.

Parent: PLAN-1496, addressing Codex round 2 on PR #572 / TASK-1510.
2026-05-17 01:45:40 -04:00
xarmian 7c663a3d3f feat(collections): add blank workspace template + retire auto-upgrade hook (IDEA-1479) (#560)
* feat(collections): add blank workspace template (IDEA-1479)

Introduces a `blank` workspace template that seeds only the two system
collections (Conventions, Playbooks) — no Tasks/Ideas/Plans/Docs, no
seeded items, no starter conventions or playbooks. Solves the
agent-self / non-template-fit use case where the existing software
templates leave undeletable ghost collections in the workspace.

Adds a new `CategoryCustom` ("Custom") top-level category so the blank
template doesn't mis-group with `startup` / `scrum` / `product`.
Category is appended last in `CategoryOrder` so it doesn't displace
recommended-path templates in the picker.

Tests:
- TestBlankTemplateShape — exactly 2 system collections, no seeds.
- TestBlankTemplateExcludesSoftwareCollections — no tasks/ideas/plans/docs.
- TestBlankTemplateAppearsInPicker — surfaces under a Custom group.
- TestSeedFromBlankTemplate — bootstrapping produces 2 collections, 0 items.

* fix: address codex review for blank template (IDEA-1479)

- CreateWorkspaceModal: remove hard-coded 'blank' picker entry that
  silently fell through to collections.Defaults(). The API-driven blank
  template (under the Custom category) is now the canonical surface.
- Dashboard: gate '+ New Task' button on tasks collection existence so
  blank workspaces don't render a button that targets a missing
  collection.
- OnboardingChecklist: accept collectionSlugs prop and filter steps
  whose target collection (plans/tasks/docs) is absent. Conventions
  step remains unconditional since the conventions collection ships
  with every template, including blank. Empty-steps guard added to
  progressPct to avoid NaN.
- web/src/lib/utils/templates.ts: add 'custom' -> 'Custom' to mirror
  the Go CategoryOrder + categoryLabels updates.
- cmd/pad/templates_picker_test.go: extend the visible-template
  assertion list to include 'blank' and assert the Custom category
  header renders.

* fix(store): gate SeedDefaultCollections on zero-collection workspaces (IDEA-1479)

The server's startup auto-upgrade hook (cmd/pad/main.go) called
SeedDefaultCollections against every workspace at boot. That hook
dates to the initial release — long before workspace templates
existed — and was written as a backfill for workspaces created
before tasks/ideas/plans/docs landed in Defaults().

Post-templates, the hook unconditionally re-materialized the
Software-template collections into any workspace missing them —
including blank-template workspaces (IDEA-1479), which ship only
Conventions + Playbooks by design. Result: every restart silently
regrew the ghost user-facing collections the blank template was
explicitly built to avoid.

Fix: SeedDefaultCollections now returns nil immediately when the
workspace has any existing collection (system or user-facing). The
rescue path still triggers for genuinely-empty workspaces, preserving
the original backfill intent.

Tests:
- TestBlankWorkspaceSurvivesSeedDefaultCollections — blank workspace
  remains 2 collections after auto-upgrade (and after a second pass).
- TestEmptyWorkspaceStillGetsDefaults — zero-collection workspace
  still gets the full Software default set.

* refactor(server): remove SeedDefaultCollections auto-upgrade at startup (IDEA-1479)

The startup auto-upgrade hook in cmd/pad/main.go dated to the initial
release, predating workspace templates entirely. Its original intent
was per-collection backfill — workspaces created before a new entry
landed in Defaults() would acquire it on next boot. Post-templates,
that semantic is incompatible with templates that legitimately
diverge from Defaults() (e.g. `blank`, which ships only Conventions
+ Playbooks by design).

Round-2 of the IDEA-1479 review attempted to keep the hook by adding
a "zero collections" guard, but Dave (after codex round 3) decided
the cleanest fix is removing the hook entirely. The codebase has
proper migration infrastructure now; any future "add a default
collection" work should land as an explicit migration where the
author chooses which workspaces to backfill.

SeedDefaultCollections itself is preserved (with the round-2 guard)
as a building block for any future explicit rescue command or
migration. Its doc comment is updated to note it's no longer
auto-invoked at startup. The round-2 regression tests
(TestBlankWorkspaceSurvivesSeedDefaultCollections,
TestEmptyWorkspaceStillGetsDefaults) still apply and pass unchanged.

* fix(store): rescue gate uses COUNT(*), not ListCollectionsMinimal (IDEA-1479)

Postgres CI on PR #560 caught a regression introduced in commit 3e71fe8:
SeedDefaultCollections's zero-collection guard called
ListCollectionsMinimal, whose SELECT uses COALESCE(settings, '') against
a JSONB column. Postgres parses the '' literal as JSON at plan time
and fails with SQLSTATE 22P02 (invalid input syntax for type json),
breaking the rescue gate and ~12 cascade test fixtures that depend on
the seeder succeeding.

The gate only needs to know whether any collection exists, not their
schema or settings. Switch to a direct COUNT(*) on the collections
table: portable across both drivers, cheaper than the minimal lister,
and avoids the broken JSON COALESCE path entirely.

Verified locally against both drivers:
  - SQLite (default): go test ./... — all PASS
  - Postgres (make test-pg infra):
    PAD_TEST_POSTGRES_URL=... go test ./... — all PASS, including
    the three direct failures (TestBlankWorkspaceSurvives…,
    TestEmptyWorkspaceStillGetsDefaults, TestSeedDefaultCollections)
    and the cascade FTS/search fixtures.

Note: ListCollectionsMinimal's COALESCE(settings, '') expression
appears to also affect production callers (handlers_dashboard,
handlers_items) on Postgres, but fixing that is out of scope for
this PR — those paths have their own tests that aren't failing in CI.
Flagged for separate follow-up.
2026-05-15 14:46:26 -04:00
xarmian 1db0e6a505 docs+test: closes the PLAN-1397 library overhaul loop (TASK-1404) (#533)
* docs+test: closes the PLAN-1397 library overhaul loop (TASK-1404)

## Tests

New `internal/collections/playbook_library_test.go` with 4 regression
guards for the invokable-first library:

- TestPlaybookLibrary_InvokableEntriesPresent — asserts ship + plan +
  decompose are all present by invocation_slug, that each has at
  least one Argument declared, and that at least 3 invokable entries
  exist. Catches T1 widening getting half-reverted or T6's library
  rebuild dropping an entry.
- TestPlaybookLibrary_AllTriggersKnown — asserts every library
  entry's Trigger is one of the canonical values (manual,
  on-implement, on-review, on-plan, on-triage, on-release,
  on-deploy, on-pr-create, on-task-complete, always). Catches typos
  that would seed an invalid trigger.
- TestPlaybookLibrary_ShipBodyShared — confirms the library `ship`
  entry and ShipPlaybook() seed share the same body constant. The
  whole point of T3 was to avoid duplication; this prevents drift.
- TestPlaybookLibraryArchive_BodiesCompiled — keeps archivedPlaybooks()
  greppable and verifies the canonical retired title ("Implementation
  Workflow") is still in the archive.

## CLAUDE.md

- Added a "Library — discovery surface" subsection to the Playbooks
  section explaining the invokable-first lineup, the archive, and
  how `softwareStarterPlaybookTitles` seeds plan + decompose into
  software workspaces.
- Expanded "Code map" with the four new/refactored library files
  (playbook_library.go, playbook_library_plan.go,
  playbook_library_decompose.go, playbook_library_archive.go).
- Cross-referenced PLAN-1397 alongside PLAN-1377 for design history.

## skills/pad/SKILL.md

- "Planning: 'Let's create a plan'" and "Decomposition: 'Break plan
  X into tasks'" now name `/pad plan` and `/pad decompose` as the
  canonical entry points, with the inline workflow as the fallback
  when those playbooks aren't activated in the workspace.
- The high-level "Planning:" intent list at the top of the routing
  section updated to match.
- Note: SKILL.md is embedded into the Go binary at build time. The
  next `make install` distributes the skill update; the .agents/
  copy regenerates automatically.

Parent: PLAN-1397. Closes the loop — all 7 tasks of the playbook
library overhaul shipped.

* fix(test): source knownPlaybookTriggers from SoftwarePlaybookTriggers per Codex review (round 1)

Round 1: the hand-maintained `knownPlaybookTriggers` map admitted
`on-pr-create`, `on-task-complete`, and `always` — all valid in
the schema for *conventions*, but NOT in `SoftwarePlaybookTriggers`
(templates.go:266). A library entry could ship one of those and
the regression test would pass, but the same trigger would be
rejected when seeded into a software workspace at activation.

Fix: derive the test's known-set from `SoftwarePlaybookTriggers`
directly. If the schema widens or narrows, the test follows
automatically — no drift between the assertion baseline and the
actual schema. Also added a note clarifying that templates with
domain-specific trigger vocabularies (e.g. hiring's
on-candidate-advance) would need their own library scoped to
that template.

The set is now exactly: manual, on-implement, on-triage,
on-release, on-plan, on-review, on-deploy.
2026-05-13 07:17:58 -04:00
xarmian 8fa1dd36f9 refactor(library): archive 9 pre-PLAN-1377 playbook bodies, rebuild library as invokable-first (TASK-1403) (#532)
Retires the legacy trigger-only library entries from the public
surface and replaces the 4-category structure with a single
`agent-workflows` category housing the three invokable workflow
playbooks (ship, plan, decompose).

## Changes

- New file `internal/collections/playbook_library_archive.go` —
  holds all 9 retired bodies in package-private `archivedPlaybooks()`.
  Bodies stay compiled so they're greppable and refactor-safe;
  per-entry "convert to invokable" / "promote to convention" /
  "retire" decisions are tracked in IDEA-1396. `var _ = archivedPlaybooks`
  keeps the symbol referenced for unused-symbol linters.

- `internal/collections/playbook_library.go::PlaybookLibrary()` —
  removed the 4 categories (workflow, planning, quality, operations)
  and the 9 bodies inline. Replaced with a single `agent-workflows`
  category containing ship + plan + decompose (the invokable trio
  landed in T3/T4/T5). All three carry InvocationSlug and Arguments
  so the library teaches the PLAN-1377 invocation model from the
  first card.

- `internal/collections/playbook_library_plan.go` /
  `playbook_library_decompose.go` — bump each helper's `Category`
  field from `workflow` to `agent-workflows` to match the new
  registry grouping.

- `internal/collections/templates.go::softwareStarterPlaybookTitles` —
  updated from the retired pair ("Implementation Workflow", "Code
  Review Process") to the new invokable pair ("Plan a new
  initiative", "Decompose a plan into tasks"). `startup` template
  separately prepends `ship` (templates.go:~441), so every software
  workspace now seeds the full invokable trio from day one.

- `internal/mcp/dispatch_http_slice4_test.go` — the activate-by-title
  fixture used "Implementation Workflow"; switched to "Ship tasks"
  (still a real library entry with trigger+scope in its activation
  payload). T6's verify section flagged this fixture; addressing it
  here keeps the build green on this branch rather than deferring
  the breakage to T7.

- `cmd/pad/main.go` — `pad library activate --help` example used
  "Implementation Workflow" as a sample title; switched to "Ship
  tasks" so the example still resolves.

## Verify

- `go build ./...` clean (no dangling references to the 9 titles in
  production code paths).
- `go vet ./...` clean.
- `go test ./...` — all packages green.
- `grep -r "Implementation Workflow" --include="*.go" --include="*.ts"
   --include="*.svelte"` returns only:
  - `playbook_library_archive.go` (expected — the archive)
  - `playbook_library_plan.go` (historical comment, intentional)
  - `templates.go:868` (demo-workspace seed item content — unrelated
    to library lookup; literal item title in a template, would not
    benefit from being retitled in this PR's scope)

Pre-existing workspaces' already-activated copies of the 9 entries
keep working — they live in workspace data, not library code. Only
future activations are affected (the legacy titles no longer resolve
via `pad library activate` or the web Library UI).

Parent: PLAN-1397. Depends on T3/T4/T5 — the library is never empty
because ship, plan, and decompose are already in place.
2026-05-13 07:11:01 -04:00
xarmian 8eb927cbbc feat(library): author the decompose invokable playbook (TASK-1402) (#531)
New library entry: `/pad decompose <PLAN-ref>` — takes a plan and
creates the child task items its body implies, with the user's
approval at every step. The natural follow-up to `/pad plan`:
that playbook creates the plan; this one turns the breakdown into
actionable items linked back via --parent.

Code shape follows the templates_startup_ship.go pattern:

- internal/collections/playbook_library_decompose.go (new) — holds
  decomposePlaybookBody + decomposePlaybookArguments + a
  DecomposePlaybook() helper.
- internal/collections/playbook_library.go — registers
  DecomposePlaybook() in the workflow category right after PlanPlaybook().

Arguments:
- target (required, ref) — the plan to decompose
- dry-run (flag, default=false) — propose without creating
- collection (optional, string, default=tasks) — for non-default
  workspaces (bugs, work-items, etc.)

Body walks: load plan + existing children → analyze the body for
actionable work → reconcile against existing children → propose
task list with priorities and dependency hints → confirm in bulk
→ create approved tasks → wire dependencies via `pad item block`
→ report with suggested next moves.

Mirrors skills/pad/SKILL.md's "Decomposition: 'Break plan X into
tasks'" workflow so the conversational and invokable forms stay
in lockstep (T7 covers SKILL.md sync).

Closes the loop opened by T4: `/pad plan` step 7 now has a real
`decompose` playbook to delegate to when it's activated in the
workspace.

Parent: PLAN-1397.
2026-05-13 07:01:49 -04:00
xarmian ad3926f643 feat(library): author the plan invokable playbook (TASK-1401) (#530)
* feat(library): author the plan invokable playbook (TASK-1401)

New library entry: `/pad plan <topic>` — a conversation-first
playbook for co-designing a new plan with the user. Subsumes the
pre-PLAN-1377 "Plan Creation" library entry, reframed as a
structured invokable procedure with explicit arguments.

Code shape follows the templates_startup_ship.go pattern:

- internal/collections/playbook_library_plan.go (new) — holds the
  body + argument spec as package-level constants and exports
  PlanPlaybook() LibraryPlaybook.
- internal/collections/playbook_library.go — registers
  PlanPlaybook() in the workflow category right after ship.

Arguments (mirroring the body's `## Arguments` section):
- topic (required, string)
- parent (optional, ref)
- collection (optional, string, default=plans)

Tone is generic across project types — no software-only vocabulary,
no Pad-specific assumptions. A research workspace can plan an
experiment; a hiring workspace can plan a recruiting push; the
structure adapts via the conversation. Mirrors the "Planning: 'Let's
create a plan'" workflow in skills/pad/SKILL.md so the conversational
and invokable forms stay in lockstep (T7 docs pass calls this out).

The body's step 7 invites the user to follow up with
`/pad decompose <new-plan-ref>` — T5 (TASK-1402) authors that
playbook next.

Parent: PLAN-1397.

* fix(library): plan playbook handles ref-form quick-action invocation, softens /pad decompose forward-reference per Codex review (round 1)

Round 1 P2: /pad decompose forward-reference in step 7 would resolve to
an unknown playbook until T5 (TASK-1402) merges. Reworded step 7 to
check whether a `decompose` playbook is activated and fall back to
inline `pad item create task` calls otherwise. Body now works
regardless of whether T5 has landed.

Round 1 P3: existing UI quick-action prompt (defaults.go:157) emits
`/pad plan {ref} "{title}" — outline goals, deliverables, and
timeline` on plan-collection cards. With my original topic-first arg
shape, the agent's NL dispatcher would bind the ref as `topic` and
treat the title as an extra positional, getting the wrong invocation
mode.

Fix: documented the dual-purpose `topic` argument explicitly. New
"## Dispatch" section in the body tells the agent to detect:
- First positional matches `^[A-Z]+-\d+$` AND resolves to a real
  item → elaborate mode (load item, work with user to expand it,
  update via `pad item update <ref> --stdin`)
- Otherwise → create-new mode (full conversation flow below)

planPlaybookArguments updated to mirror: topic now described as
"string OR ref", parent/collection noted as create-new-mode only.
Strict CLI parsing remains string-typed; the dual semantics are
agent-interpreted per PLAN-1377's design.

The existing "Plan this" quick-action prompt now routes correctly
through the elaborate path without needing changes to defaults.go.

* fix(library): align plan playbook philosophy section with step 7 fallback per Codex review (round 2)

Round 2 P3: Philosophy said "never create tasks here" which
contradicted step 7's new inline-task-creation fallback (added in
round 1 to handle the case where no decompose playbook is active).
Reworded to permit the fallback explicitly while keeping the
"don't create before user approval" rule intact.
2026-05-13 06:57:29 -04:00
xarmian 8efcc21ef5 feat(library): add ship playbook to library, shared body with startup seed (TASK-1400) (#529)
The headline invokable example from PLAN-1377 (`/pad ship <args>`) was
previously only seeded into `startup`-template workspaces. Library
discovery is the canonical place users learn what invokable playbooks
are, so `ship` now lives there too — available to any workspace
template (`scrum`, `product`, `hiring`, `interviewing`, future ones).

Single source of truth: the library entry references the same
package-level `shipPlaybookBody` + `shipPlaybookArguments` constants
that ShipPlaybook() uses. Updates to the playbook body live in one
place; both surfaces (library activation + startup seed) consume it.

The title "Ship tasks" matches ShipPlaybook()'s SeedPlaybook so the
library UI's title-keyed `activePlaybookTitles` check renders the
entry as "Active" in startup workspaces where it's already seeded —
no duplicate activation, no behavior change.

Placement: top of the existing `workflow` category. T6 will rearrange
the category structure when retiring the legacy entries; for now
adding to workflow is the least-intrusive prominent placement.

Smoke-tested locally that ship resolves through PlaybookLibrary() with
the expected title, trigger, args count, and shared content; the
permanent regression test ships in T7.

Parent: PLAN-1397.
2026-05-13 06:43:04 -04:00
xarmian 4bbd0a210d feat(collections): widen LibraryPlaybook with InvocationSlug + Arguments (TASK-1398) (#527)
* feat(collections): widen LibraryPlaybook with InvocationSlug + Arguments (TASK-1398)

Adds two optional fields to LibraryPlaybook:
- InvocationSlug — kebab-case slug for `/pad <slug>` routing (PLAN-1377)
- Arguments — argument spec mirroring the body's `## Arguments` section

Both fields are tagged with `omitempty` so existing library entries
(none of which set them) serialize unchanged. seedPlaybookFromLibrary()
now forwards both into the seeded item's Fields JSON only when set,
matching the shape ShipPlaybook() already writes.

This is the foundational task that unblocks T2 through T6 of the
playbook library overhaul.

Parent: PLAN-1397.

* fix(library): propagate invocation_slug + arguments through activation paths per Codex review (round 1)

Codex round 1 caught that the activation paths for library playbooks
rebuild the fields map and drop the new fields, so any library entry
declaring invocation_slug/arguments would lose `/pad <slug>` routing
after activation.

Fixed in three places:
- internal/cli/client.go LibraryPlaybook (the client-side mirror used
  by the CLI `pad library activate` command)
- cmd/pad/main.go libraryActivate (CLI subprocess activation)
- internal/mcp/dispatch_http_slice4.go dispatchLibraryActivate (MCP
  pad_project action=library-activate)

All three now forward invocation_slug and arguments only when set,
matching ShipPlaybook()'s shape exactly.

Web client (web/src/lib/api/client.ts) activation payload is T2's
explicit scope — left for that PR.
2026-05-13 00:26:09 -04:00
xarmian 936145c524 feat(collections): seed generic ship playbook in startup template (TASK-1386) (#523)
* feat(collections): seed generic ship playbook in startup template (TASK-1386)

Adds the de-personalized `ship` playbook to the startup template's seeded
playbooks slice, the headline example of PLAN-1377's playbook invocation
model. A fresh `pad workspace init --template startup` workspace now ships
PLAYB-N with invocation_slug=ship, ready to drive via `/pad ship <args>`
or `pad playbook run ship`.

Body is derived from the personal /ship-tasks slash command, with
project-specific bits (Codex CLI, branch naming, commit format, build
commands) marked customizable so the playbook reads cleanly on day one
without forcing the seed user to wire two playbooks together to ship
anything. ## Arguments section in the body mirrors the structured
arguments JSON field so the web UI editor (TASK-1384) can keep them
in sync.

Parent: PLAN-1377.

* fix(collections): align ship playbook target/limit with strict parser per review (round 1)

Codex round 1 findings:

1. target docstring said "space or comma separated" task refs, but the
   structured spec only has one positional 'target'. The strict CLI/MCP
   parser would bind TASK-10 then reject TASK-11. Updated body to:
   - declare 'comma-separated list' in the ## Arguments line
   - note that the agent's NL parser collapses space-separated refs to
     comma form before binding (so '/pad ship TASK-10 TASK-11' still works)

2. limit said 'default=∞' in markdown but had no default in the structured
   arguments JSON. Removed the bogus default — limit is now genuinely
   optional with 'unset = no limit', so the markdown and the spec agree.

Parent: TASK-1386 / PLAN-1377.
2026-05-12 20:16:28 -04:00
xarmian c38b3bf5cd feat(playbooks): add invocation_slug + arguments schema fields (TASK-1378) (#517)
* feat(playbooks): add invocation_slug + arguments schema fields (TASK-1378)

Foundational change for PLAN-1377 — playbooks become first-class invokable
procedures. Two new optional fields land on the Playbooks collection
schema:

- `invocation_slug` (text, kebab-case, unique-per-workspace among non-null
  values): enables `/pad <slug>` direct invocation. Nullable so
  trigger-only playbooks (e.g. on-release checklists) don't need one.
- `arguments` (json, array of {name, type, required, default, description}):
  declares the playbook's argument contract; mirrors the body's
  `## Arguments` section in queryable form.

Plumbing pieces:

- `models.FieldDef` grows two general-purpose options — `Pattern` for
  regex validation and `UniqueScope` for collection-level uniqueness.
  Both are opt-in; existing schemas are unaffected.
- `items.ValidateFields` learns the `json` field type (accepts any
  JSON-decodable value) and applies `Pattern` to string-typed values.
- `handlers_items.checkUniqueFields` queries `Store.ListItems` to enforce
  `UniqueScope == "workspace_collection"` on create + update.
- Two migrations (SQLite 054, Postgres 033) JSON-patch the playbooks
  schema on existing workspaces so the new fields show up without a
  workspace re-init.
- TypeScript `FieldDef` mirrors the Go side.

Parent: PLAN-1377.

* fix(playbooks): address Codex review round 1 findings (TASK-1378)

P1 — EditCollectionModal now round-trips opaque pattern/unique_scope
metadata. EditableField carries the new keys; the load + save paths
preserve them so re-saving the playbooks collection from the UI doesn't
strip server-side validation rules the modal doesn't yet expose
dedicated controls for. fieldFromDef mirrors the change for templates.

P2 — checkUniqueFields' pre-write ListItems check is now backed by a
partial unique index (idx_items_invocation_slug_per_collection,
SQLite + Postgres) scoped to non-empty, non-deleted rows. The pre-check
still gives users a friendly error message in the common case; the
index closes the TOCTOU race between two concurrent writers. The
create-conflict error message is now generic enough to cover both the
slug constraint and the new invocation_slug index.

P2 — `json` field type now rejects raw strings, numbers, and bools. Only
objects, arrays, and null are accepted, so a generic web text input
can't silently corrupt a structured field by emitting "[]" instead of
an actual array. FieldEditor.svelte routes `json` fields to a
read-only summary in both readonly and edit modes; dedicated editors
(like TASK-1384's playbook editor that owns `arguments`) own the
structured form.

P3 — invocation_slug regex now requires a minimum of two characters
(`^[a-z0-9][a-z0-9-]*[a-z0-9]$`) in the Go const, the SQLite migration,
the Postgres migration, and the validate tests. Single-letter slugs
would shadow plausible NL tokens (e.g. `/pad a ...`) and the doc
comment already claimed the two-char floor; this aligns code with
intent.

Parent: PLAN-1377.

* fix(playbooks): address Codex review round 2 findings (TASK-1378)

P2.1 — checkUniqueFields no longer passes IncludeArchived=true. The
application-layer pre-check now matches the partial unique index's
`deleted_at IS NULL` predicate so a soft-deleted playbook releases its
slug back to the pool and reclaiming it succeeds instead of 409'ing.

P2.2 — handleUpdateItem now maps UNIQUE constraint / duplicate key
errors from UpdateItem to HTTP 409, mirroring the create path. A true
concurrent-update race that slips past checkUniqueFields and trips the
partial unique index used to surface as a misleading 500.

(Not addressed in this round: Codex's third finding — concern about the
partial unique index applying to "every collection" — is, on close
reading, not what the index does. `ON items(collection_id, json_extract(...))`
scopes uniqueness to the (collection_id, slug) pair, so two items in
different collections with the same `invocation_slug` value coexist
fine. The migration-failure risk is theoretical: `invocation_slug` is
a brand-new field key, so no pre-existing items can have it set, and
no migration-time duplicates can exist. If a future custom collection
adopts the same field name, opting into per-collection uniqueness is
exactly the intended semantic of FieldDef.UniqueScope.)

Parent: PLAN-1377.

* fix(playbooks): map restore-path UNIQUE violations to 409 (TASK-1378)

Codex round 3: restoring an archived playbook can hit the partial
unique index on invocation_slug if a replacement item already claimed
the slug. Map UNIQUE constraint / duplicate key errors from RestoreItem
to HTTP 409 with a targeted message, matching the create + update paths.

Parent: PLAN-1377.

* fix(playbooks): map collab-snapshot UNIQUE violations to 409 (TASK-1378)

Codex round 4: the collab-snapshot PATCH branch under
`s.collab.UnderItemLock` ran its own UpdateItem call and fell through
to writeInternalError on any non-stale-snapshot error. A concurrent
edit racing the invocation_slug partial unique index would surface as
500 instead of 409. Mirror the main UpdateItem error mapping.

Codex's other round-4 finding — the partial unique index applying to
"every collection" — is not addressed because the index IS already
collection-scoped: `ON items(collection_id, json_extract(fields,
'$.invocation_slug'))`. Two items in different collections with the
same slug coexist; only same-collection duplicates conflict. Migration
duplicates are impossible because `invocation_slug` is a brand-new
field key with no pre-existing items setting it. A custom collection
that later adopts the same field name opts into per-collection
uniqueness, matching the FieldDef.UniqueScope="workspace_collection"
semantic.

Parent: PLAN-1377.
2026-05-12 17:17:56 -04:00
xarmian abf017c4e7 feat(onboarding): make banner + CLI hint template-aware (TASK-1150) (#409)
The IDEA-1 trigger phrase is no longer hardcoded — fresh scrum
workspaces surface "use pad to get BACK-1", product workspaces surface
"use pad to get FEAT-1", and any future template that ships an
agent-onboarding seed declares its primary ref once and gets the
banner / hint for free.

Mechanism:

  1. WorkspaceTemplate gains an OnboardingPrimaryRef string field —
     the canonical declaration of "this template's IDEA-1-style
     primary entry." Set per template that ships the pattern
     (startup → "IDEA-1", scrum → "BACK-1", product → "FEAT-1");
     left empty for hiring/interviewing/demo where the agent-onboarding
     pattern intentionally doesn't apply.

  2. Server: handleGetDashboard identifies the seeded primary by
     walking allItems looking for item_number=1 + source="template"
     + created_by="system" + collection_slug ∈ {ideas, backlog,
     features}. The collection-slug whitelist is what keeps hiring's
     REQ-1 (also seeded with item_number=1 + source=template) from
     being flagged as an onboarding entry — those are example items,
     not agent scripts. The dashboard response gains an
     onboarding_seed field with ref/title/slug/collection_slug/status
     plus a server-computed `active` boolean (true iff status equals
     the schema initial value).

  3. CLI: printOnboardingHints accepts the template name, looks up
     the primary ref via collections.GetTemplate, and prints the
     right "use pad to get X-1" line. Templates without a declared
     primary skip the line entirely (so hiring's pad init success
     doesn't promise a non-existent BACK-1 / IDEA-1).

  4. Web frontend: dashboard reads dashboard.onboarding_seed,
     gates the banner on `active=true`, passes ref/slug/collection
     to OnboardingIdeaBanner. The component renders the trigger
     phrase, copy button, and "Read it first" deep link from those
     props — no more hardcoded IDEA-1.

ensureWorkspace's signature gains a returned templateName so init.go
+ main.go can pass it through to printOnboardingHints. The five
existing test call sites updated.

New tests:

  internal/collections/templates_test.go
    - TestTemplatesDeclareOnboardingPrimaryRef — locks the per-template
      OnboardingPrimaryRef values (and the explicit emptiness of
      hiring/interviewing/demo).

  internal/server/handlers_dashboard_test.go
    - TestDashboardOnboardingSeed_StartupTemplate
    - TestDashboardOnboardingSeed_ScrumTemplate
    - TestDashboardOnboardingSeed_ProductTemplate
    - TestDashboardOnboardingSeed_HiringTemplate (asserts NO seed —
      hiring's REQ-1 is example data, not an onboarding entry)
    - TestDashboardOnboardingSeed_EmptyWorkspace (no template)

Removes the loadIdeaOne race-guard from +page.svelte — the dashboard
poll itself now carries the onboarding_seed.active flag so the banner
state lives entirely in the dashboard response. Drops ~50 lines of
frontend code.

Parent: PLAN-1146.
2026-05-04 12:05:07 -04:00
xarmian 8fc0cb3b8b feat(collections): seed onboarding items + explicit prefixes for scrum + product templates (TASK-1149) (#408)
* feat(collections): seed onboarding items + add explicit prefixes for scrum + product templates (TASK-1149)

Mirrors TASK-1133's pattern (PR #402) for the remaining software-category
templates. After this lands:

  - fresh `pad workspace init --template scrum`   → BACK-1 / SPRINT-2 / BUG-3 / DOC-4
  - fresh `pad workspace init --template product` → FEAT-1 / FB-2 / ROAD-3 / DOC-4

Each is a first-person note from the workspace owner's future self —
agent-invocable via `/pad let's discuss <REF>`, schema-aware terminal
verbs ("mark me done" / "completed" / "shipped" / "archived"), no
"tutorial" / "lesson" language. Bodies pulled verbatim from
DOC-1152 (scrum) and DOC-1153 (product).

Precondition fix: explicit Prefix set on five collections so DerivePrefix
doesn't yield awkward refs:

  Backlog       BACKL  → BACK
  Sprints       SPRIN  → SPRINT
  Features      FEATU  → FEAT
  Feedback      FEEDB  → FB
  Roadmap Items RI     → ROAD

Mirrors hiring template's pattern of explicit prefixes on its custom
collections. Existing scrum/product workspaces (forward-only fix) keep
their derived prefixes — the seeder doesn't migrate.

New tests:

  internal/collections/templates_test.go
    - TestScrumOnboardingItemsOrderAndShape
    - TestProductOnboardingItemsOrderAndShape
    - TestScrumProductTemplatesShipOnboardingSeedItems
    - TestScrumProductTemplatesUseExplicitFriendlyPrefixes (locks
      the prefix-fix precondition)

  internal/store/items_test.go
    - TestSeedCollectionsFromTemplateScrumRefSequence
    - TestSeedCollectionsFromTemplateProductRefSequence
      (Both also assert the prefix lands on each seeded item — drift
       in templates.go would surface here as a test failure pointing
       at the PLAN-1146 prefix precondition.)

Existing onboarding test (TestSeedCollectionsFromTemplateStartupRefSequence)
still passes — startup template untouched.

Parent: PLAN-1146. Source content: DOC-1152, DOC-1153.

* docs(comments): clarify the post-signup hint is wired in TASK-1150, not this PR (Codex review round 1)

Codex flagged that the helper-file + templates.go comments said things
like "the post-signup hint will name BACK-1" — which read as "it does
today" but actually means "it will once TASK-1150 lands." Until that
ships, the dashboard banner and CLI hint still hardcode IDEA-1 from
PR #403, so a fresh scrum/product workspace gets the seeded items but
no UI prompt that names them.

Comments now explicitly call out the in-flight state so readers
between this PR and TASK-1150 know what's wired and what isn't.

No behavior change.
2026-05-04 11:41:29 -04:00
xarmian 96253f18a2 feat(collections): seed IDEA-1/PLAN-2/TASK-3/DOC-4 in startup workspaces (TASK-1133) (#402)
* feat(collections): seed IDEA-1/PLAN-2/TASK-3/DOC-4 in startup workspaces (TASK-1133)

A fresh `pad workspace init --template startup` now seeds four onboarding
items — one per user-facing collection — that any agent can fetch and
meaningfully converse around. The post-signup hint will name IDEA-1
specifically, but PLAN-2 / TASK-3 / DOC-4 are all viable entry points
for `/pad let's discuss <REF>`.

The bodies are first-person notes from the workspace owner's future self
that introduce each collection's purpose by inviting a real conversation
about the user's project — no marker, no skill detection, no schema
fields. Word-audit clean: no "tutorial / lesson / step / walkthrough".
Bodies pulled verbatim from DOC-1139.

Sequence-stability: the existing seeder loop in store.SeedCollectionsFromTemplate
already runs SeedItems before conventions/playbooks, so the workspace-scoped
item_number sequence naturally lands at IDEA-1 / PLAN-2 / TASK-3 / DOC-4.
A dedicated test (TestSeedCollectionsFromTemplateStartupRefSequence) locks
the invariant down — drift means the post-signup hint silently misfires.

Scope: startup template only. Scrum and product templates have different
collection sets (Backlog/Sprints/Bugs and Features/Feedback/Roadmap
respectively) and need their own bodies — tracked as follow-up under
PLAN-1131. People-category templates (hiring, interviewing) are PLAN-1140.

Parent: PLAN-1131. Source content: DOC-1139.

* fix(collections): use schema-valid terminal statuses in onboarding bodies per Codex review (round 1)

The seed bodies told agents to "mark me done" but ideas/plans/docs don't
have a `done` terminal status — the HTTP/MCP update path validates select
options, so an agent following the seeded copy would hit a validation
error instead of completing the seed item.

- IDEA-1: "mark this idea done" → "mark this idea implemented"
  (Ideas terminal: implemented|rejected)
- PLAN-2: "mark me done" → "mark me completed"
  (Plans terminal: completed)
- TASK-3: unchanged — "done" is the canonical terminal for Tasks
- DOC-4: "mark me done" → "archive me"
  (Docs terminal: archived)

Caught by Codex on PR #402. Same hard validation path the rest of the
app honors — the seed copy needs to be schema-aware.
2026-05-04 09:10:31 -04:00
xarmian d84f1180a7 feat(mcp): pluggable Dispatcher + HTTPHandlerDispatcher for remote MCP (TASK-965) (#343)
* feat(mcp): pluggable Dispatcher + HTTPHandlerDispatcher for remote MCP (TASK-965)

Architectural prerequisite for PLAN-943's remote MCP at /mcp. The
existing ExecDispatcher (PLAN-942) shells out to the pad binary and
inherits credentials from ~/.pad/credentials.json — fine for local
stdio MCP where the user IS the subprocess owner, but unworkable for
a multi-tenant /mcp endpoint where the dispatcher must serve many
OAuth-authenticated users from a single process.

This PR ships the alternative path: HTTPHandlerDispatcher calls
pad-cloud's existing HTTP handler chain in-process, with the
requesting user attached via context. Same handlers, same audit /
event-bus / webhook plumbing — just no fork().

## What's in

- internal/mcp/dispatch.go: keeps the existing Dispatcher interface
  (so ExecDispatcher unchanged) and adds a context-keyed
  WithDispatchInput helper. The registry attaches the original JSON
  input map to the dispatch context so dispatchers that prefer
  structured data over reverse-parsed cliArgs can use it.
- internal/mcp/registry.go: forwards the merged input (user-supplied
  values + session workspace + root flags) to the dispatcher via
  WithDispatchInput. ExecDispatcher ignores it.
- internal/mcp/dispatch_http.go (new): HTTPHandlerDispatcher
  implementation with a routeTable[cmdPath]→RouteMapper mapping. Seed
  entry: `item create`. Adding more commands is one RouteMapper per
  cmdPath plus a routeTable insert.
- internal/server/context.go (new): exported WithCurrentUser /
  WithAPITokenAuth / WithTokenWorkspaceID + read-only
  CurrentUserFromContext / IsAPITokenFromContext. Lets internal/mcp
  synthesize an authenticated request without reaching the
  package-private context keys.
- internal/server/middleware_csrf.go: extends the existing "Bearer
  token requests skip CSRF" rule to also honor the ctxIsAPIToken
  context flag. Same semantic — non-cookie auth means no CSRF risk —
  but covers the in-process dispatch path where TokenAuth never sets
  the Authorization header. Safe because ctxIsAPIToken can only be
  set by trusted in-process code (TokenAuth on the live Bearer path,
  or server.WithAPITokenAuth from the dispatcher).

## What's tested

- Unit:
  - TestHTTPHandlerDispatcher_RoutesItemCreate — full happy path
    with a recordingHandler asserting method/path/body/user-context.
  - TestHTTPHandlerDispatcher_UnsupportedToolReturnsErrorResult —
    tools not yet in the routeTable produce IsError-flagged results
    rather than panicking.
  - TestHTTPHandlerDispatcher_NoUserReturnsErrorResult — UserResolver
    returning nil produces an IsError, never a nil-deref.
  - TestHTTPHandlerDispatcher_HandlerErrorSurfacesAsToolError — 4xx
    handler responses come back as IsError MCP results matching
    ExecDispatcher's `pad <cmd> failed: <stderr>` format.
  - mapItemCreate validation + parseFieldKVP variants.
- Integration: TestHTTPHandlerDispatcher_Integration drives the full
  *server.Server (real chi router, real SQLite store, full middleware
  chain) with a synthesized OAuth user and asserts the item lands in
  the DB.

## Scope discipline

The DoD called for "dispatch item.create end-to-end" — that's the seed
entry. Wiring the remaining ~70 MCP-exposed commands into routeTable
is naturally a follow-up before TASK-950 ships /mcp to real users
(captured as a separate task post-merge).

Audit-log assertion in the integration test is deferred until TASK-960
(B6b) lands the audit log itself.

Parent: PLAN-943.

* fix(mcp): roll status/priority/category/parent into fields JSON per Codex review (round 1)

Codex caught: mapItemCreate placed status / priority / category /
parent at the top level of the JSON body, but handleCreateItem only
reads them after unmarshalling the Fields string from the request. As
written, MCP-driven `item create` would silently drop those flags —
breaking parity with the CLI for almost every realistic call (parent-
linked tasks, priority-set items, status-overridden ideas, etc.).

Mirrored the CLI's behaviour (cmd/pad/main.go ~L2200): build a fields
map from the named flags, overlay the repeatable --field entries on
top, JSON-encode into ItemCreate.Fields. The handler's existing
schema-validation + parent-resolution path now runs unchanged.

Repeatable --field still wins last-write — locked into a new test so
it doesn't drift.

Also rejects --assign / --role with a clear error rather than silently
dropping them. The CLI resolves user-name → user-ID and role-slug →
role-ID via additional API calls before posting; replicating that
pre-resolution belongs in a follow-up that expands the route table for
production use. Failing loudly is better than partial parity.

Tests:
- TestHTTPHandlerDispatcher_RoutesItemCreate now asserts the
  status/priority/category/parent values land in fields, not the top
  level — guards against the regression directly.
- TestMapItemCreate_ExplicitFieldOverridesNamedFlag locks the
  last-write-wins precedence between --status and --field status=...
- TestMapItemCreate_RejectsUnsupportedAssignRole asserts the
  defensive error path for the deferred flags.

Parent: PLAN-943.

* fix(mcp): persist source=cli for HTTPHandlerDispatcher calls per Codex review (round 2)

Codex caught: actorFromRequest derives source from the Authorization
header — without one, dispatcher-driven calls would persist
source="web" instead of source="cli", regressing dashboard/standup/
audit attribution vs. ExecDispatcher.

Same pattern as the round-1 CSRF fix: extend actorFromRequest to also
honor the ctxIsAPIToken context flag (which TokenAuth sets on the live
Bearer-auth path and HTTPHandlerDispatcher sets via
server.WithAPITokenAuth on synthesized requests). Both signals mean
"non-cookie authenticated, attribute as CLI/agent traffic".

Integration test now asserts source="cli" on the created item, so any
future regression of this attribution surfaces immediately.

Parent: PLAN-943.

* fix(mcp): normalize collection aliases in HTTPHandlerDispatcher per Codex review (round 3)

Codex caught: CLI's `item create task ...` works because
cmd/pad/main.go's normalizeCollectionSlug maps singular/short forms
("task" → "tasks", "doc" → "docs", etc.) to the canonical slug
before posting. HTTPHandlerDispatcher's mapItemCreate skipped that
step, so the same documented call shape would 404 through the HTTP
transport even though it worked through ExecDispatcher.

Extracted the alias map to internal/collections.NormalizeSlug so the
two transports stay in lockstep without duplication. cmd/pad/main.go's
normalizeCollectionSlug now delegates to it; the in-process
dispatcher calls it from mapItemCreate after pulling the collection
out of input.

TestMapItemCreate_NormalizesCollectionAliases locks every documented
alias plus a passthrough case for custom collections.

Parent: PLAN-943.

* fix(server): WithTokenWorkspaceID actually clears on empty input per Codex review (round 4)

Codex caught: the docstring said "Pass an empty string to clear" but
the implementation early-returned `ctx` unchanged in that case,
leaving any stale ctxTokenWorkspaceID set further up the chain
active. Always overwrite so the contract holds: passing "" produces
a context where tokenWorkspaceID(r) returns "", same as a never-set
context.

Parent: PLAN-943.
2026-05-01 11:51:28 -04:00
xarmian 7cda0d7896 feat: rebrand to Perpetual Software + new tagline (IDEA-832) (#273)
Migrates from xarmian/pad to PerpetualSoftware/pad across the entire
repo and updates the product subtitle to "Collaborate with your AI
agents".

Go module rename
- go.mod: github.com/xarmian/pad → github.com/PerpetualSoftware/pad
- All Go imports updated across cmd/pad, internal/{cli,server,store,
  models,collections,items,events,metrics,webhooks} (~130 files)
- Test fixtures with the literal repo slug ("xarmian/pad" in JSON
  shapes, SSH/HTTPS git URL strings, workspace_context fixtures)
  also updated, including the secondary repo entry
  (xarmian/pad-web → PerpetualSoftware/pad-web — pad-web was also
  moved to the org per branch context)

Docs / config
- README badges, install instructions, brew tap, Docker image, source
  build path, sponsor link (sponsor link kept as personal @xarmian)
- Subtitle: "Project management for developers and AI agents." →
  "Collaborate with your AI agents." (README, manifests, web layout
  meta, .goreleaser homebrew description)
- CONTRIBUTING.md, SECURITY.md, skills/INSTALL.md
- .goreleaser.yaml: homebrew_casks owner, GHCR image, release github
  owner, cosign cert-identity regex, comments
- .github/workflows/release.yml: tap/release comments
- deploy/k8s/deployment.yaml: container image
- docs/deployment.md: clone URL
- web/static/{site.webmanifest,manifest.json}: description
- web/src/routes/+layout.svelte: meta description + og:description

Brew tap path is PerpetualSoftware/tap/pad (CamelCase, matches
GitHub user case). GHCR image is ghcr.io/perpetualsoftware/pad
(lowercased per GHCR's URL normalization). CODEOWNERS @xarmian and
FUNDING.yml github: xarmian intentionally retained — those are the
personal maintainer / sponsor account, separate from the org repo.

Verification: go build ./..., go test ./... (all pkgs pass), web
build, and make install all clean (TASK-844, TASK-845).
2026-04-28 12:26:39 -04:00
xarmian 157ca4e88f chore: bump Go toolchain to 1.26 (TASK-763) (#247)
* chore: bump Go toolchain to 1.26 (TASK-763)

Bump Go from 1.25 to 1.26 across all toolchain pins:

- go.mod — go 1.25.0 → go 1.26.0
- Dockerfile — golang:1.25-alpine → golang:1.26-alpine
- .github/workflows/ci.yml — three setup-go steps (Go, Go-Postgres, E2E jobs)
- .github/workflows/release.yml — release pipeline

No `toolchain` directive: the repo is pre-launch with no external
contributors yet, so we set the floor where we want it (hard requirement).

Verified locally before commit:
- golangci-lint v2.11.4 builds and runs under Go 1.26.2 (pinned in CI)
- golang:1.26-alpine and 1.26.2-alpine images present on Docker Hub
- go build ./... clean
- go vet ./... clean
- go test ./... all pass

Parent: PLAN-644 (OSS Repo Hygiene and Launch Polish).

* chore: gofmt -w under Go 1.26 (TASK-763)

Apply Go 1.26's gofmt to the codebase. ~41 files reformatted, all
struct-tag whitespace realignment — no semantic changes. Verified:

- gofmt -l ./cmd ./internal returns empty after
- go build ./... still clean
- go test ./... still passes (run before commit)

Bundling the gofmt diff with the toolchain bump in the same PR because
the formatting drift is a direct consequence of moving from 1.25 to
1.26; splitting them creates a mandatory two-PR ordering for no value.

Parent: PLAN-644.

* docs: bump documented Go floor to 1.26 (TASK-763)

Match go.mod's hard 1.26.0 requirement in the source-build instructions.
Caught by Codex review round 1 on PR #247.

- README.md:158 — "Go 1.25+" → "Go 1.26+"
- CONTRIBUTING.md:9 — "Go 1.25+" → "Go 1.26+"
2026-04-25 11:35:19 -04:00
xarmian e0f3583333 feat(cli): categorized template picker + interactive select (TASK-616) (#148)
* feat(cli): categorized template picker + interactive select (TASK-616)

Turns the CLI template picker from a flat alphabetical dump into a
category-aware flow that reflects the Software / People / Research /
Content / Operations / Personal taxonomy established by PLAN-609.

Library
-------
- collections.GroupTemplatesByCategory returns visible templates
  bucketed into CategoryOrder with a trailing slot for any
  custom-category templates — one canonical grouping that both CLI
  and the upcoming web picker (TASK-617) can consume.
- collections.CategoryLabel turns category slugs into display labels
  ("software" → "Software") with passthrough for unknown values.
- collections.CategoryOrder exposes the canonical display order.

CLI
---
- New cmd/pad/templates_picker.go defines:
  - printGroupedTemplates: writes the grouped listing with icons,
    aligned columns, and a dim "(default)" marker on startup.
  - pickTemplateInteractive: prompts when the user hasn't passed
    --template and is on a TTY. Accepts a number OR a template name;
    enter selects the default (startup); invalid input re-prompts.
  - canPromptForTemplate: TTY detection so scripts never block.
- pad workspace init --list-templates now uses the grouped printer.
- pad workspace init / pad init error messages for unknown templates
  show the grouped list instead of a flat dump.
- pad init now triggers pickTemplateInteractive when no --template
  flag is set AND stdin/stdout are TTYs. Non-TTY invocations fall
  back to the "startup" default unchanged.
- --template flag help no longer hardcodes "startup, scrum, product"
  since the list now grows with non-software templates.

Tests
-----
- Library: TestGroupTemplatesByCategory (canonical order, no hidden,
  every visible template assigned), TestCategoryLabel.
- CLI: TestPickTemplateInteractiveDefault / ByName / ByNumber /
  RetriesOnInvalid, TestPrintGroupedTemplatesIncludesEveryVisibleTemplate
  (smoke: every visible template renders, demo hidden, category
  headers present).

Parent: PLAN-609.

* fix(cli): propagate non-EOF prompt read errors in template picker

Per Codex review on PR #148. pickTemplateInteractive previously
mapped any ReadString error to silently selecting the default
template. A detached PTY returning EIO or a similar read failure
would quietly create a workspace with the startup template even
though the user never made a valid choice. Restrict the silent
fallback to io.EOF (which is benign for pipes, tests, closed
stdin) and bubble up any other error so the command aborts.

* test(cli): cover non-EOF read error path in template picker

Adds TestPickTemplateInteractiveSurfacesNonEOFReadErrors to verify
the behavior change from the previous commit — a non-EOF read
failure propagates up instead of silently selecting the default
template.
2026-04-18 06:31:24 -04:00
xarmian fed365d914 feat(templates): ship interviewing template (TASK-615) (#147)
* feat(templates): ship interviewing template (TASK-615)

Candidate-side companion to the hiring (company-side) template. Same
People category, near-zero collection overlap — a real proof that
one category can hold two templates with barely-related schemas.

Collections
-----------
- Applications (APP) — roles being tracked, stages from researching →
  applied → screen → interviewing → offer → accepted / rejected /
  withdrawn
- Interviews (INT) — individual rounds, child of an Application,
  with round type, format, date, prep_status (including completed)
- Companies (CO) — standalone research notes on companies, referenced
  from Applications via wiki-link so notes are persistent even across
  multiple Applications at the same company
- Contacts (CON) — referrals, recruiters, interviewers — tracked
  independently for followup hygiene
- Docs, Conventions, Playbooks

Trigger vocabularies
--------------------
- InterviewingConventionTriggers: always, on-application-submitted,
  on-interview-scheduled, on-interview-completed, on-stage-change,
  on-offer-received, on-rejection, weekly-review
- InterviewingPlaybookTriggers: on-application-submitted,
  on-interview-scheduled, on-interview-completed, on-stage-change,
  weekly-review, manual
- Scopes: all, research, applications, interviews, followups

Starter pack
------------
- Conventions (3): 48h prep notes (should/on-interview-scheduled),
  end-of-application retros (should/on-stage-change), 24h thank-yous
  (should/on-interview-completed)
- Playbooks (3): Log an Interview, Weekly Job Search Review,
  Interviewing Workspace Onboarding
- Seed items (2): one example Application, one example Company

Tests
-----
- TestInterviewingTemplate — collections present, interviewing
  triggers present and distinct from both software AND hiring triggers
- TestSeedCollectionsFromTemplateInterviewing — end-to-end: seven
  collections created, starter pack populated, prefixes correct

Parent: PLAN-609.

* fix(templates): mark interviewing Companies.closed as terminal

Per Codex review on PR #147. The Companies status field had a
'closed' option but no TerminalOptions declared, so done-state
resolution fell back to the global default set which doesn't
include 'closed' — closed companies were being counted as active
in dashboard and progress views. Adding TerminalOptions fixes
lifecycle metrics without changing user-facing options.

* fix(templates): Log-an-Interview playbook uses only interviewing collections

Per Codex review iteration 2 on PR #147. The playbook step
referenced 'Create a follow-up Task' but the interviewing
template doesn't ship a tasks collection. Rewrote the step to
use the collections that actually exist — log the thank-you as
a comment on the Interview and update the matching Contact's
last_contact. Keeps the starter playbook consistent with the
schema it ships alongside.
2026-04-18 06:10:27 -04:00
xarmian b891ba84a4 feat(templates): ship hiring template (TASK-614) (#146)
* feat(templates): ship hiring template (TASK-614)

First non-software template under PLAN-609. Proves the machinery
built by TASK-610 through TASK-613 end-to-end: category grouping,
per-template trigger vocabularies, template-owned starter packs,
domain-specific seed items.

Collections
-----------
- Requisitions (REQ) — open roles, with status/team/level/location
- Candidates (CAND) — applicants, parent-linked to a Requisition
- Interview Loops (LOOP) — interview rounds, parent-linked to a Candidate
- Feedback (FB) — per-interviewer debriefs, parent-linked to a Loop
- Docs — rubrics, process notes
- Conventions + Playbooks — using hiring trigger vocabulary

Trigger vocabularies
--------------------
- HiringConventionTriggers: always, on-candidate-advance,
  on-loop-scheduled, on-feedback-submitted, on-offer-extended,
  on-close-requisition
- HiringPlaybookTriggers: on-candidate-advance, on-interview-scheduled,
  on-feedback-submitted, on-close-requisition, manual
- HiringConventionScopes / HiringPlaybookScopes: all, sourcing,
  screening, interviewing, offers

Starter pack
------------
- Conventions (3): PII handling (must/always), requisition linking
  (should/always), 24h debriefs (should/on-feedback-submitted)
- Playbooks (2): "Advance a Candidate" (on-candidate-advance),
  "Hiring Workspace Onboarding" (manual)
- Seed items: one example Requisition, one example Candidate (both
  labeled as seeded so users can delete or overwrite)

Tests
-----
- TestHiringTemplate — collections present, trigger vocabulary uses
  hiring values and does not leak software triggers (on-commit etc.)
- TestSeedCollectionsFromTemplateHiring — end-to-end: seeding
  creates all seven collections plus populates the starter pack

Parent: PLAN-609.

* fix(web): display hiring triggers on conventions + playbooks pages

Per Codex review P1 on PR #146. The conventions page hardcoded a
software-only TRIGGERS list in its grouping loop, silently hiding
any convention whose trigger wasn't in that list — so a hiring
workspace's seeded conventions (on-candidate-advance etc.) never
appeared in the primary management UI. Same issue on the playbooks
page's filter dropdown.

- conventions page: grouping now iterates the union of the hardcoded
  TRIGGERS (in original order) plus any triggers discovered in the
  data (sorted alphabetically). Unknown triggers fall back to a
  generic bell icon + the raw trigger string via a triggerMeta helper.
  byTrigger is now SvelteMap<string, Item[]>; the narrow Trigger type
  still gates the create form.
- playbooks page: the filter dropdowns for trigger and scope now
  expose the union of the hardcoded list and any distinct values
  found on loaded playbooks. Create form still uses the narrow list.

The broader "derive options from collection schema so the CREATE
forms also follow the workspace's trigger vocabulary" is tracked
as IDEA-619 for a follow-up PR.

* fix(templates): ship explicit prefixes for hiring collections

Per Codex P2 on PR #146. The seeded candidate's content referenced
--parent REQ-1, but the default DerivePrefix turns "Requisitions"
into "REQUI" (strips trailing S, caps at 5), so the example
wouldn't resolve. Similar problems for Candidates (CANDI) and
Feedback (FEEDB).

- Add optional Prefix string field to DefaultCollection so templates
  can override the derived prefix. Empty (the default) preserves
  today's auto-derivation for every existing template.
- Thread DefaultCollection.Prefix through SeedCollectionsFromTemplate
  to the CollectionCreate call — the CreateCollection API already
  supported a Prefix field.
- Hiring template sets explicit prefixes: Requisitions → REQ,
  Candidates → CAND, Interview Loops → LOOP, Feedback → FB. The
  seeded onboarding text's --parent REQ-1 reference now resolves.
- Test asserts the expected prefixes land on the created collections.

* fix: add 'offers' to hiring playbook scopes + tolerate custom scopes in web UI

Per Codex review iteration 3 on PR #146.

- HiringPlaybookScopes now includes 'offers', matching
  HiringConventionScopes. The hiring pipeline has a distinct offer
  stage and playbook workflows tied to offer management were
  previously uncovered in the schema.
- web/conventions page: the scope filter dropdown now exposes the
  union of SURFACES (original order) plus any scopes discovered on
  loaded conventions, via a new allSurfaces $derived. Same pattern
  as the earlier triggers fix. Non-software scopes (sourcing,
  screening, interviewing, offers) now show up for hiring
  workspaces instead of being hidden by the narrow hardcoded list.

The conventions create form still uses the hardcoded SURFACES — that
broader "derive from collection schema" fix is tracked as IDEA-619.

* fix(templates): hiring Feedback BoardGroupBy uses submitted, not recommendation

Per Codex review iteration 4 on PR #146. Feedback items have two
select fields: recommendation (strong-hire/hire/mixed/no-hire/
strong-no, no terminal values) and submitted (pending/submitted,
terminal=submitted). The done-state pipeline prefers
settings.board_group_by when it's a select field, so grouping on
recommendation made terminal detection fall back to checking
recommendation against default done-statuses — never matching,
leaving submitted feedback perpetually 'active' in active-count
views. Group on submitted so completion actually registers.

* fix(templates): Advance-a-Candidate playbook uses valid Feedback fields

Per Codex review iteration 5 on PR #146. The seeded playbook told
agents to create Feedback items with recommendation=pending, but
recommendation's allowed values are only the concrete verdicts
(strong-hire, hire, mixed, no-hire, strong-no) — 'pending' would be
rejected at field validation. Updated the step to use
submitted=pending (which IS in the allowed options) and call out
that recommendation should stay blank until the interviewer
actually records a verdict.
2026-04-18 05:38:33 -04:00
xarmian 115b33849e feat(templates): software starter pack + idempotent seeding (TASK-612) (#144)
* feat(templates): software starter pack + idempotent seeding (TASK-612)

Ship the software templates (startup, scrum, product) with a curated
starter pack of conventions + playbooks so new workspaces feel
"batteries included" rather than empty shells. The pack is a safe,
small subset drawn from the existing convention/playbook library —
the library itself remains the full catalog for interactive onboarding.

Starter pack contents
---------------------
Conventions (4):
- Conventional commit format (on-commit, should)
- Never push directly to main (on-commit, must)
- Run tests before completing tasks (on-task-complete, must)
- Review your own changes before PR (on-pr-create, should)

Playbooks (2):
- Implementation Workflow (on-implement)
- Code Review Process (on-review)

The pack is materialized by looking up library items by title and
converting them to SeedConvention / SeedPlaybook via json.Marshal of
the expected field shape. When the library's wording changes, the
template's seed content changes automatically.

Store-side changes
------------------
SeedCollectionsFromTemplate is now idempotent with respect to seed
items: items are only created in collections that were freshly
created during the current call (tracked via a freshlyCreated set).
That's the invariant that lets the server's startup auto-upgrade
safely re-run on every boot without duplicating items across every
workspace in the DB.

Empty template name preserves the old behavior (default collections,
no starter pack) — this keeps backward compatibility for callers that
don't pass a template, including the server-startup auto-upgrade path
and all existing server tests. Explicit "startup" / "scrum" / "product"
now gets the starter pack.

Tests
-----
- TestSoftwareStarterPacksPopulated — guards against library-title drift
- TestSoftwareTemplatesShipStarterPacks — each software template ships a pack
- TestSeedCollectionsFromTemplateSeedsStarterPack — end-to-end seeding works
- TestSeedCollectionsFromTemplateIdempotentWithSeedItems — re-seed doesn't duplicate

Parent: PLAN-609.

* fix(cli): default pad init to startup template when --template is omitted

Per Codex review on PR #144. Without this, `pad workspace init` without
`--template` no longer seeded the starter pack, even though startup is
documented as the default. The fix lives in ensureWorkspace (shared by
both init.go and the workspace creation command in main.go) — empty
flag is rewritten to "startup" there. Tests and other direct API
callers that want an empty workspace still pass Template="" through.

* fix(cloud): auto-create workspace passes startup template for starter pack

Per Codex review iteration 2 on PR #144. The auto-create cloud-signup
flow calls SeedCollectionsFromTemplate with an empty template, which
after this PR's semantics meant new cloud workspaces got no starter
conventions/playbooks. Pass "startup" explicitly to match the CLI
init behavior.

* fix(store): propagate collection lookup errors during seeding

Per Codex review iteration 3 on PR #144. seedItem previously treated
any error from GetCollectionBySlug as a silent no-op, which hid real
DB lookup failures — a transient error during workspace creation would
make seeding appear successful while conventions/playbooks were in
fact missing. Now we distinguish the two cases:

  - err != nil  → propagate so callers can detect partial init
  - coll == nil → benign (template references a slug not in its
                   collections list; template-author bug, no-op)

* fix(store): idempotent seeding by item title (partial-init recovery)

Per Codex review iteration 4 on PR #144. The previous design gated
item seeding on collections being freshly-created-in-this-call, which
trapped partially-initialized workspaces: if a DB error fired between
collection creation and item seeding, a retry would see the
collections already existed and skip every remaining seed item.

Switch to title-based idempotency. Before inserting a seed item we
list the target collection's existing items (once per collection, via
a small cache) and skip any whose title already exists. That makes
seeding:

- Idempotent: re-running a template doesn't duplicate items
- Recoverable: retrying fills in missing items after partial init
- Retry-safe: the auto-upgrade path can re-run safely on every boot

New test TestSeedCollectionsFromTemplateRecoversPartialInit exercises
the recovery path explicitly.
2026-04-18 01:22:08 -04:00
xarmian d1c7ede735 feat(templates): parameterize conventions & playbooks schemas (TASK-611) (#143)
The Conventions and Playbooks collections currently hardcode their
trigger and scope select options to a software-centric vocabulary
(on-commit, on-pr-create, backend, frontend, ...). That makes it
impossible for a non-software template to ship domain-specific
triggers like on-candidate-advance or on-interview-scheduled, which
is a hard blocker for PLAN-609.

- Change conventionsCollection() and playbooksCollection() to accept
  trigger + scope option lists from the caller.
- Export SoftwareConventionTriggers, SoftwareConventionScopes,
  SoftwarePlaybookTriggers, SoftwarePlaybookScopes as the canonical
  software-domain defaults. Non-software templates pass their own
  lists.
- Defensively copy the caller's slices inside the helpers so a
  template package author cannot accidentally mutate a shared
  option list.
- Update scrum + product templates to pass the software defaults.
- Refactor Defaults() in defaults.go to use the same parameterized
  helpers — eliminates ~85 lines of duplicated schema definition
  that had drifted out of sync with the templates.go copy.
- Add tests covering: caller option propagation, defensive copying,
  and software-template invariants.

Parent: PLAN-609.
2026-04-18 00:31:55 -04:00
xarmian 73a6e1f3a9 feat(templates): categorize WorkspaceTemplate + hide demo (TASK-610) (#142)
Refactor the WorkspaceTemplate struct to carry the metadata and domain-
specific seed packs needed for the upcoming non-software templates.

- Add Category, Icon, Hidden, Conventions, Playbooks fields to the
  WorkspaceTemplate struct. Existing fields (Name, Description,
  Collections, SeedItems) unchanged.
- Define SeedConvention and SeedPlaybook types so templates can carry
  domain-specific rules and workflows (populated in a follow-up task).
- Introduce category constants (software, people, research, content,
  operations, personal).
- Assign Category=software and Icon to startup (🚀), scrum (🏃),
  product (📦). Mark demo (🎬) as Hidden so it no longer appears in
  the picker while remaining buildable by explicit --template demo.
- Split ListTemplates() into a filtered picker view and a new
  ListAllTemplates() for internal tooling.
- Expose category and icon on the /workspaces/templates API response
  so the web picker can group by category in a follow-up task.
- Add package tests for hidden-filtering and picker metadata
  invariants (the package previously had no tests).

Parent: PLAN-609.
2026-04-18 00:24:22 -04:00
xarmian 973887d5dd fix: comprehensive collection visibility enforcement
Close all identified bypass paths in the collection visibility system:

HIGH:
- Add requireItemVisible check to all 15+ item-by-slug handlers (get,
  update, delete, restore, move, children, progress, activity, versions,
  timeline, comments, links)
- Filter incremental sync (GET /changes) by visible collections with
  proper error handling for deleted item lookups
- Fix search to fail closed on visibility errors instead of removing
  the collection filter; apply per-workspace filtering in multi-workspace
  search path
- Empty CollectionIDs (non-nil but len 0) now returns zero results in
  ListItems and Search instead of skipping the filter
- Filter returned item links by linked item visibility; require target
  item visibility before creating links
- Block moving items into hidden collections
- Add visibility checks to comment-by-ID routes (delete, reply,
  add/remove reaction)

MEDIUM:
- SSE events for replies and reactions now include collection slug so
  visibility filtering can scope them; fail closed on visibility error
- Parent/plan resolution in create/update checks resolved parent is in
  a visible collection
- Progress endpoints compute from visible children only when user has
  restricted access
- Role board reorder checks item visibility before allowing sort changes
- Parent enrichment accepts optional visibility filter to hide parents
  from hidden collections
- Add IsSystem: true to Conventions and Playbooks in defaults.go

LOW:
- Child listing handles visibility lookup errors instead of failing open
- GetDeletedItemsWithCollection returns proper errors instead of
  swallowing them
- SetMemberCollectionAccess wrapped in transaction with workspace
  validation for collection IDs
2026-04-11 02:18:36 +00:00
xarmian d74431fbb3 feat: collection-level visibility + system collections
Add per-member collection visibility controls and mark conventions/
playbooks as system collections (TASK-413 + TASK-415).

Data model:
- workspace_members: new collection_access column ('all' or 'specific')
- New member_collection_access table (workspace_id, user_id, collection_id)
- collections: new is_system column, set for conventions and playbooks

Store methods:
- VisibleCollectionIDs(workspaceID, userID) — returns nil for "all"
  access, or specific IDs (including system collections) for "specific"
- SetMemberCollectionAccess/GetMemberCollectionAccess for CRUD
- All collection queries include is_system in SELECT/scan
- Export/import handles is_system field

Default collection definitions:
- conventionsCollection and playbooksCollection set IsSystem=true
- New workspaces get system flag on seed

D7: default collection_access is "all" — absence of restrictions
means full access. System collections always visible to members.
2026-04-11 01:02:06 +00:00
xarmian bde15d45ca Rename Phases to Plans, clean up deprecated aliases (#71)
* Rename "Phases" to "Plans" and clean up deprecated phase aliases

Renames the default "Phases" collection to "Plans" across the full stack:
- DB migration renames existing collections in-place (name, slug, prefix PLAN, icon 🗺️)
- Removes all deprecated Phase* backward-compat aliases from models and store
- Removes --phase CLI flag (use --parent instead)
- Updates convention triggers: on-phase-start/complete → on-plan-start/complete
- Updates dashboard API: active_phases → active_plans, /phases-progress → /plans-progress
- Updates all frontend components, types, and documentation

Closes IDEA-124

* Fix CSRF cookie not being cleared on logout

The SessionAuth middleware was re-issuing a CSRF cookie before the
logout handler could clear it, resulting in two Set-Cookie headers.
Skip CSRF re-issue for /api/v1/auth/ paths since auth endpoints
manage their own CSRF cookies (login sets, logout clears).

* Fix migration issues found in Codex review

- P1: Move doc_type UPDATE from migration 024 into 025, which recreates
  the table with the new CHECK constraint first (SQLite enforces CHECK
  on UPDATE, so the old constraint would reject 'plan')
- P1: Add PostgreSQL migration 005 for the collection rename (phases →
  plans) — previously only existed on the SQLite path
- P2: Recreate FTS triggers, indexes, and rebuild FTS after the table
  swap in migration 025 (DROP TABLE drops associated objects in SQLite)

* Fix parent filter field name and sync .agents skill copy

Codex review round 2 findings:
- P1: Parent filter compared against `parent_id` (wrong) instead of
  `parent_link_id` — plan filtering in collection view was broken
- P1: .agents/skills/pad/SKILL.md still had old --phase flags and
  "Phases" references — synced from the updated .claude copy
- P2: Accept legacy 'phase' filter key for backward compat with
  existing saved views that serialized the old key name

* Fix PG migration JSONB casting and add slug collision guards

Codex PR review bot findings:
- P1: PostgreSQL REPLACE/LIKE don't work on JSONB columns — cast
  schema::text and fields::text before string ops, then back to ::jsonb
- P1: If a workspace already has a custom 'plans' collection, the
  rename hits UNIQUE(workspace_id, slug) — added NOT EXISTS guard
  to both SQLite and PostgreSQL migrations
2026-04-07 14:55:23 -04:00
xarmian 367116b3a0 Unify relation fields and item links into single dependency system (#66)
* feat: unify relation fields and item links into single dependency system

Phase membership (Task→Phase) was previously stored as a UUID in the
item's fields JSON, separate from the item_links table used for
blocks/related/implements relationships. This unifies both into the
item_links table so all item relationships use one system.

Backend:
- Add 'phase' link type to item_links constants
- Migration 021: migrate existing phase field values to item_links,
  strip phase from fields JSON, remove phase field from tasks schema
- Rewrite GetPhaseProgress, GetAllPhasesProgress, GetTasksForPhase
  to JOIN on item_links instead of json_extract(fields, '$.phase')
- Add SetPhaseLink, ClearPhaseLink, GetPhaseForItem, GetTaskPhaseMap
  store helpers with single-phase constraint enforcement
- Create/update handlers intercept 'phase' in fields and route through
  links system; enrich item responses with phase_id/ref/title
- Dashboard orphan detection uses batch GetTaskPhaseMap lookup
- Add PhaseID filter to ItemListParams for link-based list filtering

Frontend:
- Remove relation field type from FieldEditor (no longer needed)
- Add link CRUD UI to item detail page: "Add relationship" inline form
  with link type picker + item search, delete buttons on existing links
- Phase links appear in Relationships section as "In phase"/"Phase"
- ItemCard reads phase from item.phase_title instead of fields.phase
- FilterBar phase filter uses item.phase_id for client-side filtering
- Add api.links.delete to frontend API client
- Fix duplicate {#each} key on dashboard attention list

Implements IDEA-106.

* fix: remove relationLabels prop from BoardView, ListView, TableView

ItemCard no longer accepts relationLabels (phase info now comes from
item.phase_title), so remove the prop from all parent view components
that were passing it through. Also remove unused .cell-relation CSS.

* fix: address PR review — atomic SetPhaseLink, migration safety, error handling

1. Migration 021: remove deleted_at filters so archived tasks and tasks
   pointing to archived phases also get their phase links migrated.

2. SetPhaseLink: wrap delete+insert in a transaction so a failed insert
   doesn't leave the item with no phase link (previously non-atomic).

3. Create/update handlers: return proper HTTP errors when phase link
   operations fail instead of logging warnings and returning 200 OK.
2026-04-04 19:47:57 -04:00
xarmian e21da3c6a4 fix: schema-driven terminal statuses replace hardcoded lists (BUG-17) (#65)
Add `terminal_options` to collection field schemas so each collection
declares which statuses are terminal/finalized. Replaces 10+ inconsistent
hardcoded status lists across backend, CLI, and frontend.

- Add TerminalOptions to FieldDef and centralized helpers in models/terminal.go
- Populate terminal_options on all default and template collections
- Replace hardcoded isDoneStatus/isTerminalItemStatus with schema-aware lookups
- Fix phase progress to count all terminal statuses (not just "done")
- Add terminal status toggle UI in collection field editor (Settings → Fields)
- Redesign collection field editor for cleaner layout and alignment
- Move Platform settings tab before Danger Zone tab
2026-04-04 18:56:30 -04:00
xarmian 9fedbe4ad6 feat: skill role awareness + role-specific conventions (#59)
* feat: skill role awareness + role-specific conventions (#PHASE-10)

Make the /pad skill role-aware so agents know what role they're acting
as and load conventions scoped to that role. Role context lives in the
conversation — no server state, no files, no new CLI commands.

Skill changes:
- Ask for role on first invocation when roles exist
- Parse "as <role>" inline: /pad as implementer, /pad what's next as reviewer
- Auto-filter work queue by active (user, role) pair
- Role-aware greeting: "Working as 🔨 Implementer. Your queue: ..."
- Load role-specific + global conventions before performing work
- Support mid-session role switching
- Updated CLI reference: --role/--assign flags, pad role commands, --comment best practice

Convention schema:
- Migration 018: add optional `role` field to Conventions collection
- Conventions with a role value apply only to that role
- Conventions without a role apply to all (backward compatible)
- Updated defaults.go with role field

* fix: use json_insert for conventions role field migration

Replace fragile REPLACE() on exact JSON string literal with SQLite's
json_insert(schema, '$.fields[#]', ...) which appends the role field
regardless of field order or custom fields in the schema. Adds a
NOT EXISTS guard via json_each() to skip if role already present.

Also adds role field to shared conventions template for non-default
workspace templates, and regression tests for schema seeding.

Addresses Codex review comment on PR #59.
2026-04-03 21:50:04 -04:00
xarmian be576d9e24 feat: agent roles — role-based (user, role) assignment for items (#58)
* feat: agent roles — role-based (user, role) assignment for items (#PHASE-9)

Introduce agent roles as a first-class concept for human-agent work
assignment. Roles describe capability specializations (Planner,
Implementer, Reviewer, etc.) and items can be assigned to a (user, role)
pair, enabling natural handoff workflows between different AI tools.

Migration:
- New `agent_roles` table (workspace-scoped, slug-unique)
- `assigned_user_id` + `agent_role_id` columns on `items` with FKs
- Removed legacy `assignee` text field from Tasks schema

Backend:
- AgentRole model + full CRUD store/API
- All item queries updated with LEFT JOINs to resolve assignment
- Item list filtering by assigned_user_id and agent_role_id
- Role transitions tracked in activity feed metadata

CLI:
- `pad role list/create/delete` commands
- `--role` and `--assign` flags on item create/update/list
- Assignment displayed in `pad item show` output

Web:
- TypeScript types + API client for agent roles
- Role badge on item cards in list/board views
- Assignment display on item detail page

* fix: enforce workspace-scoped assignments and fail fast on unresolved --assign filter

Addresses code review feedback from PR #58:

P1: Add validateAssignmentScope() to the store layer, called by both
CreateItem and UpdateItem. Verifies that assigned_user_id belongs to
the workspace (via IsWorkspaceMember) and agent_role_id exists in the
workspace (via GetAgentRole) before writing. Prevents cross-workspace
assignment leaks.

P2: The CLI `pad item list --assign <name>` now errors instead of
silently returning unfiltered results when the member lookup fails or
no workspace member matches the provided name.
2026-04-03 21:16:38 -04:00
xarmian 35f3dd1da4 feat(conventions): add structured metadata for TASK-133 (#51)
* feat(conventions): add structured metadata for TASK-133

* fix(web): add workspace update type for CI
2026-04-02 18:39:51 -04:00
xarmian f5649b912e refactor(cli): group first-release commands for TASK-127 (#45) 2026-04-02 15:28:16 -04:00
xarmian 889c6d3a56 Enhance demo template with playbook, extra convention, and multi-agent refs
The demo workspace now showcases conventions AND playbooks (both key
differentiators), references multi-agent support in the architecture doc,
and has a better description. This is what people see when they run
pad init --template demo to try Pad for the first time.
2026-03-28 04:36:47 +00:00
xarmian a6d08fdd7e Add Quick Actions — contextual prompt buttons that copy agent commands to clipboard
New QuickAction type in collection settings lets users define prompt templates
with variables ({ref}, {title}, {status}, etc.) that resolve at click time.
Lightning bolt menu appears on item detail and collection pages. Includes
default actions for Tasks, Ideas, Phases, and Docs collections. Fully
customizable via new Quick Actions tab in EditCollectionModal.
2026-03-27 01:56:31 +00:00
xarmian b3909b13ee Add 'implemented' status option to Ideas collection schema
Adds migration for existing workspaces and updates the default definition
for new workspaces.
2026-03-26 22:30:19 +00:00
xarmian 81579847c6 Initial release
Pad — project management for developers and AI agents.
Single Go binary with embedded SvelteKit web UI, SQLite storage,
CLI, and Claude Code /pad skill integration.

https://getpad.dev
2026-03-26 01:52:36 +00:00