diff --git a/README.md b/README.md index 2a3affe7..5cf25d59 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,40 @@ pad item create convention "Run tests before completing tasks" \ --field priority=must ``` -Agents load relevant conventions automatically. All agent actions are attributed in the activity feed, so you always know what the AI changed. +Agents load relevant conventions automatically, and every agent action is attributed in the activity feed — so you can see what the AI changed rather than finding it later in a diff. + +**Name your agents:** + +An agent that identifies itself gets its name shown on its writes — in the activity feed's Live and Audit views, on the dashboard's recent activity, on item timeline *activity* entries, and in the admin console's audit log and per-user activity views. With more than one agent working a project, that is the difference between "something automated touched this" and knowing which one. + +Pad takes the first of these it finds: + +```bash +# 1. Per-workspace, committed with the project — the deliberate choice. +# In .pad.toml: +# agent_name = "reviewer" + +# 2. Per-process, runtime-agnostic. Any harness can set it. +export PAD_AGENT=reviewer + +# 3. Otherwise Pad detects the runtimes it knows — Claude Code reports +# "claude-code" — and that detected id is used as the name. +``` + +**If none of the three produce a name, the write is not marked as an agent's at all** — it is recorded as the person whose credentials it used, which is the case the caveat below is about. The generic `agent` label you may see on older entries is a write that identified itself before Pad stored names, or an event type that records the actor without the name (workspace membership changes, sign-ins). + +The name is rendered exactly as sent — Pad keeps no list of approved names, and does not re-case or rewrite what you choose. + +Not every entry can show it. Comments, version snapshots, and implementation-note/decision entries record only *that* an agent acted, because the name is not stored on those rows — they still read `Agent`. Activity entries are the ones that carry it. + +**What this does not claim.** The name is supplied by the client and self-declared, so it records honesty, not identity. From `ResolveAgentName`'s own contract in `internal/cli/agent_identity.go`: + +> - an agent that omits it is indistinguishable from the human whose credentials it is using; +> - a human running `! pad ...` inside an agent's terminal inherits that terminal's environment and will be attributed to the agent. + +So it is not a basis for machine-verifiable provenance: treat it as a label an actor chose, useful for reading a trail, not as evidence about who acted. Because the credentials belong to a person either way, surfaces that exist for provenance show both — the admin audit log renders `reviewer (via Dana)` rather than picking one. + +Since the name is chosen by whoever is writing, it is displayed as an isolated unit: it is shown as sent, but it cannot re-order or restyle the text around it, and the account half of `name (via account)` is rendered separately so a chosen name cannot forge it. **Onboard agents to a new codebase:** diff --git a/plugin/skills/pad/SKILL.md b/plugin/skills/pad/SKILL.md index 9a64718d..7d850112 100644 --- a/plugin/skills/pad/SKILL.md +++ b/plugin/skills/pad/SKILL.md @@ -315,7 +315,7 @@ See the **Onboarding** entry under Natural Language Routing above — it branche 5. **Be conversational.** You're not a command executor. You're a project partner. 6. **Reference existing items.** Use `[[Item Title]]` links in content to connect items. 7. **Keep it practical.** Size each item so it's a single meaningful unit of work — what "meaningful" means depends on the workspace (one branch/PR for code, one interview round for hiring, one research question for research). Ideas should be actionable. Docs should be concise. Check the workspace's conventions for domain-specific sizing rules. -8. **Attribution matters.** Items and comments you create are stamped `created_by: agent` and `source: cli` automatically — but the agent half only works if the CLI can tell it is being run by an agent. It detects Claude Code on its own; under any other harness, set `PAD_AGENT=` in the environment (or `agent_name` in `.pad.toml`) or your writes will be recorded as the human whose credentials you are using. Note this is self-declared, not proof: it makes the trail honest, it does not make it verifiable, so never treat `created_by` on a comment as evidence that a human said something. +8. **Attribution matters.** Items and comments you create are stamped `created_by: agent` and `source: cli` automatically — but the agent half only works if the CLI can tell it is being run by an agent. It detects Claude Code on its own; under any other harness, set `PAD_AGENT=` in the environment (or `agent_name` in `.pad.toml`) or your writes will be recorded as the human whose credentials you are using. Whatever you send is DISPLAYED verbatim on the surfaces that store it — the activity feed, the dashboard's recent activity, activity entries on an item's timeline, the admin console's audit and per-user activity views — so a specific name (`reviewer`, `nightly-triage`) is more use to a reader than a generic client id. Comments, versions and note/decision entries record only that an agent acted, not which one. Note this is self-declared, not proof: it makes the trail honest, it does not make it verifiable, so never treat `created_by` on a comment as evidence that a human said something. 9. **Follow project conventions.** Always load and follow active conventions before performing work. They are project-specific rules that override your defaults. When a role is active, load both role-specific and global conventions. 10. **Learn and teach.** When the user corrects your behavior or teaches you a project-specific rule, offer to save it as a convention: "Should I save this as a project convention so future agents follow it too?" Use `pad item create convention "Title" --field trigger= --field scope= --field priority=should --stdin` with an appropriate trigger inferred from the context. If the correction is role-specific, add `--field role=`. 11. **Role context is per-conversation.** If roles exist, ask which role the user is working as on first invocation. Remember it for the session. Auto-filter queries and suggest assignments accordingly. Never block on role — if the user says "no role" or the workspace has no roles, work normally. diff --git a/skills/pad/SKILL.md b/skills/pad/SKILL.md index ed4e1f5e..3ad749e0 100644 --- a/skills/pad/SKILL.md +++ b/skills/pad/SKILL.md @@ -325,7 +325,7 @@ Run the **onboard** invokable playbook — see the **Onboarding** entry under Na 5. **Be conversational.** You're not a command executor. You're a project partner. 6. **Reference existing items.** Use `[[Item Title]]` links in content to connect items. 7. **Keep it practical.** Size each item so it's a single meaningful unit of work — what "meaningful" means depends on the workspace (one branch/PR for code, one interview round for hiring, one research question for research). Ideas should be actionable. Docs should be concise. Check the workspace's conventions for domain-specific sizing rules. -8. **Attribution matters.** Items and comments you create are stamped `created_by: agent` and `source: cli` automatically — but the agent half only works if the CLI can tell it is being run by an agent. It detects Claude Code on its own; under any other harness, set `PAD_AGENT=` in the environment (or `agent_name` in `.pad.toml`) or your writes will be recorded as the human whose credentials you are using. Note this is self-declared, not proof: it makes the trail honest, it does not make it verifiable, so never treat `created_by` on a comment as evidence that a human said something. +8. **Attribution matters.** Items and comments you create are stamped `created_by: agent` and `source: cli` automatically — but the agent half only works if the CLI can tell it is being run by an agent. It detects Claude Code on its own; under any other harness, set `PAD_AGENT=` in the environment (or `agent_name` in `.pad.toml`) or your writes will be recorded as the human whose credentials you are using. Whatever you send is DISPLAYED verbatim on the surfaces that store it — the activity feed, the dashboard's recent activity, activity entries on an item's timeline, the admin console's audit and per-user activity views — so a specific name (`reviewer`, `nightly-triage`) is more use to a reader than a generic client id. Comments, versions and note/decision entries record only that an agent acted, not which one. Note this is self-declared, not proof: it makes the trail honest, it does not make it verifiable, so never treat `created_by` on a comment as evidence that a human said something. 9. **Follow project conventions.** Always load and follow active conventions before performing work. They are project-specific rules that override your defaults. When a role is active, load both role-specific and global conventions. 10. **Learn and teach.** When the user corrects your behavior or teaches you a project-specific rule, offer to save it as a convention: "Should I save this as a project convention so future agents follow it too?" Use `pad item create convention "Title" --field trigger= --field scope= --field priority=should --stdin` with an appropriate trigger inferred from the context. If the correction is role-specific, add `--field role=`. 11. **Role context is per-conversation.** If roles exist, ask which role the user is working as on first invocation. Remember it for the session. Auto-filter queries and suggest assignments accordingly. Never block on role — if the user says "no role" or the workspace has no roles, work normally. diff --git a/web/src/lib/components/activity/EpisodeFeed.svelte b/web/src/lib/components/activity/EpisodeFeed.svelte index 45b333d3..76fd3f9e 100644 --- a/web/src/lib/components/activity/EpisodeFeed.svelte +++ b/web/src/lib/components/activity/EpisodeFeed.svelte @@ -68,7 +68,7 @@ {#snippet card(ep: Episode, live: boolean)}
- {ep.actorLabel} + {ep.actorLabel} {episodeVerb(ep.actions)} {#if ep.itemRef} {#if ep.itemSlug && ep.collectionSlug} @@ -199,6 +199,15 @@ font-weight: 700; color: var(--text-primary); white-space: nowrap; + /* An actor label is arbitrary text for agents (whatever went in + X-Pad-Agent) as well as for people. `nowrap` without a bound lets + one long name push the rest of the card's line out; the full value + stays in the title attribute. 24ch matches the activity page's + badge, where the number's reasoning and its limits are written + out. */ + max-width: 24ch; + overflow: hidden; + text-overflow: ellipsis; } .ep-verb { font-size: 12.5px; diff --git a/web/src/lib/components/activity/EpisodeFeed.svelte.test.ts b/web/src/lib/components/activity/EpisodeFeed.svelte.test.ts index a6ad6014..2c4959c8 100644 --- a/web/src/lib/components/activity/EpisodeFeed.svelte.test.ts +++ b/web/src/lib/components/activity/EpisodeFeed.svelte.test.ts @@ -154,4 +154,57 @@ describe('EpisodeFeed', () => { expect(checkpoint!.textContent).toContain('latest: Checkpoint: wiring the fold'); expect(checkpoint!.textContent).not.toContain('second line stays hidden'); }); + + // TASK-2759. foldEpisodes computes the label; this asserts the FEED renders + // it (CONVE-19 — a correct fold the card never reads would pass every test + // in activityEpisodes.test.ts). + // + // Both events are on the SAME item, deliberately: that is what makes the + // card count discriminating. Two different items would produce two cards + // however the actors were keyed, so the earlier version of this test proved + // only that labels render (codex round 10). Same item, same window, two + // names — a fold that ignored the name would yield ONE card. + it('renders each agent under its own stamped name', async () => { + mountFeed([ + act(1, { metadata: '{"agent":"wren"}' }), + act(2, { metadata: '{"agent":"rook"}' }), + ]); + await settle(); + + expect(host.querySelectorAll('.episode-card')).toHaveLength(2); + const labels = [...host.querySelectorAll('.ep-actor')].map((el) => el.textContent); + expect(labels.sort()).toEqual(['rook', 'wren']); + }); + + // Named agents alone do not discriminate: reinstating the retired + // GENERIC_AGENT_IDS filter left this file green until this case existed, + // because nothing here used a value the filter would have swallowed. + it('renders a generic-looking client id verbatim', async () => { + mountFeed([act(1, { metadata: '{"agent":"claude-code"}' })]); + await settle(); + + expect(host.querySelector('.ep-actor')!.textContent).toBe('claude-code'); + }); + + // Codex round 11. The label element must stay a : an agent name is + // self-declared text and a bidi control inside it would otherwise reorder + // the verb and item title that follow it on the card's line. Swapping the + // element back to a passes every text assertion above. + it('isolates the actor label so a bidi control cannot reorder the card', async () => { + mountFeed([act(1, { metadata: '{"agent":"wren\u202egnimalb"}' })]); + await settle(); + + const el = host.querySelector('.ep-actor')!; + expect(el.tagName).toBe('BDI'); + expect(el.textContent).toBe('wren\u202egnimalb'); + }); + + it('renders the generic label for an agent that stamped no name', async () => { + // `act`'s default metadata is '{}' — the pre-BUG-2542 shape, and the + // shape any agent that never sends the header still produces. + mountFeed([act(1)]); + await settle(); + + expect(host.querySelector('.ep-actor')!.textContent).toBe('agent'); + }); }); diff --git a/web/src/lib/components/admin/UserActivityTab.svelte b/web/src/lib/components/admin/UserActivityTab.svelte index 32b87a0a..0ece74c9 100644 --- a/web/src/lib/components/admin/UserActivityTab.svelte +++ b/web/src/lib/components/admin/UserActivityTab.svelte @@ -13,6 +13,7 @@ -->