mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 02:40:39 +09:00
Add planning mode for issue work (#5353)
## Thinking Path > - Paperclip is a control plane for autonomous AI companies. > - Issues are the core unit of work, and issue comments are how board users and agents coordinate execution. > - Some issue conversations need to produce plans and approvals instead of immediate implementation work. > - The existing issue contract did not distinguish standard execution comments from planning-oriented issue work. > - This pull request adds an issue work-mode contract and board UI affordances for standard vs planning mode. > - The benefit is that planning-mode issues can be created, displayed, discussed, and carried through agent heartbeat context without losing the normal issue workflow. ## What Changed - Added `standard` / `planning` issue work-mode contracts across DB, shared validators/types, server issue flows, plugin protocol, and adapter heartbeat payloads. - Added an idempotent `0081_optimal_dormammu` migration for `issues.work_mode`, ordered after current `public-gh/master` migrations. - Updated heartbeat/context summaries and issue-thread interaction behavior so planning work mode is preserved when creating suggested follow-up issues. - Added UI support for planning-mode issue creation, issue rows, detail composer styling, and composer work-mode toggles. - Added focused server/shared/UI tests plus a Playwright visual verification spec for planning-mode surfaces. - Rebased the branch onto current `public-gh/master` and added durable planning-mode screenshots under `doc/assets/pap-3368/`. ## Verification - `pnpm --filter @paperclipai/db run check:migrations` - `pnpm exec vitest run --project @paperclipai/shared packages/shared/src/validators/issue.test.ts` - `pnpm exec vitest run --project @paperclipai/server server/src/__tests__/heartbeat-context-summary.test.ts server/src/__tests__/issue-thread-interactions-service.test.ts server/src/__tests__/issues-goal-context-routes.test.ts --pool=forks --poolOptions.forks.isolate=true` - `pnpm exec vitest run --project @paperclipai/ui ui/src/components/IssueChatThread.test.tsx ui/src/components/NewIssueDialog.test.tsx ui/src/components/IssueRow.test.tsx ui/src/pages/IssueDetail.test.tsx` - `pnpm exec vitest run --project @paperclipai/adapter-utils packages/adapter-utils/src/server-utils.test.ts` - `PAPERCLIP_E2E_SKIP_LLM=true npx playwright test --config tests/e2e/playwright.config.ts tests/e2e/planning-mode-visual-verification.spec.ts` ## Screenshots Desktop planning detail:  Desktop planning row:  Desktop staged standard toggle:  Mobile planning detail:  Mobile planning row:  ## Risks - Medium migration risk: this adds a non-null issue column. The migration uses `ADD COLUMN IF NOT EXISTS` so installations that applied an older branch-local migration number can still apply the final numbered migration safely. - Medium contract risk: issue payloads, plugin payloads, and adapter heartbeat payloads now include work mode; compatibility is handled by defaulting missing values to `standard`. - UI risk is moderate because composer controls changed; focused component tests and visual e2e coverage exercise standard vs planning display and toggle 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 - OpenAI Codex, GPT-5 coding agent in a local Paperclip worktree, with shell/tool use. Exact context-window size is not exposed in this runtime. ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
320fd5d23b
commit
a1b30c9f35
65 changed files with 1539 additions and 214 deletions
|
|
@ -1,9 +1,133 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildPaperclipTaskMarkdown,
|
||||
mergeCoalescedContextSnapshot,
|
||||
summarizeHeartbeatRunContextSnapshot,
|
||||
summarizeHeartbeatRunListResultJson,
|
||||
} from "../services/heartbeat.js";
|
||||
|
||||
describe("buildPaperclipTaskMarkdown", () => {
|
||||
it("adds planning directives for assignment and comment task context", () => {
|
||||
const assignment = buildPaperclipTaskMarkdown({
|
||||
issue: {
|
||||
id: "issue-1",
|
||||
identifier: "PAP-3404",
|
||||
title: "Plan first",
|
||||
workMode: "planning",
|
||||
description: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(assignment).toContain("- Work mode: \"planning\"");
|
||||
expect(assignment).toContain("Make the plan only. Do not write code or perform implementation work.");
|
||||
|
||||
const commentWake = buildPaperclipTaskMarkdown({
|
||||
issue: {
|
||||
id: "issue-1",
|
||||
identifier: "PAP-3404",
|
||||
title: "Plan first",
|
||||
workMode: "planning",
|
||||
description: null,
|
||||
},
|
||||
wakeComment: {
|
||||
id: "comment-1",
|
||||
body: "Please revise the plan.",
|
||||
},
|
||||
});
|
||||
|
||||
expect(commentWake).toContain("Update the plan only. Do not write code or perform implementation work.");
|
||||
|
||||
const acceptedConfirmation = buildPaperclipTaskMarkdown({
|
||||
issue: {
|
||||
id: "issue-1",
|
||||
identifier: "PAP-3404",
|
||||
title: "Plan first",
|
||||
workMode: "planning",
|
||||
description: null,
|
||||
},
|
||||
interaction: {
|
||||
kind: "request_confirmation",
|
||||
status: "accepted",
|
||||
},
|
||||
});
|
||||
|
||||
expect(acceptedConfirmation).toContain("Create child issues from the approved plan only");
|
||||
expect(acceptedConfirmation).not.toContain("Make the plan only.");
|
||||
});
|
||||
|
||||
it("prefers ordinary comment planning guidance over stale accepted confirmation state", () => {
|
||||
const commentWake = buildPaperclipTaskMarkdown({
|
||||
issue: {
|
||||
id: "issue-1",
|
||||
identifier: "PAP-3404",
|
||||
title: "Plan first",
|
||||
workMode: "planning",
|
||||
description: null,
|
||||
},
|
||||
wakeComment: {
|
||||
id: "comment-1",
|
||||
body: "Please revise the plan.",
|
||||
},
|
||||
interaction: {
|
||||
kind: "request_confirmation",
|
||||
status: "accepted",
|
||||
},
|
||||
});
|
||||
|
||||
expect(commentWake).toContain("Update the plan only. Do not write code or perform implementation work.");
|
||||
expect(commentWake).not.toContain("Create child issues from the approved plan only");
|
||||
});
|
||||
});
|
||||
|
||||
describe("mergeCoalescedContextSnapshot", () => {
|
||||
it("clears stale accepted-plan interaction state when merging a later ordinary comment wake", () => {
|
||||
const merged = mergeCoalescedContextSnapshot(
|
||||
{
|
||||
issueId: "issue-1",
|
||||
interactionId: "interaction-1",
|
||||
interactionKind: "request_confirmation",
|
||||
interactionStatus: "accepted",
|
||||
continuationPolicy: "wake_assignee_on_accept",
|
||||
wakeReason: "issue_commented",
|
||||
},
|
||||
{
|
||||
issueId: "issue-1",
|
||||
commentId: "comment-1",
|
||||
wakeCommentId: "comment-1",
|
||||
wakeReason: "issue_commented",
|
||||
},
|
||||
);
|
||||
|
||||
expect(merged.interactionId).toBeUndefined();
|
||||
expect(merged.interactionKind).toBeUndefined();
|
||||
expect(merged.interactionStatus).toBeUndefined();
|
||||
expect(merged.continuationPolicy).toBeUndefined();
|
||||
expect(merged.commentId).toBe("comment-1");
|
||||
expect(merged.wakeCommentId).toBe("comment-1");
|
||||
});
|
||||
|
||||
it("preserves accepted-plan interaction state for the interaction wake itself", () => {
|
||||
const merged = mergeCoalescedContextSnapshot(
|
||||
{
|
||||
issueId: "issue-1",
|
||||
},
|
||||
{
|
||||
issueId: "issue-1",
|
||||
interactionId: "interaction-1",
|
||||
interactionKind: "request_confirmation",
|
||||
interactionStatus: "accepted",
|
||||
continuationPolicy: "wake_assignee_on_accept",
|
||||
wakeReason: "issue_commented",
|
||||
},
|
||||
);
|
||||
|
||||
expect(merged.interactionId).toBe("interaction-1");
|
||||
expect(merged.interactionKind).toBe("request_confirmation");
|
||||
expect(merged.interactionStatus).toBe("accepted");
|
||||
expect(merged.continuationPolicy).toBe("wake_assignee_on_accept");
|
||||
});
|
||||
});
|
||||
|
||||
describe("summarizeHeartbeatRunContextSnapshot", () => {
|
||||
it("keeps only the small retry/linking fields needed by the client", () => {
|
||||
const summarized = summarizeHeartbeatRunContextSnapshot({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { randomUUID } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
agents,
|
||||
|
|
@ -110,6 +111,7 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
|||
{
|
||||
clientKey: "root",
|
||||
title: "Create the root follow-up",
|
||||
workMode: "planning",
|
||||
assigneeAgentId,
|
||||
},
|
||||
{
|
||||
|
|
@ -153,6 +155,19 @@ describeEmbeddedPostgres("issueThreadInteractionService", () => {
|
|||
status: "todo",
|
||||
}),
|
||||
]);
|
||||
const createdIssueRows = await db
|
||||
.select({
|
||||
title: issues.title,
|
||||
workMode: issues.workMode,
|
||||
})
|
||||
.from(issues)
|
||||
.where(eq(issues.companyId, companyId));
|
||||
expect(createdIssueRows).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ title: "Create the root follow-up", workMode: "planning" }),
|
||||
expect.objectContaining({ title: "Create the nested follow-up", workMode: "standard" }),
|
||||
]),
|
||||
);
|
||||
|
||||
const children = await issuesSvc.list(companyId, { parentId: issueId });
|
||||
expect(children).toHaveLength(1);
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ const legacyProjectLinkedIssue = {
|
|||
title: "Legacy onboarding task",
|
||||
description: "Seed the first CEO task",
|
||||
status: "todo",
|
||||
workMode: "planning",
|
||||
priority: "medium",
|
||||
projectId: "22222222-2222-4222-8222-222222222222",
|
||||
goalId: null,
|
||||
|
|
@ -264,6 +265,7 @@ describe.sequential("issue goal context routes", () => {
|
|||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.issue.goalId).toBe(projectGoal.id);
|
||||
expect(res.body.issue.workMode).toBe("planning");
|
||||
expect(res.body.goal).toEqual(
|
||||
expect.objectContaining({
|
||||
id: projectGoal.id,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue