mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 01:53:33 +00:00
c4d429d14c94de941ac761333cdac0a33895a05f
49 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b437cc582d |
feat: item reminders — the fire-at-an-instant primitive, and one overdue rule for all four surfaces (IDEA-2641, closes #1010) (#1244)
* feat(store): item reminders — the fire-at-an-instant primitive (IDEA-2641) Adds the storage, the scheduler tick, and the canonical event for one-shot item reminders (GitHub #1010). Nothing in Pad acted at a target time before this: a due_date makes an item show up as overdue once somebody asks the dashboard, so "revisit TASK-X on the 1st" had to live in an external cron. A TABLE, NOT A SCHEMA-FIELD ANNOTATION. The design sketch proposed marking schema date fields with a `reminds: true` key on models.FieldDef; recon overturned it. Such a key does not survive an ordinary collection edit, two independent ways: the web editor destructures each field into an EditableField and rebuilds a fresh definition key-by-key on save, so unknown keys are dropped (`pattern` and `unique_scope` survive only because two lines were hand-added for them), and models.CollectionSchema has fixed fields with no catch-all, so any Go unmarshal+marshal round-trip strips unknown properties — the hazard retargetRelationFieldsTx mutates raw JSON to avoid. Both failures are silent and both disarm a whole collection's reminders at once. It is the same defect class that moved traits out of the schema column in TASK-2657. The table also gives the lifecycle a home. A reminder is armed, then fired, then acknowledged, and a re-arm returns it to armed — per-reminder state a field definition has nowhere to keep. remind_at is an RFC3339 UTC instant, deliberately not a `date` schema value: those admit both YYYY-MM-DD and full RFC3339 and are compared against the SERVER'S LOCAL calendar day. A fire-at time cannot carry that ambiguity. The remaining timezone question for due_date is filed separately. Firing is one transaction per reminder carrying BOTH the fired_at write and the outbox insert. That pairing is the point: a fired_at committed without its event is a reminder that silently notifies nobody and can never be retried, because the row has left the armed set; an event without fired_at fires every tick forever. The UPDATE's own `fired_at IS NULL` predicate is the arbiter, so two instances ticking at once produce exactly one winner. item.reminder_due is admitted to the closed events/1 set as v1.2, with a new PayloadReminder family and no SSE name. The subject is the REMINDER, not the item: two reminders can be armed on one item, so an item-subject event could not say which fired, and the reminder id is what an acknowledgement addresses. A new payload family rather than reusing the item snapshot for the same reason — a snapshot would validate and still not answer the only question the event exists to answer. No SSE name in v1 because the poll surface is the contract; adding one later is additive, removing one is not. Ack is explicit and nothing else acks. An item reaching a terminal status deliberately does NOT ack: that would make every status write a reminder mutation, and it would silently consume a reminder set to fire after the work was done. * feat(server): reminder surfaces, and one shared overdue rule for all four Second half of IDEA-2641: the HTTP surface, the scheduler tick's wiring, and the fix for the finding that justified the unit — `ready` / `next` did no date handling at all. OVERDUE NOW HAS ONE IMPLEMENTATION. It used to live inline in the dashboard's attention loop, which meant `pad project stale` inherited it (it filters that very list) and the recommendation surface never saw it. So a deadline reached the two surfaces that REPORT on work and never the one an agent PULLS from. overdue.go is now the only place that decides, and all four call it. Two behaviour changes fall out, both deliberate: - An overdue item bypasses the orphan branch's high/critical priority gate. That gate was where a deadline quietly stopped: a low-priority item three weeks late was reported by `stale` and never suggested by `next`. - Overdue sorts above in-progress. The list is capped at three, so a rank below in-progress would not merely order the deadline lower — on any workspace with three things in flight it would keep an overdue item off the surface entirely, which is indistinguishable from not shipping this. The server-local-today comparison is UNCHANGED and known to be wrong for multi-timezone deployments; it is filed as its own item with the cloud case stated. Changing what "overdue" means on every existing instance inside a change about where the rule LIVES is the kind of behaviour change nobody reviews. Fired reminders reach `next` / `ready` two ways, from one filtered list: PendingReminders is the addressable form (it carries the id an ack needs), and a prepended suggestion is the rendered form. They are prepended AFTER the cap rather than entered as ranking candidates — a reminder is not a task competing on priority, and whether it appeared should not depend on how busy the workspace is. Terminal-item reminders are FILTERED from the surface, never acked. Acking on terminal status would couple every status write to reminder state and would consume a reminder armed to fire after the work was done. The row stays exactly as the user left it; the distinction is observable, and asserted. Three guard tests caught this change and each was answered rather than silenced: - The request-body reader guard was right: the handlers now go through decodeJSON, inheriting the NUL refusal and the size cap. - The canonical-events guard was right: item.reminder_due is admitted to the duplicated contract table as SPEC-3 v1.7, with the reminder subject kind and the new payload family. SPEC-3's own text owes the same amendment. - The NUL census asked for a decision on eight new columns. None carries caller text: ids and FKs are server-generated, four are the server clock, and remind_at is now re-parsed and re-formatted in the STORE as well as at the edge — so the stored value is always machine-produced from a parsed time and no caller bytes reach the column. The doc comment that used to say "the caller normalizes" protected nothing. Regenerating the baseline also found that GEN_NUL_BASELINE=1, which the test's own instructions name, was never implemented — the flag did nothing, so the documented path was hand-editing the file. Implemented, so the next reader gets the mechanism the instructions promise. * test(reminders): the lifecycle, the four surfaces, and 22 killed mutants Every test here was designed against a specific mutation and the mutation was RUN. A green suite proves nothing about a suite nobody tried to break, and three of the mutants I first wrote were not experiments at all. Store (10 mutants, all killed): candidate predicate <= flipped to >=; the event emission lifted out of the fire transaction; the fire UPDATE's `fired_at IS NULL` arbiter removed; the RowsAffected check ignored; re-arm clearing fired_at but not acked_at; ack losing `fired_at IS NOT NULL`; the poll surface losing `acked_at IS NULL`; normalizeRemindAt no longer refusing; it dropping .UTC(); GetReminder losing its workspace scope. Surfaces (12, all killed): the priority gate no longer bypassing on overdue; the sort no longer ranking overdue first; attention leaving the shared helper; the reason losing its OVERDUE prefix; the comparison flipped to >; terminal items no longer skipped; terminal reminders no longer filtered; the filter ACKING instead of hiding; reminders appended instead of prepended; the tick running on a far-future clock; ack answering 200 for an unfired reminder; parseRemindAt accepting a bare date. THREE MUTANTS DID NOT COUNT ON THE FIRST PASS and were rewritten. Two failed to compile (`if false` orphaned a variable; deleting a parse orphaned an import) and one had an anchor matching two call sites. A non-compiling mutant emits zero FAIL lines and reads exactly like a surviving one — it invents a hole that is not there — so the harness reports BUILD-FAIL and ANCHOR-BAD as outcomes distinct from SURVIVED. It also restores files from an in-memory copy rather than `git checkout`, which would delete uncommitted work in the tree. ONE MUTANT GENUINELY SURVIVED and the test was at fault, not the mutant: appending rather than prepending reminder suggestions was undetectable because the fixture had a single item, so the reminder sat at index 0 either way. The fixture now fills the three-item cap with in-progress work, where an appended reminder lands fourth and vanishes. Faithful mutant, weak test — checked in that order. The same lesson shapes the four-surface fixture: it is a LOW-priority open orphan, because that is the case the old code handled worst. A high-priority task would have made the ready/next leg pass against the unfixed tree, which is a green that measures nothing. Negative controls throughout: a future deadline is not overdue and does not reach the gate bypass; a tick with nothing due fires nothing; a completed item is neither overdue nor suggested. Without them a helper that reported every date, or a tick that fired everything, would satisfy every positive leg. The lead's pin is asserted in both directions: a fired reminder on a done item is ABSENT from the surface and PRESENT and still unacknowledged in the table. Asserting only the absence would pass against an implementation that consumed the row, which is the behaviour the pin exists to forbid. * feat(mcp): pad_item.remind + ack-reminder, ToolSurfaceVersion 0.28 An agent that can RECEIVE a reminder but not set one has half the primitive. The poll surface is pad_project.next / ready, both long exposed, so reminders already reached agents — what was missing is the other half: deferring a piece of work is exactly the moment an agent knows when it wants to be asked again, and it had no way to say so. Two additive actions, two optional params. Nothing existing moved, so a v0.27 consumer enumerating neither is unaffected — the v0.13 / v0.11 / v0.8 disposition, which likewise wired existing CLI verbs onto the catalog. remind_at REFUSES a bare date rather than reading it as midnight. Worth stating because the `date` schema type accepts YYYY-MM-DD and a caller will reasonably try it here: a bare date names a 24-hour span, and choosing an hour inside it would fire at a time nobody picked. Re-arm and disarm stay CLI-only. Both address a reminder by an id the agent would have to list first, and no listing action exists on this surface — a door with no handle. Adding them later is additive. Five guards had to be taught, and each was answered on its merits rather than excluded: the HTTP parity test (route mappers added, so the actions work on the remote transport rather than being advertised and unrouted), the read-only catalog's cmdhelp fixture and expected cmdPath map, the field- conflict classifier (remind_at / reminder_id are NOT field writers — a reminder is a row in its own table addressed by its own id, so listing them as classified sources would have pointed detectFieldConflicts at something that is not a field source), and the instructions.md / README action tables. That machinery is why the version bump is safe to make now, and it earned its keep on this change: every one of the five failed on the first build after the catalog entry landed. CONVE-23 sweep for prose this falsifies: - SPEC-3 (DOC-2653) amended to v1.7 in the room, recording item.reminder_due with its new subject kind and payload family — the first canonical event with no user mutation behind it, since a scheduler tick produces it. - CLAUDE.md gains the reminder routes, the CLI verbs, and the v0.28 entry. It was also stale at 0.26 with NO v0.27 entry at all: the 0.27 unit swept instructions.md and README.md and missed this file. Both added. - skills/pad/SKILL.md gains the verbs and a routing entry, including the two things an agent will get wrong — the time is an instant, so ask for a time of day rather than picking one, and finishing the item does not acknowledge the reminder. * fix(reminders): codex round 1 — four findings, all real, all with a pin Round 1 found four defects and refuted none of them. Each fix carries a test that fails against the code as it was, and each of those was mutation-checked. **P1 — pending reminders bypassed item-level visibility.** Every other dashboard section reads `allItems`, which the store already scoped to the caller's collections AND their granted item ids. The pending-reminder list is a direct workspace-wide query and inherited none of that, so a guest holding a grant on ONE item could read the refs and titles of every other item in the collection through its reminders — an item-level leak wearing a notification's clothes. Now filtered with the same `isItemVisibleToGuest` call the sibling sections use. The test's two items share a COLLECTION on purpose: a collection-level filter was already applied, so separate collections would have made it pass against the unfixed code. **P1 — soft-deleted items could starve the queue permanently.** Candidate selection ignored `deleted_at`, and `fireOneReminder` rolls back when it finds the item gone — which leaves the reminder ARMED and therefore a candidate again on the next pass. Candidates are ordered oldest-first and bounded by a limit, so enough archived reminders fill every batch and no live reminder ever fires. Silent, too: the tick reports zero fired and looks idle. Excluded in the candidate query rather than skipped downstream, so those rows never occupy a slot; the reminders themselves are kept, so restoring an item restores its reminder with it — asserted, because a fix that reaped them would pass the starvation test alone. **P2 — the pass stopped at the first failing reminder.** The per-reminder transaction exists precisely so one unfireable row cannot hold back the rest, and `return fired, err` made that comment false — with candidates oldest-first, one persistently broken old reminder blocks every newer one forever. Now continues and joins the errors, so a pass that fired seven and failed three reports both halves rather than reading as clean. The loop is split behind an injected seam because a real mid-transaction failure is not reachable from outside: the database refuses the corrupt rows that would cause one (verified — invalid JSON in items.fields is rejected by the schema). **P2 — suggestions dropped the reminder id.** The docs tell an agent to acknowledge what it sees in next/ready, and the payload carried no handle: a stateless poller could read the reminder and had no way to retire it, so it would be shown the same item forever. `DashboardSuggestion` now carries `reminder_id` (omitempty), `pad project next` prints the exact ack command, and the test acks with the id the surface handed out rather than merely checking the field is populated — a wrong-but-present id satisfies equality with itself. Four mutants, four killed; one was rewritten first because its anchor matched two call sites and was therefore not an experiment. * fix(reminders): codex round 2 — four findings, all real **`--rearm` was unusable.** `ExactArgs(1)` forced an item ref that the rearm branch then ignored, so the flag could not be reached without supplying a ref that was silently discarded. Now `MaximumNArgs(1)`, with each mode checked explicitly: a ref is required to arm, and a ref supplied ALONGSIDE `--rearm` is refused rather than ignored — it names an item the reminder may not even belong to, and quietly dropping it is how a user learns nothing about the reminder they just moved. **`unremind --format json` emitted plain text**, breaking the parseable-output contract every sibling command honours. **The MCP `ref` param did not list `remind`.** Agents read that flat description to decide what to send, so an action missing from it is an invalid call waiting to happen. It now also says what `ack-reminder` takes instead, and why: a reminder is addressed by its own id because an item can carry several. **Fractional seconds fired early.** `time.Parse` accepts `09:00:00.900Z` and `Format(RFC3339)` drops the fraction, so it was stored as `09:00:00Z` and fired 900ms BEFORE the moment the caller named — silently, having rewritten their value on the way in. Seconds are genuinely the stored resolution (the column is compared as a string against a whole-second clock, and the tick runs every 30s), so the only question was which way to resolve it, and truncation resolved it the wrong way. `NormalizeInstant` now rounds UP: at most a second of lateness, in exchange for a guarantee that can be stated — a reminder never fires before the instant it was set for. Late is a reminder; early is a wrong answer. Whole seconds round-trip exactly, which is asserted, because an implementation that added a second unconditionally would otherwise pass. Three mutants for this round, three killed (round-up→truncate, round-up→unconditional-add, MaximumNArgs→ExactArgs). Thirty across the unit. Two fixes carry no dedicated test and it is worth being explicit rather than implying coverage: the `--format json` branch on `unremind` is a one-line output change with no server-free way to drive it, and the MCP `ref` description is prose the drift tests do not read — they assert an action is DOCUMENTED, not that a param's sentence lists it. * docs(reminders): the ack id is on the surface an agent polls, not only on the arm response CONVE-23 follow-through on the round-1 fix. Both agent-facing docs told a caller to acknowledge a reminder with the id "returned when you armed it" — true, and useless to the caller that matters: a poller reading next/ready never armed anything. The suggestion now carries reminder_id and `pad project next` prints the exact ack command, so the docs say that instead. The prose was written before the fix existed, which is exactly the case CONVE-23 is about: a change that makes an instruction stale without touching the file the instruction lives in. * test(reminders): bind the tick LOOP to the work, not just the pass (CONVE-19) Every other test in this file calls runReminderTick directly. That vouches for the component and says nothing about whether anything ever calls it — a tick that is never started is indistinguishable, from those tests, from one that is. It is the convention's exact case, and the failure I recorded on my own identity doc three times in one unit: I test the component and not the binding. Driven through the injectable tick channel so the assertion pins a SPECIFIC pass instead of racing a 30-second ticker, and polled to a bounded deadline so a loop that never runs FAILS rather than hanging the suite. Mutant: drop `s.runReminderTick()` from the select and this goes red while every direct-call test stays green. Killed. The idempotence leg exists because a second Start spawning a second loop would leave one running after Stop, making the BUG-842 drain invariant false for this sweeper specifically — the one property a copied lifecycle is most likely to get right by accident and least likely to be checked. The cmd/pad call site (cmd_server.go, alongside StartTokenReaper) stays verified by inspection: a source-scanning guard for it would be an instrument asserting facts about source, which is code with an adversary and not worth it for one line that sits in the middle of five identical neighbours. * fix(reminders): codex round 3 — a deferred reminder fired anyway, and the poll surface was unbounded **A re-arm mid-pass did not stop the fire.** The candidate scan selects an id; before the UPDATE runs, a `--rearm` can move that reminder into the future. Re-arm clears `fired_at`, so a predicate checking only `fired_at IS NULL` still matched — the pass fired a reminder the user had just deferred and emitted its event. The re-arm cannot undo that: it can clear the mark, but the event is already on the outbox and at-least-once means a consumer has seen it. The fire UPDATE now revalidates `remind_at <= nowTS` against the SAME nowTS the candidate scan used. Same-value deliberately: the arbiter and the scan must agree about when this pass is, or a reminder could pass one and fail the other for no reason but clock drift inside a single pass. **The poll surface was unbounded.** Every fired-and-unacknowledged reminder was loaded and turned into a suggestion prepended to a list that is otherwise capped at three, so a workspace with five hundred unacknowledged reminders returned five hundred suggestions — in the dashboard response, the hottest read in the product, growing until somebody acknowledged them. Two bounds, because they are two different guarantees: the query takes a window (default 50, oldest-fired first, so it holds what has waited longest), and the prepended suggestions are capped at 5 so `suggested_next` stays a recommendation rather than a second inbox. The full set stays addressable in `pending_reminders`. Truncation is REPORTED as a boolean, not a count. A count would have to be post-visibility-filter to be true for the caller reading it, and the store cannot compute that — the filter runs per item, above. "There are more than you can see here" is the strongest claim the data supports, so it is the one made. Four mutants; two killed outright, two survived and were run down under CONVE-28: - **Uncapped suggestions survived because the fixture had ONE reminder** — capped and uncapped are the same list at n=1. That is the SECOND time a single-item fixture hid a count-or-order property in this file. Fixture now arms eight; it also asserts all eight remain in `pending_reminders`, so the cap is pinned to the recommendation and not to the data. - **Removing the SQL LIMIT survived, correctly, and the test comment now says so.** The Go slice cap bounds the PAYLOAD; the SQL LIMIT bounds the DATABASE'S work. Only the first is observable at this level — with the LIMIT gone the response is still bounded, while the query silently goes back to materialising every pending row before discarding most of them. That is a memory and I/O property with no assertion available here, so it is stated as a coverage boundary rather than papered over with a green that would not have measured it. * docs(reminders): the fire predicate arbitrates against two actors, not one CONVE-23 inside the file the round-3 fix touched. The comment described the UPDATE as an arbiter for concurrent TICKS, which is what it was written for and is why I did not re-read it when asked whether a user edit could race the pass. It now says what it actually defends against, and names the general shape: an arbiter is only an arbiter with respect to the writers it can see. * fix(reminders): codex round 4 — the round-3 bound recreated the round-1 starvation Round 3 bounded the poll surface. Round 4 caught what that bound did: the query took the first N rows and the dashboard then discarded the ones it could not show — hidden items, unauthorised items, completed items — so N such rows hide a visible reminder behind them indefinitely, with no continuation to reach it. That is the SAME defect I had removed from the fire path one round earlier, reintroduced in the read path within the hour. The general form is worth stating because I clearly did not hold it: **a bounded window is only safe when the discarding happens BEFORE the bound.** Filtering above a limit is a starvation every time, and it does not matter what the filter is for. Two halves, because the two filters are not the same kind of thing: **Visibility is now scoped IN SQL**, using the same collection-id / item-id sets every other dashboard section gets through `allItems` — the same three-way shape as ItemListParams, where holding both collection grants and item grants is an OR. Invisible rows no longer occupy the window at all, which is strictly better than filtering them out afterwards and is what the sibling sections have always done. **Terminality is paged**, because SQL cannot evaluate it — a collection's schema defines which statuses are terminal. The collector refills from the next page when a page comes back short, bounded by a max scan so a workspace full of completed items cannot turn a dashboard read into a table scan. The bound is 10x the window: the common shape fills on the first page, and the pathological shape terminates in a fixed number of indexed reads. Stopping at the scan bound reports truncation, which is honest — there may be more, and we did not look. The empty-scope case is a THIRD state that reads like the second: nil CollectionIDs means unrestricted, a non-nil EMPTY slice means this caller sees no collections. Without an explicit guard they collapse, because the switch matches none of its cases at length zero and adds no clause at all — so "nothing visible" would return the whole workspace. Three mutants, one survived: the empty-scope guard, because no dashboard-level test produces that state (callers that would are refused earlier by workspace access). Faithful mutant, missing test — it now has a direct one, with a sanity leg so a build returning nothing cannot pass it by accident. A guard for a state nothing exercises is exactly the one that rots. * fix(reminders): codex round 5 — the MCP action I shipped did not work over stdio **P1: local stdio MCP `remind` was unusable.** cmdhelp derives positionals by regex from a command's `Use` string, and `<instant>` inside `remind <ref> --remind-at <instant>` matched — it became a second REQUIRED positional, so dispatch failed with `missing required argument "instant"`. The action was advertised on a transport where it could not run. **The MCP catalog's own tests did not catch it, and the reason is the finding.** That suite builds its cmdhelp document BY HAND: I wrote `Args: mkArgs("ref")` in it, so the fixture agreed with what I meant rather than with what the CLI says. Five parity and drift tests passed against a document I authored to match my own intention — the "a test that agrees with whatever the table says is not a test of the table" shape, which the canonical-events test warns about in its own comment two packages away. The new test reads the REAL command tree via cmdhelp.Build, which is the only thing in this repo that can disagree with me about what the CLI declares. **P2: `pad project ready` withheld the ack handle** that `next` prints. Showing a fired reminder on the surface an agent polls while withholding the id it needs to retire it means the same entry comes back on every poll, forever. **P2: suggestions asserted a collection they did not have.** The orphan branch admits ANY collection — its own comment claimed it gated on tasks "mirroring the active-plan branch", and that comment was simply false — while the output hardcoded `Collection: "tasks"` and the reason said "Open task". Pre-existing for high-priority items since BUG-1082; my overdue bypass widened it to any overdue item, which is how it surfaced. Fixed by carrying the item's REAL collection rather than by narrowing the branch: narrowing would silently drop the non-task items this has surfaced for a year, and the defect is the mislabelling, not the inclusion. The false comment is replaced with what the code actually does. The first version of that test used an overdue IDEA and SKIPPED — ideas use `new`, and the branch requires `open` or an active status, so it never became a candidate. A test that cannot fire is a failed reconstruction, not a pass; the fixture is now a bug-like collection whose vocabulary contains `open`, which is the population the defect can actually reach. Three mutants, three killed. Forty-one across the unit. * fix(reminders): codex round 6 — reminders fired from soft-deleted workspaces **P1, and the only defect in this unit whose consequence leaves the process.** Workspace soft-delete deliberately keeps items for the 30-day restore window, so the candidate query's filter on the ITEM's deleted_at found nothing wrong — and the tick kept firing, emitting outbound webhook events for a workspace whose owner had deleted it, possibly while deleting their account. Both queries now join workspaces and require `w.deleted_at IS NULL`. Nothing is destroyed: a restored workspace resumes firing, which the test asserts, because "stops firing" and "is destroyed" are very different answers to someone who restores a workspace and only one of them is right. That test first failed for the WRONG REASON and the fixture was at fault: it counted every outbox row in the workspace, and item creation writes its own, so the assertion was satisfiable by the fixture itself and discriminated nothing. Scoped to the reminder event type. **`Use: "remind <ref>"` declared a requirement the command contradicts.** cmdhelp derives the machine-readable arg spec from that string, and `--rearm` takes no ref — so the published contract said "required" for something optional. The requirement is CONDITIONAL, which cmdhelp cannot express, so the honest declaration is `[ref]` plus the explicit check that names both call shapes. The round-5 test grew a `required` column, which is what makes this observable at all: asserting only the arg NAMES would have passed. **The pad_item tool description omitted both new actions.** The params were declared and the actions dispatched, but the prose an agent reads to decide what a tool can do did not mention them — discoverable only by someone who already knew to look. It now describes both, including the two things an agent gets wrong: remind_at is an instant, and nothing but an explicit ack retires a fired reminder. Three mutants, three killed. Forty-four across the unit. * fix(reminders): codex round 7 — one predicate for the scan and the arbiter Third instance of one class, so this fixes the SHAPE rather than the instance. The class: the candidate scan filters on something the fire transaction does not revalidate, so a change committed between them fires a reminder that no longer qualifies. Round 3 was a re-armed instant. Round 1's soft-deleted item was the same thing caught from the other side. Round 7 is a workspace deleted between the scan and the fire — the round-6 fix added the condition to the SCAN only, and the arbiter went on not knowing about it. Fixing those one at a time is what let the third happen. `reminderFireable` is now a single string that both sites reference: the scan asks it and the fire UPDATE re-asks it, so they cannot disagree, and a fourth condition is one edit in one place rather than two edits someone has to remember are paired. Written as a correlated EXISTS on item_reminders.item_id rather than a JOIN precisely so the identical text is valid in both a SELECT and an UPDATE, and the scan drops its table alias so the two uses are the same characters. What deliberately stays outside it: `fired_at IS NULL` and `remind_at <= ?` live on the reminder row itself, are already spelled identically at both sites, and folding them in would need a parameter order the shared form cannot express. Said in the comment so the omission reads as a decision. Both directions are now tested at the arbiter — a workspace deleted mid-pass and an item deleted mid-pass — because the item case previously relied on the item load coming back nil, and someone simplifying the EXISTS down to the workspace check alone would otherwise still see green. Three mutants, three killed: the arbiter dropping the shared predicate, and the predicate dropping each of its two halves. Forty-seven across the unit. * fix(reminders): codex round 8 — workspace export silently dropped every reminder WorkspaceExport is a hand-maintained field list, so a new table joins it only if someone remembers. Reminders did not: a backup/restore, or a SQLite→Postgres migration via `pad db migrate-to-pg`, dropped every pending reminder with nothing in the destination to show anything had gone. The line that list has always drawn is item-scoped workspace CONTENT (comments, links, versions — exported) versus per-user state (stars, watches — not). A reminder has no user column and hangs off an item, which puts it on the exported side. Stating the rule rather than just adding the field, because the next person adding a table needs to know which side they are on. LIFECYCLE MARKS ARE CARRIED, not reset. A fired-and-unacknowledged reminder is still owed to whoever armed it, so it arrives pending; an armed one whose instant has passed fires once on the destination's first tick, which is what would have happened had the workspace never moved. Re-arming everything on import would invent a schedule the user did not set. NULL rather than empty string for the unset marks — the lifecycle is defined by NULL-ness, and "" would make a never-fired reminder read as fired at "". TestMigratedTablesCoversTheExport caught the second half, which I would have missed: `pad db migrate-to-pg`'s NUL preflight decides what to REFUSE on from MigratedTables, so a table the migration copies and the preflight does not know about is a gap in exactly the guard that exists to prevent one. Added there too, with the reason it can never actually fire — every column is machine-produced, so it is listed for coverage rather than expectation — and the "six tables" prose it falsified is now seven. Two mutants, two killed: export dropping the block, and import discarding the marks. Forty-nine across the unit. * test(reminders): state the fire-path invariant and pin it from the invariant The lead's read on why rounds 4 and 7 were the same class: the fire path had no stated invariant, so each fix defended an instance. This states it, and derives the pin from the paragraph rather than from the bug history. THE INVARIANT: the candidate scan is a hint and may be assumed to prove nothing. Every condition that made a row a candidate is re-asserted inside the transaction that marks it fired, in the same statement that does the marking, so checking and writing are one atomic act. Worded as "the scan proves nothing" rather than as a list on purpose — a list invites the next person to add a condition to the scan and stop, which is exactly what happened four times here. TestFirePathInvariant is the pin: one table, one row per scan-side condition, each invalidating that condition in the window between the scan and the fire and asserting the same three things — nothing fires, no event leaves, the reminder is not consumed. The earlier per-defect tests are folded in as rows; they said the same thing one instance at a time, which is how four of these shipped. Adding a fifth condition to the scan without a row here should feel like an omission. It carries a positive control, because four cases that all assert nothing happens would pass against a build that never fires at all. The matrix immediately falsified a claim in the paragraph I had just written. I wrote that the item load inside the transaction is "for the payload, not for the check"; removing the item half of reminderFireable alone changes no observable behaviour, because the load then returns nil and the deferred rollback undoes the write. Item liveness is defended TWICE and a single-mutant experiment cannot say which guard is carrying it — removing both is what kills the test. Both are kept, the predicate is named as primary (the row never matches, so no write happens at all), and the asymmetry is stated: workspace liveness has no second line, which is why dropping ITS half does fail the pin. Six mutants: five singles plus the pair. Five killed alone; the item single survives by design and is documented as such rather than left as an unexplained green. Fifty-five across the unit. * fix(reminders): codex round 9 — one legacy row could hide every reminder **P1: items.item_number is NULLABLE and I scanned it into an int.** Migration 006 added the column to existing rows, so a pre-numbering item still carries NULL — and scanning NULL into an int fails the Scan, which fails the QUERY, which degrades the whole pending-reminder section. One old row, and the feature is dark for everyone in that workspace. ListWatchesForUser, which this query was modelled on, uses sql.NullInt64 for exactly this column. I copied its shape and dropped the part that handles the column's actual nullability — the same way of being wrong as the round-5 cmdhelp fixture: borrowing a form without borrowing what it knows. The legacy row now carries no ref rather than a fabricated "PREFIX-0", which would name a different item. **P1: export shipped reminders that import could only discard.** The items section filters on deleted_at IS NULL, so a soft-deleted item is not in the bundle and its reminder can never be reunited with it. My comment claimed the item_links rationale — round-trip the raw graph so a restore reunites them — which is true for links and false here, because links keep soft-deleted endpoints in the bundle and items do not. A link is a row ABOUT two items; a reminder whose item is absent is a dangling schedule. **P2: import wrote remind_at raw.** Import is a writer, and a bundle is not necessarily one this server produced — hand-edited, or from another instance. A local offset or a bare date would land in the one column every comparison downstream treats as a UTC instant, firing early, late, or never. It now normalizes like every other door. An unparseable value is SKIPPED with a warning rather than failing the restore, matching the lenient import-side precedent already in this file, and the raw value's LENGTH is logged rather than its content. Three mutants, three killed; two needed rewriting because the single-line form did not compile — reverting the nullable scan also requires reverting the render, and dropping the normalization orphans a variable. PROCESS FAULT, recorded because it makes this round's findings weaker than they look: I edited the tree while this review was reading it — committed the invariant work and ran five mutation experiments, which write and restore source, over the same files. A review binds to the tree it read and I moved it underneath. Every finding above was re-verified against the current tree before being acted on, and the next round runs with no concurrent edits. * fix(reminders): codex round 10 — one orphaned item aborted a whole restore An ORPHANED item — one whose collection is missing from the bundle — still gets an itemMap entry. It has to: the entry is written before the skip because parent resolution inside the same loop reads the map for items it has not reached yet. So `itemMap[x] != ""` is satisfied by an id that names no row, and inserting a foreign key to it fails (SQLite enforces FKs here via the DSN's `_pragma=foreign_keys(on)`; Postgres always does). The pre-existing mapping is the sharp edge. The aggravating half was mine: this loop treated a failed reminder insert as FATAL, where item_links and item_versions both skip, so one orphaned item carrying a reminder rolled back an entire 900-item workspace restore. A reminder is the least critical thing in a bundle and it had the strictest failure handling in the file. Both halves fixed: the loop gates on items that actually landed, and a failed insert warns and skips like its siblings. TWO GUARDS THAT ONLY DIE TOGETHER, and this is measured rather than assumed. Reverting either alone leaves the test green — with the map gate restored the skip survives the FK failure, and with the fatal return restored the gate means the insert never fails. Removing both is what fails it. They are kept as a pair because they defend the same failure at different depths (prevent the bad write / survive a bad write arriving some other way), and the pair is recorded in the code so a future reader does not delete one as dead after watching its mutant survive. Second time this shape appeared today; the first was item liveness on the fire path. The bundle in the test is hand-built, because ExportWorkspace cannot produce an orphan — which is the reason it needed a test. That shape only arrives from a hand-edited or foreign bundle, and surviving those is what import is for. Three mutants: two singles that survive by design, plus the pair that kills. Sixty-one across the unit. * fix(reminders): codex round 11 — four contract slips, one of them another unit's **suggested_next returned up to eight entries against a cap of three.** Round 3 prepended reminders PAST the list's own cap, reasoning they should not compete for slots. Every consumer — the web dashboard, `pad project next`, `pad project ready` — is written for three. Worse, it silently falsified a decision recorded elsewhere: BootstrapDashboard deliberately has no suggested_next_overflow_count BECAUSE this list is capped at three upstream, and its comment names raising that cap as the moment to add one. My change made another unit's reasoning wrong in a file I never opened. The combined list is now trimmed back to three, reminders still leading — a reminder can push a task suggestion out, which is the right way round, and the full set stays addressable in pending_reminders. My first version of that trim used `limit`, which is REASSIGNED above to len(candidates) — so on a workspace whose only entries are reminders it would have truncated to zero, killing precisely the case the surface exists for. Caught by reading the surrounding lines before running anything; it has its own test now. **pending_reminders was uncapped in the bootstrap projection.** BootstrapDashboard embeds *DashboardResponse, so every new field joins the boot payload automatically — here, a window of up to 50, which is the budget PLAN-1410 spent a unit trimming. Capped at 5 with an overflow count, under its own constant rather than borrowing bootstrapAttentionCap: they answer different questions and a future change to one must not silently move the other. **Truncation was reported from the wrong question.** The collector used the store's `more` flag, which answers "is there another PAGE", not "did I read all of THIS one" — so a window filling part way through the final page reported that the caller had seen everything while unread rows sat behind the fill point. The paging bounds are now injectable so the case is testable at all: building it with a window of 50 needs ~75 rows in a specific pattern, with a window of 3 it is four. **Import accepted acked-without-fired**, which is not one of the lifecycle's three states. Such a row fires, is excluded from the pending surface because it is already acked, and can never be acknowledged because AckReminder requires acked_at IS NULL — an event emitted into permanent invisibility. The acknowledgement is dropped and the schedule kept, since an ack of something that never fired means nothing. Five mutants, five killed (one rewritten — removing the flag orphans a variable). Sixty-six across the unit. * fix(reminders): codex round 12 — a read is not a hold; scope the arm; ack from the ack Four P2s from round 12 (two independent runs, both landing on the same line of the fire path), each closed at the layer where it lives: - fireOneReminder pins the item and workspace rows FOR NO KEY UPDATE on Postgres before the arbiter UPDATE. reminderFireable re-asserted liveness at the predicate's instant and nothing held it to the commit instant; under READ COMMITTED an archival could commit in between and the event left the process about a deleted resource. Same idiom and same lock strength as CreateAttachmentForLiveItem; SQLite is excluded by its BEGIN IMMEDIATE, not skipped for convenience. Two PG-only pins verify "blocked" in pg_stat_activity, not by elapsed time; the pin-removed mutant fails both. - CreateReminder asserts "live item of THIS workspace" in the INSERT's own SELECT and returns ErrReminderItemGone otherwise. The table had an FK and no same-workspace constraint; a mismatched pair fed another workspace's title to this one's dashboard and webhooks. Handler maps it to 404. - AckReminder matches every fired row (COALESCE keeps the first ack, updated_at moves only when acked_at does), so a no-match means exactly "not fired at the instant of the ack". The handler no longer decides 409-vs-200 from the row it read before the UPDATE. - The invariant paragraph gains its missing sentence: "at that instant" means the commit instant, and the pin is what makes the predicate's instant and the commit instant the same one. Round-12 caveat carried: both runs were static reads (sandbox blocked Go's build cache), so "four" is a floor, not a measurement. Refs IDEA-2641 * fix(reminders): codex round 13 — a reminder's workspace must agree with its item's, at every read Every reader scoped by r.workspace_id and then joined the item without asserting the two agree. No door writes a disagreeing row today (CreateReminder derives the pair from the item; import maps within the workspace), and the table has nothing that forbids one — so a hand-edited bundle, a future move door, or a direct write would carry one workspace's item into another's dashboard, export, and webhooks. The identity goes into reminderFireable (scan + arbiter), the Postgres row pin, ListPendingReminders and the export query. One test writes the row raw — the only way one can exist — and asserts it is inert at each site; the predicate-removed mutant scans and fires it. Refs IDEA-2641 * fix(reminders): codex round 14 — the by-id and by-item reads assert the same identity as every other read GetReminder scoped by the row's own workspace_id and ListRemindersForItem by item_id alone, so a row whose two columns disagree — the class rounds 12 and 13 closed at the scan, the arbiter, the pin, the pending surface and the export — was still readable through the two reads that reach a single row. reminderOwned is that identity on its own, without the liveness half those two reads must not have (a fired reminder on an archived item is history worth showing). The write paths reach a row only through GetReminder, so scoping it scopes them; a row no door can write needs no door to delete it. ListRemindersForItem now takes the workspace its caller already resolved the item in. The raw-row test asserts both reads refuse the row from both sides; the reminderOwned-removed mutant surfaces it through GetReminder. Refs IDEA-2641 * fix(reminders): codex round 16 — an archived item's reminders are readable, and its verbs say "archived" The doors resolved the item live. Listing an archived item's reminders answered 409 from a GET, and ack/re-arm/delete answered a bare 404 for a reminder that exists on an item that exists — while the store, since round 14, deliberately keeps that history readable. The API already has a posture for archived items: GET reads them, mutations answer 409 "archived … restore it before editing" (writeItemResolveError). The list now follows handleGetItem; the lifecycle verbs load the item include-deleted, run the visibility check first, and then answer the same 409 every other item mutation does. One test walks archive → list 200 / ack 409 / arm 409 → restore → ack 200 on the same rows. Refs IDEA-2641 * fix(reminders): codex round 17 — one suggestion per item, the archived 409 by slug, and the door courtesy named Three findings on the server pass. (1) An item that was both a fired reminder and an ordinary candidate appeared in suggested_next twice; the ordinary entry is dropped, the reminder entry (which carries the ack id) stays, and two reminders on one item remain two entries. (2) Round 16's 409 for an archived item's reminder was written by re-resolving item.Ref, which is derived and empty for a legacy item with no item_number — so the class most likely to be legacy fell through to a bare 404. The slug is handed over instead. (3) The archived check in resolveReminderForWrite is check-then-write, and an archive landing in between lets the verb through: accepted and documented — it is the posture of every item mutation here (UpdateItem's UPDATE has no liveness clause), the outcome is benign, and putting liveness in AckReminder's WHERE would re-create the no-match ambiguity round 12 removed. Refs IDEA-2641 |
||
|
|
e747a1610c |
feat(session): registry keyed on the harness session, carrying the agent name; pad session list / prune (TASK-2767) (#1200)
## Summary TASK-2767 (IDEA-2750 part 2, with part 3 riding along — the keying fix and the reaping are one mechanism). The local session registry (`~/.pad/sessions`) was keyed on the pid of the `pad session register` subprocess, which is dead before anyone reads the file. One session left a new file per call and its own pid appeared in none of them; the only live identifier was the harness pid a reader could parse out of the socket path's basename. In practice nothing wrote it (zero callers in `plugin/`, `skills/`, or hooks) and nothing read it. Now: - **One record per session, keyed on the harness session pid** — `$PAD_SESSION_PID` (harness-agnostic override), else `$CLAUDE_PID` (verified present in both the tool shell and a live plugin monitor's `/proc/<pid>/environ`), else the calling process. A set-but-invalid value is an error, not a silent fall-through. - **The record carries the agent name** the session's writes are attributed to (`ResolveAgentName`: `.pad.toml agent_name` → `$PAD_AGENT` → detected runtime; `--agent` overrides, `--agent ""` is anonymous), the harness session id, and the messaging socket's identity (inode/device/mtime — the same binding the arm-state file uses). - **One owner-identity type, one verdict.** `internal/cli/session_owner.go`: `SessionOwner` + tri-state `OwnerLiveness` (`alive` / `dead` / `unknown`). `armStateOwnerAlive` is now `OwnerLiveness(...) == alive` with its file contract preserved (socket identity else mtime; headless pid + start token; fail closed). The registry pruner takes the opposite posture on `unknown`: on Windows `pidAlive` reports dead for every pid, and a reaper built on that would delete every live session's record. - **Verbs:** `pad session register [--agent]` (writes/refreshes; prunes dead records), `pad session list [--agent] [--cwd] [--all] [--format json]` (liveness per row, newest first; dead hidden unless `--all`), `pad session prune [--older-than DUR]` (dead always; unknown only under an explicit bound; alive never). Nothing on MCP — host-local filesystem state. - **Who registers:** `plugin/scripts/pad-monitor.sh` runs `pad session register` on start, BEFORE the consent gate — presence is a fact, consent is a grant, and the record is local/0600/never on the wire. - **Legacy v1 files** list as `legacy` rows: owner = socket-basename pid (else registrar pid), liveness by pid only (v1 recorded no socket identity, and the socket-without-identity rule would have judged every legacy record dead while its session ran). A legacy row can say a session exists, never who it is. Lead rulings on the four open decisions, all as built: `agent`/`--agent` vocabulary; no server-presence merge in `list`; register from the monitor script before the gate; wire follow-on (agent name on the stream) filed separately as IDEA-2750 part 2b. One ordering change from the plan's section A: pid precedence is `PAD_SESSION_PID` > `CLAUDE_PID` > self (explicit override beats detection, mirroring `PAD_AGENT` over runtime detection); the plan listed `CLAUDE_PID` first. ## Behaviour changes for existing users of `~/.pad/sessions` / `pad session register` - Registry files are keyed on the **harness session pid** (`PAD_SESSION_PID` → `CLAUDE_PID` → self), not the `pad` command's pid; repeated registrations overwrite one record instead of accumulating. - `pad session register` records the agent name, harness session id and socket identity; stores the **real path** of the cwd; prints a different text line and a different JSON shape (the full `SessionRecord`); and **rejects** an invalid `PAD_SESSION_PID` / `CLAUDE_PID` instead of silently keying on itself. - Existing v1 files are read as `legacy` rows (owner = socket-basename pid, no agent name) and dead ones are pruned by the next register. - The plugin monitor now registers (and prunes) on every start, before the consent gate. - `armStateOwnerAlive` now delegates to the shared `OwnerLiveness`; the consent gate's observable behaviour is unchanged on every platform and key type (codex round 4 traced every caller; matrix M29 pins the socket-keyed mapping). https://claude.ai/code/session_016zc6oxBvpax6Z3iQMsAJno |
||
|
|
99ffad1bca |
feat(server): timeline comment rows carry the agent name (TASK-2760) (#1196)
* feat(server): carry the agent name onto comment rows in the timeline (TASK-2760) An agent's comment rendered under the human's name: the name is stamped only on the linked 'commented' activity, which the timeline suppresses because the comment card stands in for it. The comment list queries now LEFT JOIN that activity and surface the name as Comment.AgentName (top-level and nested replies, on the timeline and the comments endpoint alike, through one scan helper), mirrored onto comment-kind TimelineEntry.agent_name to match the actor_name idiom. The web comment card renders it verbatim in an isolated <bdi>, separate from the human author. Store join rather than a handler-side match: the two lists are paginated independently, so a handler join misses at page edges and reads as intermittently-correct attribution. Metadata is parsed in Go, not SQL, to keep the query free of a SQLite/Postgres dialect fork. * test(store): make the activity-window premise strict, not a same-second coin flip (TASK-2760) * fix(server): replies log + link their commented activity so the agent name reaches them (TASK-2760, codex r1) The dedicated reply route wrote no 'commented' activity, and the activity is the only row that carries the writing agent's name — so a reply through the web UI rendered under a generic chip no matter what the client sent. Also rewrites the README + SKILL.md claim that comments never show the name, moves the reply test onto the real route, and asserts order/limit under the join. * fix(store): exclude comment-linked activities in the timeline's activity query (TASK-2760, codex r2) buildTimeline suppressed a comment's linked activity only when that comment was on the same page; the two sources are paginated separately, so an activity could slip through as a standalone 'commented' card. The query now excludes linked rows via NOT EXISTS on idx_comments_activity (both dialects), exact regardless of either window, and the page-local guard is removed rather than kept as a dead one that reads as load-bearing. * fix(store): item-scope the comment/activity link and freeze comment-linked activities against debounce merges (TASK-2760, codex r3) The join keyed on activity id alone while nothing in the schema ties a comment's activity to its item — scope both the LEFT JOIN and the NOT EXISTS to the item. And CreateActivityDebounced could merge a later update into the 'updated' row a comment links to, overlaying its agent stamp and bumping created_at, so two agents under one set of credentials would silently re-attribute an earlier comment; comment-linked rows are no longer merge targets. Prose corrected: the linked row is a 'commented' row OR the 'updated' row of an update that carried the comment. * fix(server,web): keep the read-skew guard beside the SQL exclusion; nowrap on every 24ch agent label (TASK-2760, codex r4) The page-local guard covers a distinct failure from the query exclusion — a comment fetched then hard-deleted before the activity query runs — so it returns with that reason written down. Sweep: of the seven 24ch agent-label rules, three lacked white-space: nowrap (both timeline cards and EpisodeFeed), so a name with spaces wrapped instead of ellipsizing; the other four already had it. Prose nits corrected; the pre-link debounce race on update-with-comment is recorded on BUG-2716 with a pointer in the handler. * docs(server,cli): state the reverse read-skew at the guard and the CLI non-rendering decision (TASK-2760, codex r5) * fix(store): debounce merge refuses a comment-linked row inside the UPDATE itself (TASK-2760, codex r6) The read-then-write left a window in which a comment could link the chosen row before the merge overwrote its agent stamp. The merge is now one statement whose predicate re-checks the link under the row write, and a zero-row merge falls through to a fresh insert. Prose corrected: a later update looks past a frozen row, to an older unlinked one or a fresh one. * fix(store,test): one freeze mechanism, and the window-edge leak proven end to end (TASK-2760, matrix survivors) The debounce SELECT-side exclusion became redundant once the UPDATE's own predicate refused linked rows, and its 'look past to an older unlinked row' semantics folded a later change into an earlier entry — a linked row now simply ends the coalescing run. And the server suite could no longer tell the SQL exclusion from the restored in-memory guard, because it only exercised the same-page case; a test now drives the page-edge case codex found (comment outside its window, activity inside), where only the query can help. * fix(web): drop a duplicate nowrap in EpisodeFeed — the rule already had it (TASK-2760, codex r7) Corrects the round-4 sweep count: of seven 24ch agent-label rules, two lacked white-space: nowrap (both timeline cards), not three. |
||
|
|
de3c9b818f |
feat(web): name the agent on the admin per-user activity views too (TASK-2759)
Codex round 12 — and it corrects MY exemption, not codex's reading of it. I listed these two tabs as exempt because their local row type omitted `metadata`. True, and the wrong reason: handleAdminGetUserActivity serializes whole models.Activity rows, so the stamped name was on the wire the entire time and only the client type dropped it. By this unit's own discriminator — does the surface hold an Activity? — they were never exempt. Verified against the handler before changing anything. The consequence was the exact gap the audit log had, on the same rows: an admin reading a user's activity saw "Updated an item via cli" with no way to tell which agent acted. The lead ruled the audit log IN on this discriminator; these belong in for the same reason. Rendered with the same rules as every other surface — <bdi>, bounded at 24ch, title for the full value, nothing shown when no name was stamped. Tests assert the binding at this surface (CONVE-19), including the empty case, the non-agent case and the bidi one. Docs updated: the surface list in the README and both SKILL.md copies now names the admin console's audit AND per-user activity views. The precision of that list is what round 2 was about, so it moves with the code. Claude-Session: https://claude.ai/code/session_01Cpr3teiHHsgcTmg2xhHA86 |
||
|
|
fa22b6680e |
docs+test: correct two over-claims and pin name escaping (TASK-2759)
Codex round 2, fresh angles.
P1, accepted — my own docs over-claimed. The README and both SKILL.md
copies said the name appears wherever agent actors appear, including "item
timelines". Comments, version snapshots and note/decision entries carry the
actor KIND and no name (that is the exempt set the plan named, and TASK-2760
files the comment half), so on a timeline only ACTIVITY entries show it. Both
now say which entries carry it and which read "Agent".
P2, accepted — the README's fallback was wrong in a way that mattered. When
nothing resolves a name, the CLI omits X-Pad-Agent entirely (client.go:1884),
so actorFromRequest records the write as "user": it is attributed to the
PERSON, not to a generic "agent". Verified both call sites rather than
reasoning from the label. The generic "agent" rows that do exist come from
pre-naming writes and from audit events logged without agentMeta.
P2, accepted — the name is attacker-influenced text and every test used
benign values, so a rewrite to {@html} would have passed. Added a markup
payload at two surfaces that build their labels through different paths,
asserting no element is created and the text survives intact.
Claude-Session: https://claude.ai/code/session_01Cpr3teiHHsgcTmg2xhHA86
|
||
|
|
a3ba6eec6c |
docs: the "name your agents" story for agent attribution (TASK-2759)
The README's For AI Agents section promised that agent actions are attributed, and said nothing about naming the agent — which was fair while nothing rendered the name. Now that five surfaces do, the section carries the precedence (.pad.toml agent_name -> $PAD_AGENT -> detected runtime), where the name shows up, and that Pad renders it verbatim rather than keeping a list of approved names. The honesty framing is QUOTED from ResolveAgentName's own contract comment rather than restated: the header is self-declared, an agent that omits it is indistinguishable from the human whose credentials it uses, and a human running `! pad ...` in an agent's terminal inherits that attribution. It is a label an actor chose, not evidence about who acted — which is also why the admin audit log shows both the agent and the account. Both SKILL.md copies gain one clause: the name an agent sends is now DISPLAYED, so a specific name beats a generic client id. Their existing attribution principle was already accurate and is otherwise untouched. Claude-Session: https://claude.ai/code/session_01Cpr3teiHHsgcTmg2xhHA86 |
||
|
|
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
|
||
|
|
6f16003199 |
fix: surface implementation notes + decision log in the item timeline (BUG-2301) (#1144)
* fix(server): merge implementation notes + decision log into the item timeline (BUG-2301) `pad item note` and `pad item decide` have written structured entries since |
||
|
|
a963e68395 |
docs(skills): de-assume the slash-command surface + route onboard shortcut through the canonical playbook (BUG-2573/2574/2575) (#1139)
* docs(skills): de-assume the slash-command surface + route onboard shortcut through the canonical playbook (BUG-2573, BUG-2574, BUG-2575)
Three coherent drift fixes across the two skill trees:
BUG-2573 — skills/pad/SKILL.md is the embed source `pad agent install`
writes for Claude Code, Codex, Cursor, Windsurf, OpenCode, Amazon Q,
Junie AND pure-MCP agents, but three sentences presented the Claude Code
slash command as THE invocation ("There is one command: /pad <anything>",
"On every /pad invocation", "the first token after /pad"). Reframed per
the PLAN-1847 house pattern: natural language is canonical, typed forms
are per-surface shortcuts, and a read-/pad-as-shorthand rule covers the
rest of the document. Verified through the installed artifact, not just
the diff: built the binary and ran `pad agent install codex` — the
reframed text reaches the non-Claude skill verbatim.
BUG-2574 — plugin/skills/onboard/SKILL.md (the most direct onboarding
route a plugin user has) inlined its own post-link setup script, silently
opting that surface out of the workspace-owned, user-editable onboard
playbook — a customized playbook never fired via the shortcut, and the
inline copy covered roughly the build mode only. The post-link half now
loads and follows the playbook (with exact-title library activation —
`pad library activate "Onboard a workspace"`, verified against the CLI's
actual arg form) and routes needs_onboarding=false to the playbook's
revisit mode. The pre-link whoami-gated half stays as BUG-2541 left it.
Checked the other dedicated plugin skills for the same class: status and
capture inline nothing playbook-owned — no change needed.
BUG-2575 — plugin/skills/pad/SKILL.md didn't know specs are decomposable:
added the "break SPEC-1 into tasks" routing entry and the plan-or-spec
wording in the decompose workflow, matching decomposePlaybookBody and the
embed source. Also synced the one other surface-agnostic drift found in
the sweep: the convention_index note that a list without --full has no
content field.
Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V
* docs(skills): fix onboard-skill mode enum, needs_onboarding semantics, and reactivation path per Codex review (round 1)
Three corrections to the rewritten post-link half, all verified against
playbook_library_onboard.go: the mode enum is auto/build/audit/revisit
(auto default) with `defaults` a separate fast-path flag — the "four
modes incl. defaults" framing came from the tracking bug's own body and
was wrong; needs_onboarding:false only means a user-created item exists,
not that onboarding ever ran, so the skill no longer declares setup
complete on it; and a draft/deprecated onboard playbook must be
reactivated in place, since invocation_slug is workspace-unique and
library activation beside an existing entry duplicates or fails.
Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V
* docs(skills): activation before load + honest auto-mode routing per Codex review (round 2)
The post-link half now runs as an ordered three-step: ensure-active
(reactivate in place, library only when absent), THEN load the body,
then mode framing — a literal reader of the previous text ran
`pad playbook show onboard` before the existence check, failing on
missing playbooks and loading stale drafts. And the mode note no longer
claims auto picks "a fuller pass": verified against the playbook's
pre-flight, auto routes ANY user-created item to revisit, so the skill
now says to pass an explicit mode=build/audit override (which the
playbook honors) when the user says the workspace was never really set
up.
Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V
* docs(skills,mcp): propagate activation ordering + auto-mode routing to the sibling onboarding routes per Codex review (round 3)
The round-2 corrections lived only in the focused onboard skill; the
embed skill's Onboarding entry, the plugin pad skill's, and the
pad_onboard MCP prompt still said load-then-activate with a bare
library-activate fallback, and none warned that the playbook's auto mode
routes any workspace with user-created items to revisit. All three now
carry the same semantics: ensure-active first (reactivate a
draft/deprecated entry in place — invocation_slug is workspace-unique,
so library activation beside an existing entry duplicates or fails),
then load, plus the explicit mode=build/audit override note.
Claude-Session: https://claude.ai/code/session_017jD6t1zjxGSq47SQpZfp1V
|
||
|
|
ac05d8a2b1 |
fix(cli): fail fast and quiet on non-interactive workspace init (BUG-2538, BUG-2577) (#1111)
* Fail fast and quiet on non-interactive `pad workspace init` BUG-2538: initCmd drove runBrowserSetup/doBrowserLogin unconditionally when the instance needed first-run setup or login, blocking a non-interactive caller (script, CI, headless agent) on a browser handoff nobody can complete. Gate both branches on canPromptForConfig(), mirroring the precedent already used by `pad init` (init.go:205-206), and fail fast with a hint pointing at `pad init --email/--name/--password` or `pad auth setup`/`pad auth login`. BUG-2577: offerSkillInstall (shared by workspace init and workspace link) printed a "(Y/n): " prompt even when the answer would be auto-defaulted rather than read, because it gated on cli.IsTerminal() (stdin only). Switch to canPromptForConfig() (stdin AND stdout), which is the same predicate now used for BUG-2538 and the more robust of the two checks already in the codebase. Claude-Session: https://claude.ai/code/session_01BhQoeaWXxJbvw86ezzK8dt * Fix wrong remedy in BUG-2538's !Authenticated error message codex r1: the !Authenticated branch suggested `pad init --email/--name/--password`, but those headless flags only bootstrap the first admin account and only fire when SetupRequired — for an already-set-up-but-unauthenticated instance, `pad init` falls through to its own ungated Step 4 re-auth (BUG-2592), so the suggestion relocated the hang instead of avoiding it. Drop the pad-init suggestion in this branch only; point at `pad auth login` and note there's no non-interactive login path yet. SetupRequired branch is unchanged — its pad-init suggestion is correct for that state. Claude-Session: https://claude.ai/code/session_01BhQoeaWXxJbvw86ezzK8dt * Fix two more inaccurate remedies flagged by codex r2 1. SetupRequired branch: `pad init --email/--name/--password` silently eats the caller's workspace name/--template — pad init creates its own CWD-named workspace as a side effect, so a re-run of the original `pad workspace init <name> --template <t>` short-circuits on the link pad init just made with no signal <name>/<t> were ignored. Switch the remedy to `pad auth setup --email/--name/--password`, which bootstraps the admin account only (no workspace side effects), then re-run the original command. 2. !Authenticated branch: the "no non-interactive login path exists" claim was false — `pad auth login --interactive` reads email/password off a plain, TTY-ungated bufio.Reader (doInteractiveLogin, cmd_auth.go:554+; BUG-1886 made it piped-bytes-safe), so it works fine when credentials are piped in. Reworded to point at it and dropped the incorrect BUG-2592 reference (that bug tracks pad init's ungated Step 4, not a missing login mechanism). TestWorkspaceInitNonTTYSetupRequired's assertion updated from "pad init" to "pad auth setup" to match; TestWorkspaceInitNonTTYNotAuthenticated needed no change (still asserts "pad auth login"). Claude-Session: https://claude.ai/code/session_01BhQoeaWXxJbvw86ezzK8dt * Update skill docs invalidated by the non-interactive fast-fail fix codex r3: BUG-2538/BUG-2577 made this diff's own docs stale. Four files (skills/pad/SKILL.md, plugin/skills/pad/SKILL.md, plugin/skills/onboard/SKILL.md, plugin/skills/capture/SKILL.md) still say non-interactive `pad workspace init` on a configured-but- unauthenticated machine "blocks for minutes with no non-interactive fallback" — that was true pre-fix (per BUG-2541's verification) and is false now. Reworded the WHY without dropping the underlying do-not-run-blind guidance: an agent's tool call is always non-interactive, so it now gets a fast, actionable error instead of a hang, but the error still just says a human needs an interactive terminal — `pad auth whoami` remains the right check to run instead. Where the docs' `pad init` claims are about the still-unfixed session-expired path (BUG-2592, this diff's Step-4 sibling, left untouched), those claims are unchanged and now cite BUG-2592 explicitly. skills/INSTALL.md:24 updated separately (P3): notes the non-interactive silent-install branch of `pad workspace init`'s skill offer, alongside the existing interactive-prompt description. Docs only, no Go changes — go build/test and embed.go's //go:embed skills/pad/SKILL.md still resolve; no test asserts the old wording. Claude-Session: https://claude.ai/code/session_01BhQoeaWXxJbvw86ezzK8dt |
||
|
|
403a6de19d |
docs(skill): structured bootstrap-failure branch + onboarding precondition in the embed source (BUG-2541) (#1100)
* docs(skill): structured bootstrap-failure branch + onboarding precondition in the embed source (BUG-2541) `skills/pad/SKILL.md` — the //go:embed source `pad agent install` writes into user projects — had TASK-2537's minimal safety note but not the structured branch the plugin copy carries. Ports it, minus the Claude-Code-specific shell advice (the embed source also serves Codex, Cursor, Windsurf, OpenCode and pure-MCP agents with no shell at all), so it now names the two stderr signatures as separate cases with opposite handling. Also adds the Onboarding routing entry's missing precondition. It sent the agent to load the onboard playbook, which lives IN a workspace — so on the unlinked path `pad playbook show onboard` fails exactly the way bootstrap just did, and the entry routed into a dead end. Ride-along from the lead's citation sweep: the stale "hangs indefinitely / no timeout" wording still shipped in plugin/skills/onboard/SKILL.md and plugin/skills/pad/SKILL.md (two places). All three now carry the bounded wording. RE-VERIFIED RATHER THAN INHERITED, per this item's own requirement — and the inherited correction needed one too: - Read the code myself. First-admin setup is capped at 20m (bootstrap.go::bootstrapPollTimeout). The auth poll (cmd_auth.go::pollAndSaveCLIAuth) has NO wall-clock limit of its own: it exits only on ctx.Done, `approved`, or `expired`, and `continue`s past transient errors. So the ~5m bound is entirely the server-side session TTL (cli_auth_sessions.go::cliAuthSessionTTL) — if the server becomes unreachable after the session is created, it polls forever. "Bounded, not indefinite" is right for the ordinary case and wrong for that one; the skill text now says both. Filed separately. - Observed it, not just read it. On a configured-but-unauthenticated HOME, `pad workspace init` printed the browser URL and was still waiting when a 25s cap killed it. `pad auth whoami` returned in 0.106s in that state AND in the unconfigured one, with distinguishable output — the "fast, safe" claim the whole branch rests on. - The exact stderr strings were wrong in both copies: the not-configured case has NO `Error:` prefix (`Pad is not configured. Run 'pad auth configure' first.`), only the unlinked one does. Corrected from captured output. Verified through `pad agent install claude` into a scratch project and read back off the installed file, not the diff. Claude-Session: https://claude.ai/code/session_01QCMLhHQBrMHVML3YKdm4Cd * docs(skill): `pad auth whoami` blocks in a TTY when unconfigured — qualify the claim (codex review) Both plugin copies said `pad auth whoami` "never blocks waiting on input". It does, in one state: `whoamiCmd` → `getConfiguredConfig()`, and on an unconfigured machine that enters the interactive configure flow whenever `canPromptForConfig()` is true — i.e. stdin AND stdout are both terminals (configure.go:357). My own measurement (0.106s) was non-interactive, so it could never have caught this; Codex found it by reading the call path. The embed copy already had the right qualification ("in non-interactive use it returns immediately"); this brings the two plugin copies in line and says why the qualification is the operative one for an agent. Codex's other finding — that the embed source's opening still frames `/pad` as THE command, for surfaces with no slash commands — is real but pre-existing and editorial rather than part of this port. Filed as BUG-2573. The auth-poll timeout gap found while re-verifying the hang is BUG-2572. Claude-Session: https://claude.ai/code/session_01QCMLhHQBrMHVML3YKdm4Cd * docs(skill): `pad init` does not fail fast either — correct both plugin copies (codex round 2) Both plugin copies told agents that in the configured-but-unauthenticated state, `pad init` "fails fast" and merely "needs a TTY to complete" — offered as the contrast to `pad workspace init`'s browser-poll block. It is false. `cmd/pad/init.go`'s Step 4 calls `doBrowserLogin` with no TTY guard when the server is already initialized but the client isn't authenticated. Observed: on a configured-but-unauthenticated HOME, non- interactive, `pad init` printed the same browser URL and was still waiting when a 20s cap killed it — identical to `pad workspace init`. That parenthetical was the one thing in the paragraph that could have made an agent run a command instead of handing back, so it was the worst line to have wrong. Both copies now say it is no safer as a probe. Third claim in these three files this task that was wrong because it was reasoned rather than run — the first two being "hangs indefinitely" (bounded, mostly) and "whoami never blocks" (it prompts in a TTY). Codex's other two round-2 findings are real but out of this port's scope and filed: BUG-2574 (the plugin onboard skill inlines its own script instead of loading the canonical onboard playbook) and BUG-2575 (plugin decompose entries omit SPEC targets the embed source and playbook both support). Claude-Session: https://claude.ai/code/session_01QCMLhHQBrMHVML3YKdm4Cd * docs(skill): narrow the `pad init` claim to the state it is actually true in (codex round 3) The previous commit replaced one over-broad claim with the opposite one. "Fails fast" was wrong for the configured-but-unauthenticated case; "no safer as a probe, blocks identically" is wrong for the genuinely-unconfigured one. Both measured, non-TTY, same binary: unconfigured → 0.106s, "Error: Pad is not configured..." configured-but-unauthenticated → browser URL, still waiting at 20s The instruction ("don't run it yourself") was right in both readings, so this is the justification being wrong rather than the advice — which is exactly the failure mode this whole item is about, and I reproduced it while fixing it. Both plugin copies now say which state each behaviour belongs to. Codex's other round-3 finding — that the plugin onboard skill gates its recovery branch on `.pad.toml` being absent, so a STALE link skips it entirely and dead-ends at the same bootstrap failure — is real and is the pre-link half of the same skill's problem. Added to BUG-2574 rather than widened into this port. Claude-Session: https://claude.ai/code/session_01QCMLhHQBrMHVML3YKdm4Cd |
||
|
|
212d59e7c6 |
fix(cli,server): make agent attribution actually happen (BUG-2542) (#1088)
* fix(cli,server): make agent attribution actually happen (BUG-2542)
Agent CLI writes were recorded as the human whose credentials they used.
Three independent defects, each verified by reading the path AND by
probing a live instance — the item deliberately held the mechanism open,
so none of this is inherited.
1. THE HEADER WAS NEVER SENT. actorFromRequest sets actor="agent" on one
signal: the X-Pad-Agent header. The only code that sets it took the
value from `agent_name` in .pad.toml and nowhere else — no
environment detection, no session detection. This repo's .pad.toml
has only `workspace`, so the header has never been sent from here and
every agent write has looked human. ResolveAgentName now resolves
.pad.toml → $PAD_AGENT → detected runtime.
2. ITEM CREATE DISCARDED THE ACTOR. createItemChecked called
actorFromRequest and kept only the source (`_, src :=`), never
setting input.CreatedBy, so store.CreateItem fell through to its
"user" default — even for an agent that DID send the header.
Comments have always stamped it correctly; item creation silently did
not, which made the skill's own contract false on its own terms.
3. SINGLE-ITEM PATCH NEVER STAMPED LastModifiedBy. Bulk ops do
(handlers_items_bulk.go); the single-item path did not, so an item
edited only by agents read as human-edited.
Only entries VERIFIED against a live session belong in the runtime
detection table, so it has exactly one: Claude Code exports CLAUDECODE=1
to child processes, confirmed by reading a pad subprocess's environment
inside one. Guessing at Cursor/Windsurf/Aider variable names would put
unverified claims in a shipped binary and misattribute silently when
wrong; those set $PAD_AGENT until someone confirms a signature.
WHAT THIS DOES NOT DO, stated in the code and the skill rather than left
for someone to assume: the header is client-supplied and self-declared.
An agent that omits it is indistinguishable from the human it borrows
credentials from, and a human running `! pad ...` inside an agent's
terminal inherits that environment and is attributed to the agent. This
makes the trail HONEST, not VERIFIED — it is not a basis for
machine-verifiable human-approval provenance, which needs a channel the
agent cannot author at all. The incident behind this item is exactly
that distinction: an agent's relay of a human's words was recorded
indistinguishably from the human typing them.
Contract corrected in both skill copies, since the item's first question
was which of contract and behavior was wrong. It was the contract: it
promised automatic agent attribution that only ever applied to
workspaces that had opted in.
Tests, each mutation-tested against its own defect reverted alone:
- TestResolveAgentName — precedence plus the negative that makes it mean
something: a plain human shell must still resolve to "". Fails 2/5
reverted.
- TestItemAttribution_AgentVsHuman — agent and human legs for create,
update and the create-stamp-survives-edit invariant. Fails on the
create stamp reverted; fails 2/2 on the update stamp reverted.
The update leg deliberately uses the OTHER writer: insertItemTx seeds
last_modified_by FROM created_by, so a same-writer edit passes whether
or not the PATCH stamps anything — the first version of this test did
exactly that and passed its own counterfactual. Caught only because
each fix was reverted separately.
- TestItemAttribution_ExplicitBodyValueWins — an explicit body value
still beats the header.
End-to-end on a live instance through the real CLI, no .pad.toml opt-in:
agent session → created_by/last_modified_by/comment all `agent`; same
binary with CLAUDECODE stripped → all `user`.
Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag
* fix(server): artifact import wrote a UUID into created_by (BUG-2542)
Found by Codex while reviewing the attribution fix. handleImportArtifact
set `input.CreatedBy = u.ID`, which is the wrong DOMAIN for the field
rather than merely the wrong value: created_by holds the role — "user"
or "agent" — and consumers compare it against those literals
(CommentThread.svelte, TimelineVersionCard.svelte). An imported item
matched neither and rendered as neither.
It also would have defeated the fix in the parent commit at this path: a
non-empty CreatedBy suppresses the actor stamp, so imports would have
kept a UUID while every other create path started recording the actor.
The line contradicted the comment directly above it, which said Source
was being left blank precisely so createItemChecked could stamp it "like
every other create path". Now both fields are left blank and stamped
together.
The user's identity has its own home — the items.created_by_user_id
column — which no create path currently populates. That is a separate
gap and is not widened into this change.
Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag
* fix: close the remaining attribution bypasses Codex found (BUG-2542)
Review found no P1s and three P2 families beyond the artifact-import bug
already fixed in
|
||
|
|
f241a6298e |
docs(skill): port the applicable plugin-review corrections into the embed source (TASK-2537) (#1087)
* docs(skill): port the applicable plugin-review corrections into the embed source
skills/pad/SKILL.md is the //go:embed source that `pad agent install`
writes into user projects; it had drifted from corrections made to the
plugin's copy during TASK-2534's review rounds. Selective port — the two
files diverge by design (surface-agnostic vs Claude-Code-only), so none
of the plugin's Claude-Code-specific material comes across.
Each ported item re-verified against the code, since line numbers and
wording differ between the copies:
- Ideation example passed `--content "..." --stdin` together.
cmd_item.go:141 shows --stdin OVERWRITES the --content value and then
blocks on io.ReadAll with nothing piped, so the example as written
hangs. Dropped --stdin.
- Retro's plan-status flip carried no --comment, contradicting Key
Principle 2 four lines below it.
- Key Principle 2 named `blocked` as a task status. templates.go:157 has
open / in-progress / done / cancelled — no `blocked`.
- The role-board pointers claimed `pad server open` → /{workspace}/roles.
cmd_server.go:999 appends only the workspace slug; there is no
sub-path. Replaced with navigate-to-the-Roles-page wording.
Two more from the same review rounds that apply here and were not listed
on the task, found by diffing the copies:
- The convention/playbook BODY loads used `--format json` without
`--full`. Since the v0.9 summary shape, `pad item list` returns
cli.ToItemSummaries, which has no `content` field at all — so a skill
told to "follow ALL returned conventions" was reading titles. Added
--full to the seven trigger-load examples and to the retro's task
load, where the bodies are the point.
- `open "$IMG"` is macOS-only, in a file installed on every platform.
Not ported, and not a gap in this file: the whoami-gated
`pad workspace init` routing. That correction guards a blind self-heal
in the plugin's bootstrap-failure branch, and the embed source has no
such branch — its only `pad workspace init` mention is a neutral
see-also. The absence of ANY bootstrap-failure guidance here may be
worth its own item; inventing that section is outside a port.
Verified through the real embed path, not by reading the diff: rebuilt
and ran `pad agent install` for both targets (claude → .claude/skills,
codex/cursor/windsurf/opencode → .agents/skills) into scratch dirs, and
confirmed the installed artifacts carry every correction and none of the
superseded strings. go test ./... green.
Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag
* docs(skill): --full on the eighth body-load; file the bootstrap-failure gap
Codex found one miss and one scope question.
The miss: the `convention_index` bullet's own body-load example lacked
both `--format json` and `--full`, so the call that the bullet exists to
describe — pull the triggered convention BODIES this index only names —
came back in the summary shape with no `content`. It is the eighth such
call; I corrected seven and missed the one inside prose rather than in a
code block. Added, with a clause saying why, since this is the bullet a
reader consults specifically to learn how to fetch bodies.
The scope question: the embed source has no bootstrap-failure branch at
all, so an unlinked workspace routes into onboarding that cannot run,
and a naive `pad workspace init` self-heal can hang the tool call
indefinitely. Codex is right that it is a real gap and right that it is
surface-agnostic. It is not a port, though — the plugin's correction
guards a self-heal this file does not contain, so there is nothing here
to correct, only something to write. Filed as BUG-2541 with the failure
modes, the verified hang, and the de-Claude-ing needed, rather than
widened into this PR.
Re-verified through `pad agent install`: 9 body-load calls now carry
--full in the installed artifact.
Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag
* docs(skill): warn off the blind `pad workspace init` self-heal
Codex held on the bootstrap-failure gap after I deferred it to BUG-2541,
and the objection is fair on one point: deferring the whole branch left
the shipped artifact one step from the hazard, since the documented
fallback ("use the individual CLI calls") needs the same workspace link
that just failed, so a reader following this file has nowhere to go and
`pad workspace init` is the obvious next reach.
So the hazard gets addressed here and the structure stays in BUG-2541.
Four sentences, surface-agnostic: bootstrap failing usually means setup;
the individual-call fallback won't help; do NOT run `pad workspace init`
blind, because on a configured-but-unauthenticated machine it hangs
indefinitely on a browser-setup URL with no timeout and no
non-interactive fallback — wedging the tool call rather than failing it;
gate on `pad auth whoami` and hand back to the user otherwise.
That IS what TASK-2537's finding-1 asked for — "the same whoami-gated
treatment" for this file's onboarding-adjacent text. I first read its
parenthetical ("if any references pad workspace init as a blind
self-heal") as gating the whole item, and since this file has no such
reference, as nothing to do. The intent was to keep the file from
leading an agent into the hang, which it could.
What stays in BUG-2541: the two stderr signatures as named cases, the
Onboarding routing entry's missing precondition, and re-verifying the
hang rather than inheriting the claim. Noted there.
Verified through `pad agent install` again, not the diff.
Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag
* docs(skill): narrow the workspace-init warning to what the code does
I wrote that `pad workspace init` "hangs indefinitely ... with no
timeout", inherited from TASK-2534's write-up. Codex flagged it and it
is wrong. What the code does:
- first-admin setup polls under an explicit 20-minute cap
(internal/cli/bootstrap.go::bootstrapPollTimeout);
- the configured-but-unauthenticated branch polls a CLI auth session
every 2s with no wall-clock timeout of its own, but exits on the
server's `expired` status, and that session's TTL is 5 minutes (20 for
the setup handoff) — internal/store/cli_auth_sessions.go.
Bounded, then. The hazard is still real and still worth the warning — it
blocks an agent's tool call for minutes on a flow only a human at a
browser can finish — but the text now says that instead of claiming a
permanent wedge. Also qualified the whoami claim: it returns immediately
in NON-INTERACTIVE use; getConfiguredConfig can prompt on an interactive
TTY (cmd/pad/configure.go:80).
Worth naming because it is the same failure I had just written into
BUG-2541 as work to do — "re-verify the hang claim rather than
inheriting it" — and then committed the inherited claim as fact in the
same breath. An explanation I have not checked is a claim, not a hedge,
and putting it in a file that ships to users makes it everyone's. The
correction is on BUG-2541 too, so that item's body isn't left asserting
it.
Claude-Session: https://claude.ai/code/session_01QGbUKZBAZoWdEgiTNWsXag
|
||
|
|
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.
|
||
|
|
4bacea530f |
feat(mcp): expose pad_project ready + stale actions (#878)
Add read-only `ready` and `stale` actions to the pad_project MCP tool, mirroring the existing CLI `pad project ready` / `pad project stale`. `ready` returns the actionable backlog (query-oriented counterpart to `next`); `stale` lists items needing attention. Both HTTP dispatchers already existed; this wires them onto the catalog surface. `pad project reconcile` stays CLI-only (shells out to `gh` for live PR state — a local-git dependency MCP agents lack). Bumps ToolSurfaceVersion 0.12 -> 0.13 across version.go, instructions.md, README, CLAUDE.md; adds readOnlyActions entries, drift-guard test entries, and a SKILL.md routing line. TASK-2019 Claude-Session: https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS |
||
|
|
c846cff4fd |
feat(project): agent-accessible activity feed (pad project activity + MCP action) (#877)
* feat(project): agent-accessible activity feed (pad project activity + MCP action)
Add a non-streaming, bounded activity query so agents can catch up on
what other agents/users changed since they last worked — the query
counterpart to the live `pad project watch` SSE stream.
- CLI: `pad project activity [--limit N] [--actor user|agent] [--since DATE]`
backed by the existing GET /workspaces/{ws}/activity feed.
- MCP: `pad_project.activity` action (passThrough) + cloud HTTP route.
- Extend the activity endpoint with a server-side `since` date filter
(handler parse + store SQL clause) so limit/actor/since behave
identically across CLI, stdio MCP, and cloud HTTP transports.
- Bump ToolSurfaceVersion 0.11 -> 0.12 (drift guard, README, CLAUDE.md,
instructions.md) and add a SKILL.md querying-guidance line.
Tests: store since-filter test, HTTP dispatch test, catalog action test.
Claude-Session: https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS
* fix(mcp): mark pad_project.activity read-only in tool surface
Add activity to readOnlyActions so the serialized MCP tool surface emits
read_only:true (missing entries default to write). Spot-check it in
tool_surface_test.go to guard against regression.
Claude-Session: https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS
|
||
|
|
7aa5cb98f3 |
perf(bootstrap): compact JSON for agents; trim SKILL.md reference sections (#873)
Part A: `pad bootstrap --format json` now emits compact (no-indent) JSON via a new cli.PrintJSONCompact helper. Its canonical consumer is the /pad agent skill; pretty-print indentation was ~29% of the payload (49696 -> 35118 bytes on this workspace, saving 14578 bytes). Humans keep --format markdown. Part B (conservative): condense the Role Awareness section and the playbook-authoring guidance in skills/pad/SKILL.md to on-demand pointers, keeping the load-bearing core behavior + activation gotcha inline and ALL routing behavior intact. Saves 2764 bytes of fixed per-session overhead. No MCP tool-surface change; ToolSurfaceVersion unchanged. Claude-Session: https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS |
||
|
|
15ad930d78 | feat(bootstrap): add convention_index for triggered-convention discovery (TASK-2004) (#848) | ||
|
|
7ec19b5f7f |
feat(skill): NL-canonical playbook invocation in SKILL.md + MCP catalog (TASK-1859) (#749)
* feat(skill): NL-canonical playbook invocation in SKILL.md + MCP catalog (TASK-1859) Generalizes PLAN-1847's onboarding treatment to every invokable playbook. The invocation-model intro now establishes that natural language is the canonical way to invoke a playbook and `/pad`·`$pad`·`pad_playbook run` are per-surface shortcuts; the greeting, the rendered intent-match message, the examples preface, the plan/decompose routing entries, the "creating a playbook" section, and the Planning/Decomposition workflow subsections all lead with intent and label the slug forms as shortcuts. catalog_playbook.go's tool description gets the same reframing. Left untouched: the skill's own `/pad <anything>` entry-point syntax (that's Claude Code skill invocation, not a playbook slug) and onboarding (already NL-canonical from PLAN-1847). The plan/ideate/retro MCP prompts had no `/pad <slug>` references. Parent: PLAN-1858. Claude-Session: https://claude.ai/code/session_01KmxkPxLksjf1pmrZDpsnTJ * fix(skill): use pad_playbook tool's action/ref form, not CLI-style 'run', per Codex review (round 1) Round-1 review noted that 'pad_playbook run <slug>' reads as a non-existent CLI command — pad_playbook is an MCP tool invoked with action: run, ref: <slug>. Reworded all MCP-shortcut mentions in SKILL.md and the catalog tool description to the structured tool-call form so agents don't try a bogus command. Claude-Session: https://claude.ai/code/session_01KmxkPxLksjf1pmrZDpsnTJ |
||
|
|
09335c4916 |
feat(onboard): active "want me to set it up?" offer on needs_onboarding (TASK-1850) (#742)
Convert the passive needs_onboarding nudge into an active, lead-with-it offer, mirrored across both agent-instruction surfaces: - SKILL.md: the nudge rule now leads with "Want me to set it up?" and codifies offer-not-auto-run + respect-a-decline-for-the-session. - internal/mcp/instructions.md: previously had NO needs_onboarding rule at all, so pure-MCP agents got the bootstrap flag but were never told to act on it. Added a "New workspace: offer to set it up" section with the same offer wording, pointing at the pad_onboard prompt / pad_playbook get ref:onboard to actually run onboarding once accepted. Agent offers, never auto-runs. Agent-instruction surfaces only. Parent: PLAN-1847. Claude-Session: https://claude.ai/code/session_01KmxkPxLksjf1pmrZDpsnTJ |
||
|
|
7962fabaeb |
feat(onboard): make natural language the canonical onboard trigger (TASK-1849) (#741)
* feat(onboard): make natural language the canonical onboard trigger (TASK-1849)
The onboard procedure already single-sourced to the seeded playbook body —
both SKILL.md and the MCP pad_onboard prompt deferred to it rather than
restating it. But two gaps remained:
- Both hardcoded `/pad onboard`, a Claude-Code-ism. SKILL.md installs into
Codex (`$pad`) and others too, so the nudge/routing copy was wrong off
Claude Code. Reframe NL ("set up my workspace") as the canonical trigger;
`/pad onboard` · `$pad onboard` · the `pad_onboard` MCP prompt are now
per-surface shortcuts into the same playbook.
- The MCP prompt resolved the playbook via CLI commands (`pad playbook
list`/`show`) that a shell-less MCP client can't run. Rewrite it to use
the `pad_playbook` tool (action:list / action:get ref:onboard); CLI form
kept as a secondary note. This closes the real single-source gap.
Agent-instruction surfaces only (SKILL.md + MCP prompt text); no CLI/MCP
catalog changes (CONVE-1741).
Parent: PLAN-1847. Absorbs cancelled TASK-1848.
Claude-Session: https://claude.ai/code/session_01KmxkPxLksjf1pmrZDpsnTJ
* fix(onboard): use pad_library activate for shell-less onboard recovery per Codex review (round 1)
Round-1 review flagged that the MCP prompt's missing-playbook fallback sent
agents to the web UI even though pad_library action=activate is callable from
a shell-less MCP client. Activate "Onboard a workspace" via the tool, then
re-list, before deferring to the user.
Claude-Session: https://claude.ai/code/session_01KmxkPxLksjf1pmrZDpsnTJ
|
||
|
|
96a32aa39d |
feat(bootstrap,skill): add needs_onboarding flag + retire legacy Onboarding workflow (TASK-1504,1505) (#578)
PLAN-1496's bootstrap-signal + skill-cleanup pair, shipped together
because TASK-1505's nudge rendering depends on TASK-1504's bootstrap
field.
TASK-1504 (bootstrap: needs_onboarding):
- internal/store/items.go: new WorkspaceHasUserCreatedItems(workspaceID)
store method. Backed by SELECT EXISTS with the predicate
`source != 'template'` — defined as the inverse of template seeding
rather than enumerating user-side source values, so new attribution
surfaces (mcp, api, future) count automatically.
- internal/server/handlers_bootstrap.go: AgentBootstrap struct gets
the NeedsOnboarding bool field (always emitted — not omitempty,
since the agent reads it on every /pad invocation). BuildAgentBootstrap
computes it via the new store method. On query error the flag falls
back to false (safe default: don't nag).
- Visibility filtering deliberately omitted — needs_onboarding is a
workspace-level state signal, not a per-user view. Two members
reading bootstrap concurrently should see the same answer.
- Two focused tests:
- TestBootstrapNeedsOnboardingFlag walks the lifecycle (fresh
workspace → create user item → flag flips).
- TestBootstrapNeedsOnboardingIgnoresTemplateSeeds locks the
template-seeds-don't-count invariant on the startup template,
which ships seeded conventions, playbooks, and the onboard
playbook itself.
TASK-1505 (skill update):
- skills/pad/SKILL.md:
- Context Loading section: new bullet documenting needs_onboarding
with the exact nudge wording the agent should render when true,
plus the "don't nag past first user item" + "respect prior
decline" rules.
- Onboarding workflow section: deleted (~30 lines). Replaced with
a one-paragraph pointer at the /pad onboard playbook. The skill
is the dispatcher; the playbook body is the script.
- Routing entry under "set up my workspace": simplified from the
bloated PR #577 round-3/4 text into a clean two-bullet form
(canonical phrasing + legacy IDEA-1 phrasing both → /pad onboard).
- internal/mcp/prompts_data.go: the pad_onboard MCP prompt body
was duplicating the same step-by-step script the SKILL.md section
carried. Replaced with the same dispatch-to-playbook pointer.
internal/mcp/prompts_test.go: TestPromptsLockstep_CoreCommands
fragments updated to assert the new dispatch fragments
(`pad playbook list`, `pad playbook show onboard`).
Parent: PLAN-1496.
|
||
|
|
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.
|
||
|
|
aaa581b105 |
refactor(bootstrap): extend dashboard caps to active_items/active_plans/by_role (TASK-1422) (#541)
* refactor(bootstrap): extend dashboard caps to active_items/active_plans/by_role/suggested_next (TASK-1422) Implements IDEA-1421 (absorbed into PLAN-1410's v0.4 envelope). Extends the BootstrapDashboard wrapper from TASK-1413 to cap four more dashboard sub-arrays with parallel overflow counts, same shape and semantics as the existing attention/recent_activity caps. ## Struct + caps Four new int fields on BootstrapDashboard (all `,omitempty`): - active_items_overflow_count - active_plans_overflow_count - by_role_overflow_count - suggested_next_overflow_count Four new cap constants alongside the existing two: - bootstrapActiveItemsCap = 5 - bootstrapActivePlansCap = 5 - bootstrapByRoleCap = 5 - bootstrapSuggestedNextCap = 5 capBootstrapDashboard extended with four parallel truncate-and-count blocks — same shallow-copy mutation pattern, source pointer untouched (the dashboard endpoint still returns its full-length arrays per its own contract). ## Tests TestCapBootstrapDashboard rewritten to cover all six caps under one contract. `mk` now takes a `dashCounts` struct (Att/Rec/Items/Plans/ Role/Sugg) so each subtest exercises specific caps without populating the others. Each of the four existing subtests (under-cap-no-overflow, over-cap-truncates-and-counts-overflow, source-pointer-unchanged, exact-cap-no-overflow) now asserts the new caps too. Added tiny assertLen/assertOverflow helpers to keep the per-array assertion noise from drowning the contract being tested. bootstrapSectionBytes extended to surface the four new cap-effect lines when triggered, table-driven so future caps drop in cleanly. seedBootstrapSizeFixture updated to seed 6 in_progress tasks (was 5 open) so the new active_items cap fires visibly in the per-section breakdown: "active_items capped: 5 shown, 1 overflow". The status flip is deliberate — dashboard.active_items filters on isActiveStatus(), which excludes initial/terminal statuses; open tasks never appeared in the section. ## Budget bootstrapSizeBudget 7 KiB → 9 KiB. Note that this is FIXTURE-side growth, not shape-side regression: the fixture now seeds enough active items to exercise the new cap (active_items section was 0 bytes when tasks were status=open). The cap itself is purely a SAVINGS — on docapp it drops active_items from 7 → 5 entries with overflow_count=2. Budget history note in handlers_bootstrap_test.go updated with the TASK-1422 line and an explicit "fixture-side, not shape-side" explanation so future readers understand why the budget moved up. ## Out of scope - Slim BootstrapRole projection — TASK-1423. - Schema label + sort_order trim — TASK-1424. - ToolSurfaceVersion 0.3 → 0.4 bump — TASK-1418 (final PR). Parent: PLAN-1410. Resolves IDEA-1421 once merged. * fix(bootstrap): drop unreachable suggested_next cap (TASK-1422 follow-up) Address Codex P1 finding on PR #541: `suggested_next_overflow_count` was unreachable in production responses because `buildDashboardResponse` already truncates `SuggestedNext` to 3 upstream (see "Take top 3" comment in handlers_dashboard.go:854-858), while my bootstrap cap was 5. The cap-and-overflow logic could only have fired against synthetic test state, never against the real dashboard pipeline. Two responses to consider: 1. Lower bootstrap's cap to a number smaller than 3 — defeats the upstream design choice (3 IS the intentional limit). 2. Drop the bootstrap-side cap — clean, no dead surface. Going with (2). If the upstream cap is ever raised or removed, that's the moment to add a suggested_next_overflow_count back. Removed: - SuggestedNextOverflowCount field on BootstrapDashboard - bootstrapSuggestedNextCap constant - The truncate-and-count block in capBootstrapDashboard - The suggested_next row in TestCapBootstrapDashboard's dashCounts helper and all five subtest assertions - The suggested_next entry in bootstrapSectionBytes's cap-line loop The fixture still seeds tasks and a plan, so the no-cap path on SuggestedNext is naturally exercised through TestBootstrapSizeBudget. The godoc on BootstrapDashboard now explicitly calls out the exclusion + the upstream-cap rationale so a future reader knows why suggested_next is missing from the otherwise-uniform cap set. Parent: PLAN-1410 / TASK-1422. * docs(skill): align SKILL.md dashboard cap description with TASK-1422 Address Codex P3 finding on PR #541: the SKILL.md `Context Loading` section described only the two original cap fields (attention_overflow_count, recent_activity_overflow_count). After TASK-1422 the bootstrap response carries three more (active_items_overflow_count, active_plans_overflow_count, by_role_overflow_count), and the agent needs to know to pull the full set via `pad project dashboard` when any of them are > 0. Updated the bullet to enumerate all five capped sub-arrays and state the overflow-field pattern generically rather than per-field. Same in-PR-sync pattern used for TASK-1413, TASK-1415, TASK-1416. Parent: PLAN-1410 / TASK-1422. * docs(bootstrap): fix three stale comments after dropping suggested_next cap (TASK-1422 follow-up) Address Codex P3 finding on PR #541 round 3: three stale doc strings referenced the old shape (with suggested_next) after the cap was dropped in the prior commit. Updated: 1. handlers_bootstrap_test.go budget-history line for TASK-1422 — removed `suggested_next` from the cap list and added the "deliberately excluded — already capped to 3 upstream" rationale so future readers know why the otherwise-uniform cap set is missing one. 2. handlers_bootstrap.go BootstrapDashboard godoc — changed "two overflow counts" to "five overflow counts (one per capped sub-array)". 3. handlers_bootstrap.go capBootstrapDashboard godoc — changed "both caps are untriggered" to "all caps are untriggered". Tidy-up only, no behavior change. Same skill-↔-code sync hygiene that has been the running theme across PLAN-1410's review loops. Parent: PLAN-1410 / TASK-1422. * docs(bootstrap): update remaining stale call-site comment for capBootstrapDashboard (TASK-1422 follow-up) Final stale-doc cleanup per Codex P3 on PR #541 round 4: the BuildAgentBootstrap dashboard-wrapping call-site comment still listed only attention + recent_activity. Updated to enumerate all five capped sub-arrays for parity with the godoc on BootstrapDashboard / capBootstrapDashboard. Same hygiene as the previous commit; no behavior change. Parent: PLAN-1410 / TASK-1422. |
||
|
|
6a981e7433 |
docs(skill): compress NL Routing examples, trim playbook authoring, drop MCP note (TASK-1416) (#539)
* docs(skill): compress NL Routing examples, trim playbook authoring section, drop MCP note (TASK-1416) Final SKILL.md trim in PLAN-1410's skill-side compression series. Three targeted changes: 1. NATURAL LANGUAGE ROUTING — compress example density The "example phrasing → command" pairs in each sub-category were over-enumerated — the model handles intent matching without an exhaustive lookup table. Compressed each sub-category to its canonical pattern(s) while keeping the section structural map intact (Role management, Creating items, Querying, Updating, Working with attachments, Planning, Ideation, Dependencies, Reports, Retrospective, Onboarding, Creating a playbook). Pattern: where a section had 5-11 "intent → command" lines all illustrating the same command verb with slight wording variants, collapsed to a 1-2 line summary that describes the routing rule directly. Where a section had genuinely-distinct commands (e.g. Querying covers dashboard / next / list / search), kept one canonical line per command verb. 2. PLAYBOOK AUTHORING — replace worked examples with a compact pointer The "Authoring trigger-only" and "Authoring slug-invocable with arguments" subsections previously included full ~25-line heredoc examples — useful when the schema-aware --field parsing was new, now memorizable scaffolding. Replaced with a 2-line pointer listing the two authoring surfaces (CLI / Web UI) and the key `--field 'arguments=[...]'` shape. The model knows the heredoc pattern; the worked example was redundant. 3. MCP NOTE — dropped The "Note for agents using MCP instead of this skill" preamble (~700 B) only applied to readers of the file — agents loading this skill are by definition using the CLI surface, not MCP. The MCP catalog reference at getpad.dev/mcp/local stays the canonical source for that surface; removing the note avoids carrying its bytes in every skill load. Measurements: Before TASK-1416: 34,786 b / 479 lines After: 29,448 b / 393 lines Delta this PR: -5,338 b (-15%) Cumulative PLAN-1410 skill-side reduction (TASK-1414/1415/1416): Baseline (TASK-1410 start): 40,193 b / 567 lines After all three trims: 29,448 b / 393 lines Total reduction: -10,745 b (-26.7%) Parent: PLAN-1410. Remaining: TASK-1417 (final measurement back into the plan body) and TASK-1418 (ToolSurfaceVersion 0.3 → 0.4). * fix(skill): add status=active activation requirement to playbook authoring (TASK-1416 follow-up) Address Codex P2 finding on PR #539: the compressed authoring pointer dropped `--field status=active` from the example. New playbooks default to status=draft, but slug routing and trigger-intent matching only dispatch status=active entries. Following the trimmed instructions would create /pad <slug> playbooks that silently fall through to NL routing. The original (pre-TASK-1416) worked example included status=active; I lost it when collapsing to a pointer. Restored explicitly: - New "**Activation matters**" paragraph calling out the default-draft pitfall and the silent-fall-through behavior. - CLI example now includes `--field status=active`. - Web UI bullet explicitly mentions flipping draft → active before save. Same in-PR-sync pattern as TASK-1413's SKILL.md alignment commit and TASK-1415's activation-check correction. Parent: PLAN-1410 / TASK-1416. |
||
|
|
335b145343 |
docs(skill): replace Planning/Decomposition workflow fallbacks with one-liner playbook pointers (TASK-1415) (#538)
* docs(skill): replace Planning/Decomposition workflow fallbacks with one-liner playbook pointers (TASK-1415)
The Planning and Decomposition workflows in SKILL.md each had:
1. A leading "use the <slug> playbook" pointer (the canonical
entry point).
2. A multi-line "if the playbook isn't activated, fall back to
this inline workflow" block duplicating most of the playbook's
contract.
For software templates the playbooks auto-seed via
softwareStarterPlaybookTitles, so the fallback fires approximately
never. Non-software workspaces activate from the library UI, which
also makes the fallback transient at best.
Replaced each section with a one-liner pointer that:
- Names the canonical /pad <slug> invocation
- Explains how to confirm activation (bootstrap's playbooks array
or `pad playbook show <slug>`)
- Tells the user to activate via the library UI when missing,
and offer to walk through manually in the meantime — without
duplicating the playbook's step contract here
Measurements:
SKILL.md total: 36,473 → 34,786 bytes (-1,687 b / -5%)
SKILL.md lines: 501 → 479
Cumulative against PLAN-1410 baseline:
SKILL.md total: 40,193 → 34,786 bytes (-13.5% so far)
Parent: PLAN-1410. Next: TASK-1416 (NL Routing + authoring example + MCP note).
* fix(skill): correct playbook activation-check guidance (TASK-1415 follow-up)
Address Codex review findings on PR #538:
P2 — `pad playbook show <slug>` is not a valid activation check.
The resolver returns playbooks by invocation_slug regardless of
status, and default output omits status. A draft/deprecated
`plan` playbook would be treated as active. Corrected to direct
the agent at the bootstrap's `playbooks` array (which carries
status) for the activation check, and noted explicitly that
`pad playbook show` alone is insufficient.
P3 — The "**Planning:**" routing bullet still pointed at
"otherwise inline workflow (see below)" after the inline
fallbacks were removed in the parent commit. Replaced with a
pointer to library activation, matching the (now-trimmed)
workflow section.
Both findings would have left agents in a workspace without
active plan/decompose playbooks pointing at the wrong fallback.
Fixed in-PR so skill ↔ playbook contract stays strictly
synchronized (same pattern as TASK-1413's SKILL.md sync commit).
Parent: PLAN-1410 / TASK-1415.
|
||
|
|
efccea6dfe |
docs(skill): compress CLI Reference to patterns the skill drives (TASK-1414) (#537)
The CLI Reference section grew over time with every-flag enumeration,
multiple worked examples per command, and edge-case commands the
skill never invokes (webhooks REST API trivia, full --fields DSL
walkthrough for collection create, multiple per-command examples
that all illustrate the same flag pattern).
Compressed to the patterns the natural-language routing actually
drives, with `pad <cmd> --help` as the explicit escape hatch for
anything else.
Measurements:
- SKILL.md total: 40,193 → 36,473 bytes (-3,720 b / -9%)
- SKILL.md line count: 567 → 501
- CLI Reference section: ~6,500 → ~3,400 bytes (-48%)
Preserved:
- All command verbs the NL routing references (item create/list/
show/update/delete/search/comment/comments/bulk-update, role
list/create/delete, project dashboard/next/standup/changelog,
playbook list/show/run, attachment list/show/view/upload/
download, collection list/create, server info/open, auth whoami,
bootstrap).
- The hard rule against reading ~/.pad/attachments/ directly
(kept the explanation tight: "bypasses ACLs, breaks on Pad
Cloud / S3, skips the variant pipeline").
- The `--field key=value` schema-aware pattern with concrete
examples for convention + playbook (the two collections most
likely to drive its use).
- The two-mode collection create (`--fields` DSL vs `--schema`
full CollectionSchema), with the when-to-use guidance retained.
Dropped or compressed:
- Per-command multi-example listings (one canonical pattern each).
- The full Webhooks subsection — webhooks are REST-API-only and
the skill never invokes them directly; pointer in the catch-all.
- The trailing "Output Formats" footer — replaced by an inline
note at the section header that --format json works everywhere.
- Verbose per-line comments inside code blocks; the patterns are
self-documenting at this scale.
Parent: PLAN-1410 / TASK-1414. Bootstrap-shape work already in main
(TASK-1411/1412/1413). Remaining: SKILL.md workflow + NL routing
trims (TASK-1415/1416), then final measurement + version bump.
|
||
|
|
638a456f98 |
refactor(bootstrap): dedup recent_activity, drop convention slug, cap dashboard arrays (TASK-1413) (#536)
* refactor(bootstrap): dedup recent_activity, drop convention slug, cap dashboard arrays (TASK-1413)
Three bundled handler-level cleanups against PLAN-1410's bootstrap
shape. Total fixture savings: 8,992 → 6,355 bytes (-2,637 b / -29%).
1. Drop duplicate top-level `recent_activity`
AgentBootstrap.RecentActivity was bit-for-bit identical to
AgentBootstrap.Dashboard.RecentActivity. Removed:
- AgentBootstrap.RecentActivity field
- capRecentActivity() helper
- recentActivityWindow constant
- the time import (no longer used)
Fixture savings: -1,751 bytes.
2. Drop `slug` from AgentBootstrapConvention
Agents address convention items by ref (CONVE-N); slug was dead
weight. Removed the field + the population line in
collectAlwaysOnConventions.
Fixture savings: -78 bytes.
3. Cap dashboard.attention + dashboard.recent_activity to 5 in bootstrap
New BootstrapDashboard wrapper embeds *DashboardResponse (so the
wire shape stays compatible — same field names, same nesting) and
adds two overflow counts:
- attention_overflow_count (omitempty when zero)
- recent_activity_overflow_count (omitempty when zero)
The cap is applied via capBootstrapDashboard which shallow-copies
the DashboardResponse before truncating the slices, so callers
downstream of buildDashboardResponse (the dashboard endpoint
itself, the web UI) see their original full-length arrays
unchanged. `pad project dashboard` contract is preserved verbatim.
Fixture savings: -789 bytes (recent_activity capped 9 → 5;
attention untouched, fixture has 0 attention items).
Coverage:
- TestCapBootstrapDashboard (4 subtests): under-cap-no-overflow,
over-cap-truncates-and-counts-overflow, source-pointer-unchanged,
exact-cap-no-overflow. Locks in the cap contract independent of
the full bootstrap pipeline.
- TestBootstrapEmptyArraysNotNull updated: the top-level
recent_activity key was removed from the required-keys list,
with a separate assertion that guards against it reappearing.
- TestBootstrapEmptyWorkspace updated: removed the b.RecentActivity
nil-check; added a (defensive) check that dashboard's nested
recent_activity is non-nil when dashboard is present.
- bootstrapSectionBytes now surfaces the cap effect ("attention
capped: 5 shown, 4 overflow") when triggered, so the trim's
value is legible from CI output.
bootstrapSizeBudget tightened 11 KiB → 8 KiB to lock in the win.
Budget-history comment updated.
Out of scope (handled by later PLAN-1410 PRs):
- Skill-file trim (TASK-1414/1415/1416)
- Final measurement (TASK-1417)
- ToolSurfaceVersion 0.3 → 0.4 (TASK-1418, after all shape
changes land)
Parent: PLAN-1410.
* docs(skill): align SKILL.md bootstrap shape with PLAN-1410 / TASK-1413
The skill's `Context Loading` section described the old wire shape:
- `dashboard {...}` — active items, attention, suggested next, recent activity
- `recent_activity [...]` — capped to the last 24h
After TASK-1413 the top-level `recent_activity` field is gone (it was
a bit-for-bit duplicate of `dashboard.recent_activity`), and the
remaining `dashboard.recent_activity` is capped by COUNT (top 5) not
by TIME (24h window). The two cap fields (attention_overflow_count
and recent_activity_overflow_count) tell the agent how much was
trimmed so it can decide whether to follow up with a full
`pad project dashboard` query.
Per the Codex P2 finding on PR #536: documenting the new contract
in this PR keeps skill ↔ wire-shape strictly synchronized (no
window where the docs are wrong about the shape this PR ships).
Parent: PLAN-1410 / TASK-1413.
|
||
|
|
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.
|
||
|
|
c2014fa7f8 |
fix(cli): schema-aware --field parsing for non-string typed fields (BUG-1125)
pad item create/update --field key=value previously stored every value
as a string, so json / number / checkbox / multi_select fields were
rejected by the server-side validator. The new parseFieldFlag helper
fetches the collection schema once per command and parses each value
according to its declared field type:
- json / multi_select → json.Unmarshal
- number → strconv.ParseFloat
- checkbox → strconv.ParseBool
- text / url / select / date / relation / unknown → raw string
Schema-fetch failure degrades gracefully to pre-fix string-only behavior.
pad item list --field is unchanged (URL query param, not validator).
Verified against both repros: --field reading_time=3 on blogs (the
original number case) and --field 'arguments=[{...}]' on playbooks (the
json case that surfaced authoring the ship playbook). String fields show
no regression.
Skill update folded in: the "Authoring slug-invocable playbooks with
arguments" section in skills/pad/SKILL.md previously routed users to the
web editor as the only path for structured arguments. With this fix the
CLI handles it in one command, so the section now leads with the CLI
flow and demotes the web editor to an alternative.
|
||
|
|
9764b2fe92 |
docs: document playbook invocation surface (TASK-1387) (#525)
* docs: document playbook invocation surface (TASK-1387)
Closes out PLAN-1377 — Make Playbooks first-class invokable procedures —
by bringing the four user-facing docs surfaces up to date with the
shipped invocation model. The pad-web docs ship in a separate commit
(../pad-web@main: docs: document playbook invocation surface).
- CLAUDE.md — new Playbooks section after Data Model covering the
three invocation surfaces, the invocation_slug/arguments schema
fields, bootstrap-returns-metadata, the seeded ship playbook, the
web UI editor, and a code map. MCP section grows pad_playbook,
pad://workspace/{ws}/bootstrap, and the pad_set_workspace embedded
response note.
- skills/pad/SKILL.md — adds a "Creating a playbook" subsection under
natural-language routing with CLI examples for trigger-only and
slug-invocable playbooks, plus a Playbooks block in the CLI
reference (pad playbook list/show/run with parsing rules).
- README.md — one-line bump in the feature list mentioning the new
/pad <slug> invocation form and the seeded ship playbook.
Parent: PLAN-1377.
* fix(docs): correct bootstrap route + CLI arguments authoring per Codex review (round 1)
Codex round 1 findings:
[P2] CLAUDE.md cited GET /api/v1/workspaces/{ws}/bootstrap but the
implemented route is /api/v1/workspaces/{ws}/agent/bootstrap (server.go
line 1182). Documented endpoint would 404 for HTTP integrators.
[P2] SKILL.md '--field arguments=[...]' example would fail validation
— pad item create stores all --field values as strings, while
arguments is a json field type. Rewrote the slug-invocable-playbook
authoring guidance to direct agents at the web UI editor for
structured argument authoring (the canonical path the editor was
built for) with the CLI handling everything else. Same fix applied
to the pad-web /docs/agent-integration page in a separate
../pad-web@main commit.
Parent: TASK-1387 / PLAN-1377.
* fix(docs): use full /{username}/{workspace}/playbooks route in SKILL.md per Codex review (round 2)
Codex round 2 finding:
[P3] SKILL.md's recommended web editor path was '/{workspace}/playbooks',
but the SvelteKit route is '/{username}/{workspace}/playbooks'. The
prior path would 404 or land on the wrong workspace. Fixed.
The pad-web docs ship the matching fix in a separate commit at
../pad-web@main: docs(playbooks): use full /{username}/{workspace}
route path per review.
Parent: TASK-1387 / PLAN-1377.
* fix(docs): bump CLAUDE.md MCP tool surface to v0.3 + close SKILL.md backtick per Codex review (round 3)
Codex round 3 findings:
[P2] CLAUDE.md still labelled the tool surface as v0.2; internal/mcp/
version.go advertises ToolSurfaceVersion = '0.3' (since PLAN-1377 /
TASK-1380). Updated to v0.3 and added a note about what v0.3 introduced
(pad_meta.action: bootstrap, pad_set_workspace embedded-bootstrap
response, pad://workspace/{ws}/bootstrap resource).
[P3] SKILL.md's web-editor route had the parenthetical inside the
code span: '`/{username}/{workspace}/playbooks (click "+ New
Playbook")`' — closed the backtick after '/playbooks' so the
rendered code span is the literal path.
The pad-web docs ship the matching v0.3 bump in a separate commit at
../pad-web@main: docs(mcp/tools): bump tool surface to v0.3.
Parent: TASK-1387 / PLAN-1377.
* fix(docs): bump stale MCP catalog references to v0.3 per Codex review (round 4)
Codex round 4 finding [P2]:
Three places still described the MCP catalog as v0.2, contradicting the
v0.3 surface block that landed in this PR:
- CLAUDE.md 'MCP server' lede paragraph — bumped to v0.3, added
pad_playbook to the listed tools, and noted what v0.3 introduced.
- skills/pad/SKILL.md MCP note for MCP-using agents — bumped to v0.3,
added pad_playbook to the listed tools and called out the playbook
invocation surface + bootstrap action.
- README.md 'Tool catalog (v0.2)' block — bumped to v0.3, added the
pad_playbook row, the pad_meta.action: bootstrap row, the bootstrap
resource, and bumped tool_surface_version.
Parent: TASK-1387 / PLAN-1377.
* fix(docs): bump MCP server-side self-description to v0.3 per Codex review (round 5)
Codex round 5 finding [P2]:
Two MCP-server-internal documentation surfaces still advertised v0.2:
- internal/mcp/instructions.md — the markdown blob the server returns
to MCP clients as initialization instructions. Updated 'Tool surface
(v0.2) / Eight tools' to 'Tool surface (v0.3) / Nine tools', added
pad_playbook with list/get/run, added bootstrap to pad_meta's actions,
noted pad_set_workspace's embedded-bootstrap response, and added
pad://workspace/{ws}/bootstrap to the resource list.
- internal/mcp/catalog_meta.go — the pad_meta tool's Description string
said 'v0.2 tool catalog' twice. Bumped both to v0.3.
These ship inside the binary; MCP clients read them directly so v0.2
mentions there contradict the v0.3 catalog the handshake actually
advertises (ToolSurfaceVersion in version.go).
Parent: TASK-1387 / PLAN-1377.
* fix(docs): finish MCP self-description v0.3 cleanup per Codex review (round 6)
Codex round 6 findings [P3]:
[1] catalog_meta.go's padMetaTool block-comment said 'Three actions,
all handled inline' even though bootstrap (the v0.3 fourth action)
dispatches through env.Dispatch. Fixed both the count and the
dispatch description, added the bootstrap row to the action list.
Also corrected the v0.2 mentions in actionMetaToolSurface's comment
and removed the rollout-era language now that the cmdhelp walker is
retired.
[2] instructions.md said 'Nine tools, each with an action enum' but
pad_set_workspace doesn't take an action. Clarified the count as
'eight resource × action tools, plus pad_set_workspace (which takes
a workspace slug only)' and scoped the 'Always pass action' rule to
the eight resource × action tools.
Parent: TASK-1387 / PLAN-1377.
* fix(docs): finish MCP self-description nine-tool wording per Codex review (round 7)
Codex round 7 findings:
[1] catalog_meta.go's padMetaToolDescription still mentioned the
PLAN-969-rollout cmdhelp walker contributing to tools/list. The walker
was retired in TASK-981. Rewrote the tool-surface action description
to match current behavior and explicitly note pad_set_workspace is
registered separately (not enumerated by tool-surface).
[2] actionMetaToolSurface's comment claimed scope includes
pad_set_workspace; the impl only loops env.Catalog. Updated the
comment to be accurate — tool-surface enumerates the eight catalog
tools only, callers should account for pad_set_workspace as a known
extra.
[3] CLAUDE.md and README.md described the MCP surface as if every
listed tool was resource × action. pad_set_workspace takes
'workspace' only. Reworded both to match instructions.md's
'eight resource × action tools plus pad_set_workspace' framing.
Parent: TASK-1387 / PLAN-1377.
|
||
|
|
955553b6c2 |
docs(skill): rewrite /pad skill for bootstrap + slug routing (TASK-1383) (#522)
* docs(skill): rewrite /pad skill for bootstrap + slug routing (TASK-1383)
PLAN-1377 T6. Three substantive changes to skills/pad/SKILL.md:
1. Context Loading section now uses a single `pad bootstrap --format
json` call instead of four separate ones (project dashboard,
collection list, conventions list, role list). Documents the
AgentBootstrap struct's shape and explains why one call beats
four (~200-400ms saved per /pad invocation, stable shape,
no view stitching).
2. New "Playbook Invocation (slug routing)" section ahead of the
natural-language routing. Spells out the rule: if the first token
after /pad is an exact match against a kebab-case invocation_slug
from the bootstrap's playbooks array, dispatch to that playbook.
Otherwise fall through to NL routing. Examples cover /pad ship
PLAN-1377, /pad release 0.5.0, /pad draft-tweet TASK-X
platforms=x,bluesky, and the kebab-case discipline that keeps
/pad let's discuss IDEA-3 from misrouting.
3. "Before Performing Work" reframes the trigger-specific
convention/playbook lookup: bootstrap already carries the
always-on conventions + full playbook metadata, so the only
on-demand load is trigger-matched conventions. Schema vocabulary
reads from the bootstrap's collections[] payload, not a separate
`pad collection list` call.
Greeting note added: if bootstrap returns any playbooks with
invocation_slug, surface the user-callable set ("Playbooks available:
/pad ship, /pad release, ...") so users discover what's invokable —
same shape as the existing roles greeting.
Parent: PLAN-1377.
* fix(skill): filter playbook routing/greeting on status=active (TASK-1383)
Codex round 1: the skill's playbook routing rule and greeting surfaced
every entry with an invocation_slug, but bootstrap returns all
playbooks regardless of status (so draft/deprecated entries with a
slug got advertised as runnable). The skill now explicitly requires
status=active for both the greeting list and the slug dispatch
decision — drafts can keep their slug while in-flight without
accidentally firing.
Parent: PLAN-1377.
* fix(skill): trigger-based intent match also filters status=active (TASK-1383)
Codex round 2: the slug-routing fix from round 1 didn't extend to the
trigger-based intent-matching branch, so 'let's do a release' could
still surface a draft on-release playbook. Apply the same status=active
filter consistently.
Parent: PLAN-1377.
|
||
|
|
9fecb82e82 |
docs: surface --schema flag in SKILL.md + CLAUDE.md (TASK-1336) (#484)
PR #482 / TASK-1334 added the --schema flag to `pad collection create`. PR #483 / TASK-1335 wired it through MCP. Agents reading the live SKILL.md (embedded in the binary) need to see the new path or they'll keep reaching for --fields DSL and hit the original BUG-1284 symptom. Updates: - skills/pad/SKILL.md "Collections" section: now shows both --fields DSL and --schema JSON forms side-by-side. Calls out exactly when --schema is required (terminal_options et al.), and notes the label-from-key auto-fill so agents know empty labels are safe. - CLAUDE.md command list: adds the --schema variant alongside the existing --fields example so the project-level cheat sheet stays accurate. The CLI Long help (`pad collection create --help`) and the MCP tool description were already updated in TASK-1334 / TASK-1335. Parent: PLAN-1333. |
||
|
|
d1fb61097e |
docs(onboarding): document IDEA-1 trigger phrase across README, CLAUDE.md, and /pad skill (TASK-1138) (#406)
* docs(onboarding): document the IDEA-1 trigger phrase across README, CLAUDE.md, and the /pad skill (TASK-1138) Make the seeded onboarding entry point (PLAN-1131) discoverable in every doc surface a fresh user might land on. README.md Quick Start gains a follow-up paragraph after `pad init`. Names the trigger phrase verbatim so a copy-paste lands deterministically. Tone matches in-product hint copy from PR #403; no "tutorial" / "lesson" language. CLAUDE.md Authentication section gets a paragraph after `pad auth setup` pointing developers + agents at the same trigger phrase. Also enumerates the four seeded refs (IDEA-1 / PLAN-2 / TASK-3 / DOC-4) for context, with pointers to the source-of-truth code (internal/collections/templates_onboarding.go) and design history (PLAN-1131). skills/pad/SKILL.md Adds a bullet under the Onboarding routing section: an explicit "use pad to get IDEA-1" trigger and the schema-aware terminal-status guidance per collection (Ideas → implemented, Plans → completed, Tasks → done, Docs → archived). Frames the seed items as ordinary items the agent reads and acts on — no "onboarding mode" — so the no-marker / no-skill-detection design from PLAN-1131 stays clean. pad-web (../pad-web) is intentionally not touched — separate repo per CONVE-159. Spawned TASK-1142 to pick up the pad-web getting-started flow as a follow-up. Parent: PLAN-1131. Origin: IDEA-1128. * fix(docs): scope the IDEA-1 hint to post-workspace-creation, not bootstrap setup, per Codex review (round 1) Codex caught that the original wording suggested users could go straight to `use pad to get IDEA-1` after `pad auth setup`. But `pad auth setup` only creates the first admin account — no workspace. IDEA-1 is only seeded when a `startup`-template workspace is created (`pad init` or `pad workspace init`). Tightened to call out the precondition explicitly: a startup-template workspace must exist before the trigger phrase resolves. Spawned TASK-1143 to fix the matching CLI hint behavior — PR #403's `printIdeaOneTriggerHint` after `pad auth setup` has the same imprecision and should either drop the IDEA-1 mention or point users at `pad init` first. Out of scope for this docs PR. |
||
|
|
273d75c06e |
docs(mcp): refresh README + SKILL.md for v0.2 surface (TASK-976) (#359)
Updates the in-repo documentation to match what shipped in PLAN-969: - README.md's MCP section now describes the v0.2 catalog (8 tools, resource × action shape) instead of the retired v0.1 verb explosion. Documents both stability constants (CmdhelpVersion 0.1 + ToolSurfaceVersion 0.2) and points consumers at the structured error envelope contract. - skills/pad/SKILL.md gets a one-line callout that the MCP surface is hand-curated and distinct from the CLI verb tree this skill drives. Prevents future "I added a CLI command, why isn't it in MCP?" confusion. - CLAUDE.md was already updated in TASK-981; verified to match. Companion change for getpad.dev/mcp/local lives in ../pad-web. Parent: TASK-976 → PLAN-969. |
||
|
|
5e27989ab8 |
feat(attachment): add pad attachment view|show|list CLI surfaces (IDEA-898) (#321)
* feat(attachment): add `pad attachment view|show|list` CLI surfaces (IDEA-898) Agents and CLI users had no first-class way to fetch attachment bytes through the API: the only path to read an `` reference was to read the raw blob out of `~/.pad/attachments/<storage_key>`, which bypasses workspace ACLs, doesn't work on Pad Cloud / remote / Postgres deployments, skips the variant pipeline (TASK-872 / TASK-879 / TASK-880), and breaks when storage moves to S3. Three new subcommands wrap the existing REST endpoints: - `pad attachment view <id> [-o path]` — agent-friendly: with no `-o`, fetches to a fresh OS temp directory using the stored filename and prints just the absolute path on stdout (so `$(pad attachment view <id>)` composes cleanly into shell pipelines). Reuses `download`'s atomic temp-then-rename pattern via a shared helper. - `pad attachment show <id>` — HEAD-based metadata only; surfaces MIME, size, filename, ETag, Last-Modified. - `pad attachment list [--item REF] [--category X] [--attached|--unattached] [--collection ID] [--sort ...] [--limit N] [--offset N]` — workspace list. The `--item REF` flag resolves a TASK-5-style ref to a UUID client-side and passes it to a new `item_id` query param on the list endpoint (server side: AttachmentListFilters.ItemID, ~6 lines in the store + 1 in the handler). Skill update: `skills/pad/SKILL.md` gains a "Working with attachments" subsection plus a CLI Reference entry, both ending in the hard rule that agents must NEVER read directly from `~/.pad/attachments/`. * style(cli): gofmt AttachmentListParams field alignment CI's golangci-lint v2 flagged this with the gofmt formatter (configured with simplify: true in .golangci.yml). The contiguous Sort/Limit/Offset block at the end of the struct needs uniform column alignment — gofmt considers the doc comment above Sort attached to that field rather than a block separator, so the three int/string fields get aligned together. Verified locally with `golangci-lint run --timeout=5m ./...` (v2.11.4 to match CI) — 0 issues. Local make lint only runs `go vet ./...` and `golangci-lint` wasn't installed, which is why this slipped through; filing a separate follow-up to mirror the CI checks in the local workflow. |
||
|
|
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).
|
||
|
|
4d02616d54 |
refactor(skill): generalize /pad for domain-agnostic workspaces (TASK-613) (#145)
* refactor(skill): generalize /pad for domain-agnostic workspaces (TASK-613) The /pad skill is baked into the binary and runs the same regardless of whether the workspace is a software project, a hiring pipeline, or a research notebook. This change strips the software-centric framing without losing any dev workflow — dev-specific rules that used to live in SKILL.md are already shipped as conventions by the software templates (TASK-612). Changes ------- - Always-on conventions blurb now frames trigger vocabulary as workspace-dependent and gives non-software examples (anonymize candidate names, always cite sources). - "Before Performing Work" no longer hardcodes on-commit / on-pr-create as if they were universal. Replaced with a generic X→on-X mapping and a note that each template defines its own trigger set, with the software set called out as the common case. - "Creating items" examples mix dev and non-dev patterns and add a guiding note to match intent to the workspace's actual collections. - Planning workflow drops "Each task should be PR-sized" — replaced with a domain-neutral sizing guideline (software=PR, hiring=loop, research=question, etc.). PR-sizing for software workspaces is captured in the template's conventions. - Onboarding workflow now prefers the template's onboarding playbook as its first step, falls back to the codebase scan only when a playbook isn't present or is software-flavored. Makes room for hiring/interviewing/research templates to ship their own onboarding flows without skill churn. - Key principle #7 ("Keep it practical — tasks should be PR-sized") rewritten to reference the workspace's conventions. No CLI, API, or store changes — skill file only. Parent: PLAN-609. * fix(skill): replace literal on-X placeholder, include playbook triggers Per Codex review on PR #145: - The on-X placeholder in the pre-action example commands would be run literally by an agent and return 0 results, causing required conventions to be skipped. Replaced with explicit <trigger> angle- bracket placeholders plus concrete examples (on-implement, on-commit, on-review) an agent can substitute or run directly. - Software playbooks use a slightly different trigger vocabulary from software conventions (on-triage, on-release, on-review, on-deploy, manual). Updated the \"inspect the schema\" guidance to call out that agents should inspect BOTH the Conventions and Playbooks schemas to discover triggers, not the Conventions one alone. |
||
|
|
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
|
||
|
|
063ff92d00 |
feat: generalized parent/child items with progress tracking (#70)
* feat: generalize parent/child items — any item can have children with progress tracking
Replaces the phases-only task widget with a generalized parent/child system.
Any item (Phase, Idea, Doc, Task, etc.) can now be a parent of child items,
getting automatic progress bars, burndown charts, status grouping, drag-drop
reordering, and recursive expand/collapse up to 3 levels deep.
DB: migrate link_type 'phase' → 'parent' (migration 023)
Store: generalized methods (GetChildItems, GetItemProgress, SetParentLink
with cycle detection), drop collection filters, per-child terminal status
API: new /items/{slug}/children and /items/{slug}/progress endpoints
Frontend: ChildItems, ChildChart, NestedChildren components replace PhaseTasks
CLI: --parent flag (--phase kept as hidden alias), list/show/changelog updated
Docs: CLAUDE.md, SKILL.md, pad-web updated for parent/child model
Full backward compatibility: old 'phase' link_type, --phase flags, phase_id/
phase_ref JSON fields all still work as deprecated aliases.
Closes PHASE-16 (9 tasks).
* fix: update collection list page to use item_id from phasesProgress response
The TS client return type changed from phase_id to item_id but the
collection list page still referenced p.phase_id, causing svelte-check
type errors in CI.
* fix: address Codex review findings — PG migration, terminal statuses, metrics, CSRF resilience
- Add PostgreSQL migration 003 to rename 'phase' links to 'parent'
- Use schema-defined terminal statuses in GetAllItemProgress instead of hardcoded defaults
- Pass computed terminal statuses from page to ChildItems component
- Only special-case 'parent' field key when not defined in collection schema
- Move MetricsMiddleware before Recoverer so panics are counted
- Always mount ChildItems for SSE subscriptions even with 0 children
- Exclude soft-deleted children from has_children enrichment query
- Re-issue CSRF cookie when session is valid but cookie is missing
- Show actual API error messages in create-item toasts
|
||
|
|
edd259b1b0 |
feat: role board — cross-collection view, dashboard breakdown, agent bindings (#60)
* feat: role board — cross-collection view, dashboard breakdown, agent bindings (#PHASE-11)
Add a standalone role board page showing all work organized by agent
role across every collection. This is the "human orchestrator" view —
see at a glance what's queued for each capability and who's working it.
Agent bindings:
- Add `tools` text field to agent_roles table (migration 019)
- CLI: `pad role create "Implementer" --tools "Claude Code + Sonnet"`
- Lightweight notes about preferred tools — no per-user binding table
Dashboard role breakdown:
- `pad project dashboard` now includes `by_role` section
- Shows item count, assigned users, and tools per role
- CLI renders role summary table with icons
Role board API:
- `GET /workspaces/{ws}/roles/board` — items from all collections grouped by role
- Filters terminal-status items (done, cancelled, etc.)
- Supports `?assigned_user_id=X` for "my work" filtering
- Returns role info, items, and assigned user list per lane
Web UI:
- New page at /{workspace}/roles with horizontal lane layout
- Collection badges on cards (items span collections)
- "My Work" toggle to filter by current user
- Empty states for no roles and empty lanes
- Sidebar nav: 🎭 Roles link added
- Responsive: stacks vertically on mobile
Skill:
- References role board in greeting and "who's working on what" patterns
* feat: add assignment picker to item detail page
Replace read-only assignment display with interactive dropdowns for
assigning users and roles directly from the item detail page.
- User dropdown populated from workspace members
- Role dropdown populated from agent roles
- Either can be set or cleared independently
- Saves immediately on change via PATCH API
- Added assigned_user_id/agent_role_id/clear_* to ItemUpdate type
* feat: add role management UI to roles page
Add a "Manage" toggle in the role board header that reveals an inline
panel for creating, editing, and deleting roles directly from the UI.
- Role cards show icon, name, description, tools, and item count
- Edit inline: name, icon, description, tools
- Create new roles with a dashed card form
- Delete with confirmation dialog
- Board auto-refreshes after changes
* refactor: replace inline role management with dialog modal
The inline horizontal card grid was cramped and hard to use. Replace
with a proper <dialog> modal that opens from the ⚙ Manage button.
- Vertical list of role rows with icon, name, description, tools, item count
- Inline edit mode per row with labeled fields
- Create new role form at the bottom with clear field labels
- Click backdrop or ✕ to close, board refreshes on close
- Native dialog handles backdrop, escape key, and focus trapping
* fix: role board mobile layout matches collection kanban, unassigned first
- Unassigned lane now appears first (before role lanes)
- Mobile: horizontal swipe with scroll-snap at 75vw columns, matching
the collection BoardView pattern (no vertical stacking)
* feat: add drag-and-drop between role board lanes
Items can now be dragged between role lanes to reassign their role.
Uses svelte-dnd-action matching the collection BoardView pattern.
- Drag items between role lanes to change role assignment
- Drag to Unassigned lane to clear role
- Drop target highlight on hover
- Touch support with 500ms delay (same as collection board)
- Haptic feedback on mobile drag start
- Board refreshes after drop to sync server state
* fix: auto-assign user on drag to role lane, show unassigned in My Work
- When dragging an unassigned item into a role lane, automatically
assign the current user alongside the role
- "My Work" filter now shows items assigned to you OR items with no
user assignment, so unassigned work remains visible and claimable
* fix: three-state filter on role board — All, My Work, Unassigned
Replace the My Work toggle with a segmented button group offering
three filter modes:
- All: show everything (default)
- My Work: items explicitly assigned to the current user
- Unassigned: items with no user assignment
* fix: replace filter buttons with Highlight Mine toggle
Remove the three-state filter (All/My Work/Unassigned) and replace
with a single "Highlight Mine" toggle that dims cards not assigned
to the current user. All items remain visible and draggable — your
items just visually pop while others fade to 35% opacity (hovering
restores to 70%).
* fix: resolve undefined loadBoard and myWorkOnly in role board page
Replace 6 references to nonexistent `loadBoard()` with `loadData()`
(the actual data-loading function), and replace `myWorkOnly` with
`highlightMine` (the actual state variable). Fixes svelte-check errors
that caused CI Web Build to fail.
* fix: role breakdown pointer aliasing and terminal status filtering
P1: Copy role.ID to a local variable before taking its address in
GetRoleBreakdown, avoiding potential pointer aliasing from the range
variable (safe in Go 1.22+ but clearer with an explicit copy).
P2: Add terminal status exclusion to the GetRoleBreakdown SQL query
so dashboard counts match the board view. Previously, done/completed/
cancelled items were included in role counts, inflating active load.
Addresses Codex review comments on PR #60.
|
||
|
|
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. |
||
|
|
998716ae49 |
feat: unified item timeline with comment-on-update, threading, and reactions (#54)
* feat: unified item timeline with comment-on-update, threading, and reactions (IDEA-115) Replace the separate Comments section and Version History modal on the item detail page with a single chronological timeline that interleaves comments, activities, and content versions. Key changes: - Add --comment flag to `pad item update` so agents/users can explain status changes inline (creates a comment linked to the activity) - Add threaded replies (parent_id on comments) with inline reply UI - Add emoji reactions on comments (new comment_reactions table) - New /timeline API endpoint merges comments, activities, and versions server-side with dedup and collapsing of rapid edits - New Svelte timeline components: ItemTimeline, TimelineCommentCard, TimelineActivityCard, TimelineVersionCard, ReactionPicker - Remove Implementation Notes and Decision Log inputs from web UI - Update skill docs to encourage --comment on status changes * fix: address review findings for PR #54 (iteration 1) - Use ListItemVersions instead of ListVersions in timeline endpoint so item content history renders correctly - Add workspace validation to reply and reaction handlers to prevent cross-workspace comment mutation - Raise activity cap from 500 to 10000 to avoid silently truncating long timelines - Fix toggleReaction to always POST (idempotent) instead of incorrectly matching other users' reactions for DELETE - Await onReply promise before clearing draft to prevent duplicate submissions and lost drafts on failure - Register reaction_added/reaction_removed in SSE ITEM_EVENTS so reactions from other sessions appear in real-time Co-Authored-By: Claude <noreply@anthropic.com> * fix: address review findings for PR #54 (iteration 2) - Fix nil pointer dereference in timeline handler when item not found - Store empty string instead of NULL for reaction user_id so UNIQUE constraint works correctly in SQLite - Add workspace validation to handleDeleteComment (cross-workspace deletion was possible) - Fix SKILL.md duplicate numbering (4. appeared twice) - Remove || true debug artifacts from reaction conditionals - Replace SvelteMap with plain Map in non-reactive groupReactions - Use != null checks for timeline API params to handle offset=0 - Log warning on comment creation failure during item update Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
89db556e29 | feat(server): add info command for TASK-134 (#52) | ||
|
|
f5649b912e | refactor(cli): group first-release commands for TASK-127 (#45) | ||
|
|
a219f81633 |
fix: CLI and skill file now use issue IDs (TASK-5) instead of slugs (#15)
Agents were using verbose slugs because: 1. The skill file (SKILL.md) taught them to use `<slug>` in every example 2. CLI output showed slugs in parentheses rather than issue IDs 3. CLI usage strings said `<slug>` not `<ref>` 4. JSON output lacked a `ref` field, so agents parsing JSON only saw slugs Changes: - Add computed `ref` field to Item model (e.g. "TASK-5") in JSON output - CLI create/update/delete/edit output now prominently shows issue IDs - All CLI usage strings changed from `<slug>` to `<ref>` - Issue IDs displayed in bold cyan (not dim) in list/show/grouped views - Skill file rewritten to use issue IDs in all examples and instructions - Dashboard API includes `item_ref`/`ref` in attention, suggestions, phases - Search results now include item_number and collection_prefix for refs - CLAUDE.md updated to document issue ID usage |
||
|
|
4708e5bacf |
docs: Update README, SKILL.md, CLAUDE.md, and add GitHub templates
README: Complete rewrite with value proposition, comparison table, feature showcase, installation guide, architecture diagram, CLI reference. SKILL.md: Add standup, changelog, watch, dependencies, and webhooks to the agent skill so AI tools know about new capabilities. CLAUDE.md: Document webhooks package, new API endpoints, and all new CLI commands. GitHub templates: Bug report and feature request issue templates (YAML), pull request template with checklist. |
||
|
|
d318ecf7fc |
Add workspace onboarding: CLI hints, web checklist, codebase detection, and relation field fix
- Print suggested /pad prompts after `pad init` creates a new workspace - Add `pad onboard` command that detects project tooling (language, build system, test runner, CI, linter) and suggests matching conventions from the library - Replace empty workspace welcome box with OnboardingChecklist component showing a 4-step guided setup with progress bar and /pad prompt hints - Add contextual tips with /pad prompts to empty collection states - Add onboarding workflow to /pad skill for agent-driven codebase analysis - Fix relation fields storing slugs instead of UUIDs: server now resolves slugs/refs to UUIDs for relation-type fields on both create and update |
||
|
|
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 |