mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 06:16:06 -10:00
fix(chat): name tasks by identifier, not UUID, in tool rows and agent text (#19635)
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - New users meet Paperclip through the onboarding chat with their
first agent, and through task briefs that agents write
> - A browser walk of onboarding found internal handles and
control-plane jargon in those user-facing surfaces
> - A tool row showed `Read taskId: e3ecf9bc-…`, an agent-written brief
cited `task … (id 2b330993-…)`, and a Codex agent told a new user about
"receipts", "execution policy", and a "rejected policy patch"
> - Raw UUIDs and mechanism narration make the product look broken and
give the user nothing to act on
> - This pull request makes tool rows name a task by its identifier, and
tells agents to reference identifiers and to describe outcomes, not
mechanics
> - The benefit is that a new user sees "Read <task identifier>" and
plain progress updates
## Linked Issues or Issue Description
**What happened?**
In the task chat, a collapsed tool row rendered the raw call argument
`taskId: <uuid>`. An agent-written task description included a raw task
UUID next to the task identifier. A Codex chief of staff narrated
internal mechanics (receipts, execution policies, a rejected policy
patch) to a new user in chat.
**Expected behavior**
Tool rows name the task by its identifier. Agent-written text links
tasks and documents by identifier. Agents tell the user what happened,
what it means, and the next step.
**Steps to reproduce**
1. Run onboarding with the Codex API runner and accept a single-task
proposal.
2. Watch the handoff in the chief-of-staff chat.
3. See the task lookup tool row and the narration. With the Claude API
runner and the plan path, open a child task created from the plan and
read its description.
## What Changed
- `ui/src/components/task-chat/transcript-adapter.ts`: a tool row's
target never contains a UUID argument. A `taskId`/`issueId` UUID is kept
as `taskRefId` on the tool item.
- New `ui/src/components/task-chat/task-reference.ts`:
`useTaskReferenceLabel` reads the task from the shared issue detail
query cache and returns its identifier. It returns "task" until the task
resolves or when it cannot.
- `TaskChatRunnerActivityGroup` and `TaskChatToolCard` show that
identifier as the row subject. They load the task only for rows that
have a task reference.
- `skills/paperclip/SKILL.md` (Comment Style): two new required rules.
"No raw IDs in prose" and "Describe outcomes, not mechanics".
- First-task onboarding skill: a "Talk in outcomes" rule. Non-inline
handoff inputs use an identifier link, not a raw UUID.
- Chief-of-staff persona (`AGENTS.md` for the first agent): the same
plain-language rule, so it applies in every chat.
- `skills/paperclip-converting-plans-to-tasks/SKILL.md`: task
descriptions reference other tasks and documents by identifier link,
never by raw UUID.
- No change to `skills-releases/` snapshots. They are frozen releases.
## Verification
Red on master (fix files reverted, new tests kept):
```
ui tool-row-task-reference.test.tsx
FAIL ... > falls back to a plain word while the task cannot be resolved
AssertionError: expected 'ReadtaskId: e3ecf9bc-099a-4b10-8169-9…' not to contain 'e3ecf9bc-099a-4b10-8169-9ce490a91e3d'
Tests 3 failed (3)
server user-facing-wording-guidance.test.ts
Tests 6 failed (6)
```
Green with the fix:
```
ui tool-row-task-reference.test.tsx Tests 3 passed (3)
server user-facing-wording-guidance.test.ts Tests 6 passed (6)
```
Neighbouring suites:
- `cd ui && npx vitest run src/components/task-chat
src/components/TaskChatThread src/components/transcript`: 70 files, 1448
passed, 1 skipped.
- From the repo root: `onboarding-first-task-assets`,
`paperclip-skill-utils`, `onboarding-first-task-admission`,
`hiring-operational-examples`, `cli-invocation-safety`, and the new
guidance test all pass.
- `pnpm check:token-gates`: all gates clean. The change adds no styles.
- `ui` typecheck: no errors in the changed files.
- Not run locally: `agent-skills-routes.test.ts` hit a 30 s `beforeAll`
import timeout on a cold cache. CI covers it.
## Risks
- Low risk. A row that names a task loads that task once through the
shared issue detail cache. The thread usually has it cached already. A
failed load shows "task" and does not retry.
- Rows now show other non-UUID arguments after the identifier (for
example `<identifier> · status: done`). Rows without a task reference do
not change.
- The skill text changes the guidance that agents read. They do not
change permissions, tools, or API behavior.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, run in Claude Code
with tool use (shell, file edits, test execution).
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
6c7cc2c5d6
commit
7c673e695f
15 files changed
+357
-77
No files matched your search
@@ -67,14 +67,14 @@ The skill/reference inventory and eval cases are the only normative behavior sou
|
||||
| skill:skills/paperclip/SKILL.md:reading-granted-secrets:585 | optional_agent_tool | skills/paperclip/SKILL.md:585 |
|
||||
| skill:skills/paperclip/SKILL.md:critical-rules:611 | optional_agent_tool | skills/paperclip/SKILL.md:611 |
|
||||
| skill:skills/paperclip/SKILL.md:comment-style-required:632 | always_agent_tool | skills/paperclip/SKILL.md:632 |
|
||||
| skill:skills/paperclip/SKILL.md:update:664 | optional_agent_tool | skills/paperclip/SKILL.md:664 |
|
||||
| skill:skills/paperclip/SKILL.md:task-documents-and-deliverables:674 | always_agent_tool | skills/paperclip/SKILL.md:674 |
|
||||
| skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:688 | optional_agent_tool | skills/paperclip/SKILL.md:688 |
|
||||
| skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:719 | optional_agent_tool | skills/paperclip/SKILL.md:719 |
|
||||
| skill:skills/paperclip/SKILL.md:searching-issues:749 | optional_agent_tool | skills/paperclip/SKILL.md:749 |
|
||||
| skill:skills/paperclip/SKILL.md:full-reference:759 | optional_agent_tool | skills/paperclip/SKILL.md:759 |
|
||||
| skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:763 | always_agent_tool | skills/paperclip/SKILL.md:763 |
|
||||
| skill:skills/paperclip/SKILL.md:incidental-feedback:780 | optional_agent_tool | skills/paperclip/SKILL.md:780 |
|
||||
| skill:skills/paperclip/SKILL.md:update:668 | optional_agent_tool | skills/paperclip/SKILL.md:668 |
|
||||
| skill:skills/paperclip/SKILL.md:task-documents-and-deliverables:678 | always_agent_tool | skills/paperclip/SKILL.md:678 |
|
||||
| skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:692 | optional_agent_tool | skills/paperclip/SKILL.md:692 |
|
||||
| skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:723 | optional_agent_tool | skills/paperclip/SKILL.md:723 |
|
||||
| skill:skills/paperclip/SKILL.md:searching-issues:753 | optional_agent_tool | skills/paperclip/SKILL.md:753 |
|
||||
| skill:skills/paperclip/SKILL.md:full-reference:763 | optional_agent_tool | skills/paperclip/SKILL.md:763 |
|
||||
| skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:767 | always_agent_tool | skills/paperclip/SKILL.md:767 |
|
||||
| skill:skills/paperclip/SKILL.md:incidental-feedback:784 | optional_agent_tool | skills/paperclip/SKILL.md:784 |
|
||||
| skill:skills/paperclip/references/api-reference.md:paperclip-api-reference:1 | optional_agent_tool | skills/paperclip/references/api-reference.md:1 |
|
||||
| skill:skills/paperclip/references/api-reference.md:response-schemas:9 | optional_agent_tool | skills/paperclip/references/api-reference.md:9 |
|
||||
| skill:skills/paperclip/references/api-reference.md:agent-record-get-api-agents-me-or-get-api-agents-agentid:11 | optional_agent_tool | skills/paperclip/references/api-reference.md:11 |
|
||||
|
||||
@@ -236,72 +236,72 @@
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:664",
|
||||
"id": "skill:skills/paperclip/SKILL.md:668",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L664:update",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L668:update",
|
||||
"heading": "Update",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:674",
|
||||
"id": "skill:skills/paperclip/SKILL.md:678",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L674:task-documents-and-deliverables",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L678:task-documents-and-deliverables",
|
||||
"heading": "Task Documents and Deliverables",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
"semanticOperation": "write_document",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:688",
|
||||
"id": "skill:skills/paperclip/SKILL.md:692",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L688:planning-required-when-planning-requested",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L692:planning-required-when-planning-requested",
|
||||
"heading": "Planning (Required when planning requested)",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
"semanticOperation": "write_document",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:719",
|
||||
"id": "skill:skills/paperclip/SKILL.md:723",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L719:key-endpoints-hot-routes",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L723:key-endpoints-hot-routes",
|
||||
"heading": "Key Endpoints (Hot Routes)",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:749",
|
||||
"id": "skill:skills/paperclip/SKILL.md:753",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L749:searching-issues",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L753:searching-issues",
|
||||
"heading": "Searching Issues",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:759",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L759:full-reference",
|
||||
"heading": "Full Reference",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:763",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L763:conversational-confirmation-answers",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L763:full-reference",
|
||||
"heading": "Full Reference",
|
||||
"primaryDisposition": "control_plane_owned",
|
||||
"semanticOperation": "runtime_reconciliation",
|
||||
"expectedMockState": "runtime_decision_record"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:767",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L767:conversational-confirmation-answers",
|
||||
"heading": "Conversational confirmation answers",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
"semanticOperation": "call_api",
|
||||
"expectedMockState": "operation_result"
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:780",
|
||||
"id": "skill:skills/paperclip/SKILL.md:784",
|
||||
"kind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L780:incidental-feedback",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md#L784:incidental-feedback",
|
||||
"heading": "Incidental feedback",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
"semanticOperation": "scoped_discovery",
|
||||
|
||||
@@ -5,6 +5,6 @@ Generated by `scripts/generate-capability-contract.mjs`; do not edit generated f
|
||||
- Skill/reference headings: 167
|
||||
- Legacy MCP tools: 42
|
||||
- Eval cases: 106 across 16 groups
|
||||
- Deterministic content SHA-256: `bd38d281bc178b72c2d8646313c0254b5022b799029a256f77bb3f4b8c878519`
|
||||
- Deterministic content SHA-256: `c42941dfaafd16b83f448575de8f229b0c23221ffa947b0da02dbbba7faec102`
|
||||
|
||||
Every row has exactly one primary disposition, a source anchor, a semantic operation, and a mock-state expectation.
|
||||
@@ -405,9 +405,9 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:update:664",
|
||||
"id": "skill:skills/paperclip/SKILL.md:update:668",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:664",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:668",
|
||||
"title": "Update",
|
||||
"expectedSemantics": "Skill guidance headed “Update”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -416,13 +416,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:664"
|
||||
"skill:skills/paperclip/SKILL.md:668"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:task-documents-and-deliverables:674",
|
||||
"id": "skill:skills/paperclip/SKILL.md:task-documents-and-deliverables:678",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:674",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:678",
|
||||
"title": "Task Documents and Deliverables",
|
||||
"expectedSemantics": "Skill guidance headed “Task Documents and Deliverables”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
@@ -431,13 +431,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:674"
|
||||
"skill:skills/paperclip/SKILL.md:678"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:688",
|
||||
"id": "skill:skills/paperclip/SKILL.md:planning-required-when-planning-requested:692",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:688",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:692",
|
||||
"title": "Planning (Required when planning requested)",
|
||||
"expectedSemantics": "Skill guidance headed “Planning (Required when planning requested)”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -446,13 +446,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:688"
|
||||
"skill:skills/paperclip/SKILL.md:692"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:719",
|
||||
"id": "skill:skills/paperclip/SKILL.md:key-endpoints-hot-routes:723",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:719",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:723",
|
||||
"title": "Key Endpoints (Hot Routes)",
|
||||
"expectedSemantics": "Skill guidance headed “Key Endpoints (Hot Routes)”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -461,13 +461,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:719"
|
||||
"skill:skills/paperclip/SKILL.md:723"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:searching-issues:749",
|
||||
"id": "skill:skills/paperclip/SKILL.md:searching-issues:753",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:749",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:753",
|
||||
"title": "Searching Issues",
|
||||
"expectedSemantics": "Skill guidance headed “Searching Issues”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -476,13 +476,13 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:749"
|
||||
"skill:skills/paperclip/SKILL.md:753"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:full-reference:759",
|
||||
"id": "skill:skills/paperclip/SKILL.md:full-reference:763",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:759",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:763",
|
||||
"title": "Full Reference",
|
||||
"expectedSemantics": "Skill guidance headed “Full Reference”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -490,29 +490,29 @@
|
||||
"assertionClasses": [
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:759"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:763",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:763",
|
||||
"title": "Conversational confirmation answers",
|
||||
"expectedSemantics": "Skill guidance headed “Conversational confirmation answers”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
"requiredGrants": [],
|
||||
"assertionClasses": [
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:763"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:incidental-feedback:780",
|
||||
"id": "skill:skills/paperclip/SKILL.md:conversational-confirmation-answers:767",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:780",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:767",
|
||||
"title": "Conversational confirmation answers",
|
||||
"expectedSemantics": "Skill guidance headed “Conversational confirmation answers”.",
|
||||
"primaryDisposition": "always_agent_tool",
|
||||
"requiredGrants": [],
|
||||
"assertionClasses": [
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:767"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "skill:skills/paperclip/SKILL.md:incidental-feedback:784",
|
||||
"sourceKind": "skill_heading",
|
||||
"sourceAnchor": "skills/paperclip/SKILL.md:784",
|
||||
"title": "Incidental feedback",
|
||||
"expectedSemantics": "Skill guidance headed “Incidental feedback”.",
|
||||
"primaryDisposition": "optional_agent_tool",
|
||||
@@ -521,7 +521,7 @@
|
||||
"control_plane_invariant"
|
||||
],
|
||||
"evidenceIds": [
|
||||
"skill:skills/paperclip/SKILL.md:780"
|
||||
"skill:skills/paperclip/SKILL.md:784"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
/**
|
||||
* Agents write task briefs and chat replies that people read. The Nov 2026
|
||||
* onboarding walk found raw task UUIDs in an agent-written brief ("on task
|
||||
* BRA-2 (id 2b330993-…)") and a Codex chief of staff narrating receipts,
|
||||
* execution policies and policy patches to a new user. These checks keep the
|
||||
* instructions that shape that text explicit about both.
|
||||
*/
|
||||
const read = (relative: string) => readFile(new URL(relative, import.meta.url), "utf8");
|
||||
|
||||
function section(markdown: string, heading: string): string {
|
||||
const start = markdown.indexOf(heading);
|
||||
if (start < 0) throw new Error(`Missing section: ${heading}`);
|
||||
const next = markdown.indexOf("\n## ", start + heading.length);
|
||||
return markdown.slice(start, next < 0 ? undefined : next);
|
||||
}
|
||||
|
||||
describe("agent-written text names tasks by identifier, never by UUID", () => {
|
||||
it("the paperclip skill's comment style forbids raw UUIDs in comments and descriptions", async () => {
|
||||
const style = section(await read("../../../skills/paperclip/SKILL.md"), "## Comment Style (Required)");
|
||||
expect(style).toContain("**No raw IDs in prose (required):**");
|
||||
expect(style).toMatch(/never paste a raw UUID/i);
|
||||
expect(style).toContain("[BRA-2](/BRA/issues/BRA-2#document-plan)");
|
||||
});
|
||||
|
||||
it("the first-task handoff references inputs by identifier link, not UUID", async () => {
|
||||
const skill = await read("../onboarding-assets/first-task/skills/first-task/SKILL.md");
|
||||
const handoff = skill.slice(skill.indexOf("Copy required input payloads verbatim"));
|
||||
expect(handoff).toMatch(/source reference as an identifier link[^.]*not a raw UUID/i);
|
||||
});
|
||||
|
||||
it("plan-to-task conversion keeps UUIDs out of task descriptions", async () => {
|
||||
const skill = await read("../../../skills/paperclip-converting-plans-to-tasks/SKILL.md");
|
||||
expect(skill).toMatch(/identifier links[^.]*never raw UUIDs/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chat agents describe outcomes, not internal mechanics", () => {
|
||||
it("the first-task workflow forbids narrating control-plane mechanics to the user", async () => {
|
||||
const skill = await read("../onboarding-assets/first-task/skills/first-task/SKILL.md");
|
||||
expect(skill).toContain("**Talk in outcomes.**");
|
||||
for (const term of ["receipts", "execution policies", "patches", "runs"]) expect(skill).toContain(term);
|
||||
expect(skill).toMatch(/what happened, what it means for them, and the next step/);
|
||||
});
|
||||
|
||||
it("the chief-of-staff persona carries the same plain-language rule into every chat", async () => {
|
||||
const persona = await read("../onboarding-assets/first-task/chief-of-staff/AGENTS.md");
|
||||
expect(persona).toMatch(/plain language/i);
|
||||
expect(persona).toMatch(/never narrate internal mechanics/i);
|
||||
expect(persona).toMatch(/raw IDs/);
|
||||
});
|
||||
|
||||
it("the paperclip skill applies the rule to every user-facing comment", async () => {
|
||||
const style = section(await read("../../../skills/paperclip/SKILL.md"), "## Comment Style (Required)");
|
||||
expect(style).toContain("**Describe outcomes, not mechanics (required):**");
|
||||
});
|
||||
});
|
||||
@@ -1 +1,3 @@
|
||||
You are {{agentName}}, chief of staff for {{organizationName}}. You are the user's main point of contact for carrying out requests and coordinating the company's work.
|
||||
|
||||
Speak to the user in plain language: say what happened, what it means for them, and what comes next. Never narrate internal mechanics (receipts, execution policies, runs, API calls) or show raw IDs; name tasks by their identifier, like PIX-3.
|
||||
@@ -17,6 +17,8 @@ next steps directly. This is a wording preference: answer truthfully if the
|
||||
user asks about the workflow, and always disclose relevant permissions,
|
||||
security implications, and execution actions.
|
||||
|
||||
**Talk in outcomes.** The user is new to Paperclip. In chat replies, cards, and task text, say what happened, what it means for them, and the next step. Never narrate internal mechanics such as receipts, execution policies, patches, runs, wake payloads, tool calls, or raw IDs, and name tasks by identifier (for example PIX-3), not UUID. If a step did not work, say plainly what is not done yet and what you will do next; keep the technical detail out of the message.
|
||||
|
||||
This is the user's first task in Paperclip. Your job is to understand what they want and propose a path forward. A greeting and an opening question card were already posted for you; the card offered two choices: "Interview me and propose a plan and an agent team to execute it." (option `interview`) or "I have a task in mind" (option `task`, with a text field). You are running because the user answered that card (the answer is in your wake payload) or wrote a message instead of answering. Don't re-introduce yourself and don't post the opening card again.
|
||||
|
||||
Work in this order.
|
||||
@@ -97,7 +99,7 @@ Work in this order.
|
||||
- For a plan-only request, retain the accepted plan on this task. Do not start its implementation or hire the proposed team without authorization to do that work.
|
||||
- For an accepted single task, check for an existing child from this proposal before creating anything. Create exactly one child linked to this onboarding task and assign it to yourself. On later wakes, continue that same child instead of creating another.
|
||||
- If the accepted child work requires a file deliverable, include the approved scope, document requirements, and requested filenames in a self-contained child description. Delegate the work to the child's own Paperclip execution, even when the child is assigned to yourself. That execution must save the document and publish the requested file on the child before marking it done. Do not create or register that file from the onboarding run, or mark the child done before its execution delivers the file. Native `register_deliverable` is bound to its current task.
|
||||
- Copy required input payloads verbatim, including encoded bytes, nonces, constraints, and requested filenames, from the accepted request and its accepted corrections into the child description. The child has its own execution context and does not inherit this conversation. Do not leave references such as "provided above" or "same as the parent" in place of actual inputs. When an input is not inline, include its actual retrievable source reference and ensure the child is authorized to read it. Keep credentials and secrets out of handoff text.
|
||||
- Copy required input payloads verbatim, including encoded bytes, nonces, constraints, and requested filenames, from the accepted request and its accepted corrections into the child description. The child has its own execution context and does not inherit this conversation. Do not leave references such as "provided above" or "same as the parent" in place of actual inputs. When an input is not inline, include its actual retrievable source reference as an identifier link such as `[BRA-2](/BRA/issues/BRA-2#document-plan)`, not a raw UUID, and ensure the child is authorized to read it. Keep credentials and secrets out of handoff text.
|
||||
- Parent-child linkage is not a dependency. Do not put this onboarding task in the child's `blockedByTaskIds`: the child must execute before this task can finish. Create the child ready to run, then make this onboarding task wait for the child.
|
||||
- While waiting for that file-producing child, preserve existing blocker IDs and add the child as a dependency. Native runners use `set_dependencies`, then `paperclip_block` with the child agent as owner and child completion as the unblock action; legacy runners use the normal issue API with `blockedByIssueIds` and a blocked disposition. End the turn so the child can execute. Do not poll for its result while holding the onboarding run.
|
||||
- When child completion resumes the onboarding task, verify the saved child document and retrievable file, then reply with the actual child task and download links. Reuse the recorded result instead of producing another file on the parent or creating another child.
|
||||
|
||||
@@ -14,6 +14,7 @@ Create a separate task only for a useful boundary: another owner or permission,
|
||||
independent parallel output, a real prerequisite, independent review, or substantial
|
||||
follow-up that needs its own lifecycle. Check the available agents before assigning.
|
||||
Give each delegate enough context and a clear deliverable to work independently.
|
||||
People read these descriptions: reference other tasks and documents with identifier links (for example `[BRA-2](/BRA/issues/BRA-2#document-plan)`), never raw UUIDs.
|
||||
|
||||
Set real dependencies through `blockedByIssueIds` (or the native dependency tool).
|
||||
Parent/child nesting and prose do not delay execution. Independent work can start
|
||||
|
||||
@@ -644,6 +644,10 @@ When posting issue comments or writing issue descriptions, use concise markdown
|
||||
|
||||
Never leave bare ticket ids in issue descriptions or comments when a clickable internal link can be provided.
|
||||
|
||||
**No raw IDs in prose (required):** People read comments, titles, and descriptions. Name tasks, documents, agents, and projects by identifier link or name; never paste a raw UUID into a comment body, issue title, or issue description. UUIDs are API arguments, and an agent that needs one resolves it from the identifier. Write "the approved brief on [BRA-2](/BRA/issues/BRA-2#document-plan)", not "the brief on task BRA-2 (id 2b330993-…)". Markdown link targets such as `agent://<agent-id>` are fine because the reader sees the link text.
|
||||
|
||||
**Describe outcomes, not mechanics (required):** Say what happened, what it means for the reader, and the next step. Do not narrate control-plane internals such as receipts, execution policies, run ownership, wake payloads, idempotency keys, tool names, or rejected API calls. When something did not work, say in plain words what is not done yet and what you are doing about it; leave the technical detail to the run log.
|
||||
|
||||
**Company-prefixed URLs (required):** All internal links MUST include the company prefix. Derive the prefix from any issue identifier you have (e.g., `PAP-315` → prefix is `PAP`). Use this prefix in all UI links:
|
||||
|
||||
- Issues: `/<prefix>/issues/<issue-identifier>` (e.g., `/PAP/issues/PAP-224`)
|
||||
|
||||
@@ -34,6 +34,7 @@ import { latestReasoningHeadline } from "./reasoning-summary";
|
||||
import { toolActivityPresentation, type ToolIcon } from "./tool-taxonomy";
|
||||
import { useTaskChatShimmer } from "./TaskChatShimmer";
|
||||
import { TaskChatDiffCounts, TaskChatDiffView } from "./TaskChatDiffView";
|
||||
import { useTaskReferenceLabel, withTaskReference } from "./task-reference";
|
||||
|
||||
type Activity = TaskChatActivityPhaseItem["items"][number];
|
||||
type ToolActivity = Extract<Activity, { kind: "tool" }>;
|
||||
@@ -105,7 +106,7 @@ function providerTrailing(item: Extract<Activity, { kind: "protocol" }>): Pick<A
|
||||
return {};
|
||||
}
|
||||
|
||||
function presentation(item: Activity, active: boolean): ActivityRowPresentation | null {
|
||||
function presentation(item: Activity, active: boolean, taskLabel?: string): ActivityRowPresentation | null {
|
||||
if (item.kind === "tool") {
|
||||
const tool = toolPresentation(item);
|
||||
const running =
|
||||
@@ -119,7 +120,7 @@ function presentation(item: Activity, active: boolean): ActivityRowPresentation
|
||||
? tool.runningLabel
|
||||
: tool.completedLabel;
|
||||
const terminal = tool.family === "terminal";
|
||||
const subject = item.target?.trim();
|
||||
const subject = withTaskReference(item.target?.trim() || undefined, taskLabel);
|
||||
// With a concrete subject, the verb alone reads better: "Read README.md".
|
||||
// A finished command needs no verb at all: the icon and "$" say it ran.
|
||||
// A failure or stop keeps its marker ("Read · failed") unless an exit
|
||||
@@ -219,7 +220,36 @@ function ActivityContent({
|
||||
item: Activity;
|
||||
active: boolean;
|
||||
}) {
|
||||
const row = presentation(item, active);
|
||||
if (item.kind === "tool" && item.taskRefId) {
|
||||
return <TaskReferenceActivityContent item={item} taskRefId={item.taskRefId} active={active} />;
|
||||
}
|
||||
return <ActivityRowContent item={item} active={active} />;
|
||||
}
|
||||
|
||||
/** A tool row about one task names it by identifier, resolved from its UUID. */
|
||||
function TaskReferenceActivityContent({
|
||||
item,
|
||||
taskRefId,
|
||||
active,
|
||||
}: {
|
||||
item: Activity;
|
||||
taskRefId: string;
|
||||
active: boolean;
|
||||
}) {
|
||||
const taskLabel = useTaskReferenceLabel(taskRefId);
|
||||
return <ActivityRowContent item={item} active={active} taskLabel={taskLabel} />;
|
||||
}
|
||||
|
||||
function ActivityRowContent({
|
||||
item,
|
||||
active,
|
||||
taskLabel,
|
||||
}: {
|
||||
item: Activity;
|
||||
active: boolean;
|
||||
taskLabel?: string;
|
||||
}) {
|
||||
const row = presentation(item, active, taskLabel);
|
||||
const shimmer = useTaskChatShimmer(item.id);
|
||||
if (!row) return null;
|
||||
const Icon = row.icon;
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { TaskChatToolItem } from "./task-chat-model";
|
||||
import { toolTaxonomy } from "./tool-taxonomy";
|
||||
import { useTaskChatShimmer } from "./TaskChatShimmer";
|
||||
import { TaskChatDiffCounts, TaskChatDiffView } from "./TaskChatDiffView";
|
||||
import { useTaskReferenceLabel, withTaskReference } from "./task-reference";
|
||||
|
||||
const STATUS_ICON = {
|
||||
pending: { Icon: Loader2, spin: false, tone: "text-muted-foreground" },
|
||||
@@ -25,6 +26,20 @@ const STATUS_ICON = {
|
||||
interrupted: { Icon: X, spin: false, tone: "text-muted-foreground" },
|
||||
} as const;
|
||||
|
||||
function ToolTarget({ text }: { text: string }) {
|
||||
return (
|
||||
<span className="task-chat-collapsed-line-fade min-w-0 flex-1 font-mono text-(length:--text-micro) leading-4">
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** Names the task a call is about by identifier; its UUID is never shown. */
|
||||
function TaskReferenceToolTarget({ taskRefId, target }: { taskRefId: string; target?: string }) {
|
||||
const taskLabel = useTaskReferenceLabel(taskRefId);
|
||||
return <ToolTarget text={withTaskReference(target, taskLabel)!} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool invocation as a flat activity row (v7): [glyph] [name] [mono target] …
|
||||
* [hover chevron] [status]. No card chrome — activity is metadata, not
|
||||
@@ -61,10 +76,10 @@ export function TaskChatToolCard({ item }: { item: TaskChatToolItem }) {
|
||||
</span>
|
||||
<span data-conversation-tool-running={item.status === "in_progress" && shimmer ? "true" : undefined} className="flex min-w-0 flex-1 items-baseline gap-2 overflow-hidden">
|
||||
<span className="shrink-0 font-medium leading-4">{item.name}</span>
|
||||
{item.target ? (
|
||||
<span className="task-chat-collapsed-line-fade min-w-0 flex-1 font-mono text-(length:--text-micro) leading-4">
|
||||
{item.target}
|
||||
</span>
|
||||
{item.taskRefId ? (
|
||||
<TaskReferenceToolTarget taskRefId={item.taskRefId} target={item.target} />
|
||||
) : item.target ? (
|
||||
<ToolTarget text={item.target} />
|
||||
) : null}
|
||||
</span>
|
||||
<span className="ml-auto flex shrink-0 items-center gap-2">
|
||||
|
||||
@@ -232,6 +232,11 @@ export interface TaskChatToolItem {
|
||||
* next to the tool name — the v7 toolrow "target".
|
||||
*/
|
||||
target?: string;
|
||||
/**
|
||||
* Task the call is about, when its input named one by UUID. Rows show the
|
||||
* task's identifier for it; the UUID itself is never rendered.
|
||||
*/
|
||||
taskRefId?: string;
|
||||
detail?: string;
|
||||
diff?: TaskChatDiff;
|
||||
/** Resolved permission decision badge, when one applied. */
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { Issue } from "@paperclipai/shared";
|
||||
import { getIssueCoreQueryOptions } from "@/lib/issueDetailCache";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
|
||||
/**
|
||||
* The identifier ("PIX-3") of a task a tool call named by UUID. A task the
|
||||
* thread already loaded costs no request; otherwise only the core issue view
|
||||
* is read, under the cache key other surfaces share. Until it resolves, or
|
||||
* when it cannot, the row says "task" — never the id.
|
||||
*/
|
||||
export function useTaskReferenceLabel(taskId: string): string {
|
||||
const queryClient = useQueryClient();
|
||||
const cached = queryClient
|
||||
.getQueryData<Pick<Issue, "identifier">>(queryKeys.issues.detail(taskId))
|
||||
?.identifier?.trim();
|
||||
const { data } = useQuery({
|
||||
...getIssueCoreQueryOptions(queryClient, taskId),
|
||||
enabled: !cached,
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
return cached || data?.identifier?.trim() || "task";
|
||||
}
|
||||
|
||||
/** A tool row's subject: the referenced task first, then any other argument. */
|
||||
export function withTaskReference(target: string | undefined, taskLabel: string | undefined): string | undefined {
|
||||
if (!taskLabel) return target;
|
||||
return target ? `${taskLabel} · ${target}` : taskLabel;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// @vitest-environment jsdom
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { TranscriptEntry } from "@/adapters";
|
||||
import { queryKeys } from "../../lib/queryKeys";
|
||||
import { TaskChatRunnerActivityGroup } from "./TaskChatRunnerActivityGroup";
|
||||
import { TaskChatToolCard } from "./TaskChatToolCard";
|
||||
import { summarizeToolInput, transcriptToTaskChatItems } from "./transcript-adapter";
|
||||
import type { TaskChatActivityPhaseItem, TaskChatToolItem } from "./task-chat-model";
|
||||
|
||||
const issuesApi = vi.hoisted(() => ({ get: vi.fn(), getCore: vi.fn() }));
|
||||
vi.mock("../../api/issues", () => ({ issuesApi }));
|
||||
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const TASK_UUID = "e3ecf9bc-099a-4b10-8169-9ce490a91e3d";
|
||||
const TS = "2026-11-06T15:41:52.000Z";
|
||||
|
||||
/** The Codex walk's collapsed row: a task lookup whose only argument is the task UUID. */
|
||||
function taskLookupRow(): TaskChatToolItem {
|
||||
const entries: TranscriptEntry[] = [
|
||||
{ kind: "tool_call", ts: TS, toolUseId: "call-1", name: "Read", input: { taskId: TASK_UUID } },
|
||||
{ kind: "tool_result", ts: TS, toolUseId: "call-1", content: "{}", isError: false },
|
||||
];
|
||||
const items = transcriptToTaskChatItems(entries, { runId: "run-1", agentName: "Head baker", running: false });
|
||||
const tool = items.find((item): item is TaskChatToolItem => item.kind === "tool");
|
||||
if (!tool) throw new Error("expected a tool row");
|
||||
return tool;
|
||||
}
|
||||
|
||||
const phase = (items: TaskChatToolItem[]): TaskChatActivityPhaseItem => ({
|
||||
id: "phase", kind: "activity_phase", items, active: false, summary: "",
|
||||
});
|
||||
|
||||
describe("tool rows never show raw task UUIDs", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
let queryClient: QueryClient;
|
||||
beforeEach(() => {
|
||||
issuesApi.get.mockReset();
|
||||
issuesApi.getCore.mockReset();
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
});
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
function renderRow(tool: TaskChatToolItem) {
|
||||
act(() => root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TaskChatRunnerActivityGroup defaultExpanded item={phase([tool])} />
|
||||
</QueryClientProvider>,
|
||||
));
|
||||
return container.querySelector(`[data-activity-row="${tool.id}"]`);
|
||||
}
|
||||
|
||||
it("keeps a UUID argument out of the summarized target", () => {
|
||||
expect(summarizeToolInput({ taskId: TASK_UUID }) ?? "").not.toContain(TASK_UUID);
|
||||
expect(summarizeToolInput({ issueId: TASK_UUID, includeComments: true }) ?? "").not.toContain(TASK_UUID);
|
||||
});
|
||||
|
||||
it("shows the task identifier in place of its UUID", () => {
|
||||
queryClient.setQueryData(queryKeys.issues.detail(TASK_UUID), {
|
||||
id: TASK_UUID, identifier: "PIX-3", title: "Create printable cake menu and logo",
|
||||
});
|
||||
const row = renderRow(taskLookupRow());
|
||||
expect(row?.textContent).not.toContain(TASK_UUID);
|
||||
expect(row?.textContent).toContain("PIX-3");
|
||||
});
|
||||
|
||||
it("reads only the core task view for an uncached task, on both tool-row surfaces", async () => {
|
||||
issuesApi.getCore.mockResolvedValue({ id: TASK_UUID, identifier: "PIX-3", title: "Create printable cake menu and logo" });
|
||||
const tool = taskLookupRow();
|
||||
act(() => root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TaskChatToolCard item={tool} />
|
||||
</QueryClientProvider>,
|
||||
));
|
||||
const card = container.querySelector('[data-testid="task-chat-tool-card"]');
|
||||
await vi.waitFor(async () => {
|
||||
await act(async () => { await Promise.resolve(); });
|
||||
expect(card?.textContent).toContain("PIX-3");
|
||||
});
|
||||
expect(card?.textContent).not.toContain(TASK_UUID);
|
||||
expect(issuesApi.getCore).toHaveBeenCalledTimes(1);
|
||||
expect(issuesApi.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to a plain word while the task cannot be resolved", async () => {
|
||||
issuesApi.getCore.mockRejectedValue(new Error("not found"));
|
||||
const row = renderRow(taskLookupRow());
|
||||
await vi.waitFor(async () => {
|
||||
await act(async () => { await Promise.resolve(); });
|
||||
expect(issuesApi.getCore).toHaveBeenCalled();
|
||||
});
|
||||
expect(row?.textContent).not.toContain(TASK_UUID);
|
||||
expect(row?.textContent).toContain("task");
|
||||
});
|
||||
});
|
||||
@@ -183,6 +183,30 @@ const TARGET_KEYS = [
|
||||
|
||||
const TARGET_MAX = 96;
|
||||
|
||||
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
/** Argument keys that name one Paperclip task by its id. */
|
||||
const TASK_REF_KEYS = ["taskId", "task_id", "issueId", "issue_id"] as const;
|
||||
|
||||
/** A raw id is an internal handle, never something a person reads. */
|
||||
function isOpaqueId(value: unknown): boolean {
|
||||
return typeof value === "string" && UUID_RE.test(value.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* The task a tool call is about, when its input names one by UUID. The row
|
||||
* shows that task's identifier (resolved at render time) instead of the id.
|
||||
*/
|
||||
export function toolInputTaskRef(input: unknown): string | undefined {
|
||||
if (input == null || typeof input !== "object" || Array.isArray(input)) return undefined;
|
||||
const record = input as Record<string, unknown>;
|
||||
for (const key of TASK_REF_KEYS) {
|
||||
const value = record[key];
|
||||
if (isOpaqueId(value)) return (value as string).trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function clip(text: string, max: number): string {
|
||||
const oneLine = text.replace(/\s+/g, " ").trim();
|
||||
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
|
||||
@@ -196,21 +220,23 @@ function clip(text: string, max: number): string {
|
||||
export function summarizeToolInput(input: unknown): string | undefined {
|
||||
if (input == null) return undefined;
|
||||
if (typeof input === "string")
|
||||
return input.trim() ? clip(input, TARGET_MAX) : undefined;
|
||||
return input.trim() && !isOpaqueId(input) ? clip(input, TARGET_MAX) : undefined;
|
||||
if (typeof input !== "object") return clip(String(input), TARGET_MAX);
|
||||
const record = input as Record<string, unknown>;
|
||||
for (const key of TARGET_KEYS) {
|
||||
const value = record[key];
|
||||
if (typeof value === "string" && value.trim())
|
||||
if (typeof value === "string" && value.trim() && !isOpaqueId(value))
|
||||
return clip(value, TARGET_MAX);
|
||||
}
|
||||
// The acpx log parser synthesizes { text, status } onto tool inputs from the
|
||||
// event's summary line — presentation noise, not call parameters.
|
||||
// event's summary line — presentation noise, not call parameters. Raw ids
|
||||
// (task, agent, run UUIDs) are handles for the API, not for people.
|
||||
const entries = Object.entries(record).filter(
|
||||
([k, v]) =>
|
||||
(typeof v === "string" ||
|
||||
typeof v === "number" ||
|
||||
typeof v === "boolean") &&
|
||||
!isOpaqueId(v) &&
|
||||
k !== "text" &&
|
||||
k !== "status",
|
||||
);
|
||||
@@ -890,6 +916,7 @@ export function transcriptToTaskChatItems(
|
||||
name: toolDisplayName(entry.name),
|
||||
rawName: entry.name ?? undefined,
|
||||
target: summarizeToolInput(entry.input),
|
||||
...(toolInputTaskRef(entry.input) ? { taskRefId: toolInputTaskRef(entry.input) } : {}),
|
||||
status: "in_progress",
|
||||
...(entry.parentToolUseId ? { parentToolUseId: entry.parentToolUseId } : {}),
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user