[codex] Add issue document locking (#6009)

## Thinking Path

> - Paperclip orchestrates AI-agent companies through company-scoped
issues, comments, and issue documents.
> - Issue documents are the durable place where plans, handoffs, and
other work artifacts are revised over time.
> - Some documents need to be preserved as operator-approved snapshots
while agents continue working on the same issue.
> - Without document locking, a later board or agent write can overwrite
the document key that reviewers expected to remain stable.
> - This pull request adds board-managed issue document locks and makes
agent writes to locked keys create a derived document instead of
mutating the locked document.
> - The benefit is safer document handoffs: approved or frozen issue
documents stay immutable until the board explicitly unlocks them.

## What Changed

- Added `locked_at`, `locked_by_agent_id`, and `locked_by_user_id`
document fields plus migration `0085_tranquil_the_executioner.sql`.
- Added document lock/unlock service behavior, route endpoints, activity
events, and locked-document write protections.
- Made agent document writes to locked keys create a new derived key
such as `plan-2` rather than overwriting the locked document.
- Surfaced lock state through shared issue document types, UI API
methods, document header lock controls, and activity formatting.
- Added server and UI tests for lock/unlock behavior, locked document
immutability, and UI action visibility.
- Updated `doc/SPEC-implementation.md` with the V1 document lock
contract and endpoints.

## Verification

- `git rebase public-gh/master` completed cleanly after committing the
branch changes.
- `git diff --check` passed before commit.
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/documents-service.test.ts
server/src/__tests__/issue-agent-mutation-ownership-routes.test.ts
ui/src/components/IssueDocumentsSection.test.tsx
ui/src/components/IssueContinuationHandoff.test.tsx
ui/src/lib/document-revisions.test.ts` passed: 5 files, 32 tests.

## Risks

- Medium risk because this changes the document persistence contract and
adds a migration.
- The migration uses `ADD COLUMN IF NOT EXISTS` and guarded foreign-key
creation so it remains safe for users who may have already applied an
earlier copy of the migration.
- Locked documents intentionally reject board edits/deletes/restores
until unlocked; any existing workflows that expected direct overwrite
need to unlock first.
- Agent writes to locked keys now create derived documents, which may
create extra issue documents when agents retry locked writes.

## Model Used

- OpenAI Codex coding agent based on GPT-5, with tool use and local code
execution in the Paperclip worktree.

## 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
- [ ] 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:
Dotta
2026-05-15 08:54:55 -05:00
committed by GitHub
parent 901c088e14
commit 03ad5c5bea
18 changed files with 684 additions and 27 deletions
@@ -0,0 +1,8 @@
ALTER TABLE "documents" ADD COLUMN IF NOT EXISTS "locked_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "documents" ADD COLUMN IF NOT EXISTS "locked_by_agent_id" uuid;--> statement-breakpoint
ALTER TABLE "documents" ADD COLUMN IF NOT EXISTS "locked_by_user_id" text;--> statement-breakpoint
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'documents_locked_by_agent_id_agents_id_fk') THEN
ALTER TABLE "documents" ADD CONSTRAINT "documents_locked_by_agent_id_agents_id_fk" FOREIGN KEY ("locked_by_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;
END IF;
END $$;
@@ -596,6 +596,13 @@
"when": 1778355326070,
"tag": "0084_issue_recovery_actions",
"breakpoints": true
},
{
"idx": 85,
"version": "7",
"when": 1778787362162,
"tag": "0085_tranquil_the_executioner",
"breakpoints": true
}
]
}
+3
View File
@@ -16,6 +16,9 @@ export const documents = pgTable(
createdByUserId: text("created_by_user_id"),
updatedByAgentId: uuid("updated_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
updatedByUserId: text("updated_by_user_id"),
lockedAt: timestamp("locked_at", { withTimezone: true }),
lockedByAgentId: uuid("locked_by_agent_id").references(() => agents.id, { onDelete: "set null" }),
lockedByUserId: text("locked_by_user_id"),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
},
+3
View File
@@ -1604,6 +1604,9 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
createdByUserId: existing?.createdByUserId ?? null,
updatedByAgentId: null,
updatedByUserId: null,
lockedAt: existing?.lockedAt ?? null,
lockedByAgentId: existing?.lockedByAgentId ?? null,
lockedByUserId: existing?.lockedByUserId ?? null,
createdAt: existing?.createdAt ?? now,
updatedAt: now,
body: input.body,
+3
View File
@@ -96,6 +96,9 @@ export interface IssueDocumentSummary {
createdByUserId: string | null;
updatedByAgentId: string | null;
updatedByUserId: string | null;
lockedAt: Date | null;
lockedByAgentId: string | null;
lockedByUserId: string | null;
createdAt: Date;
updatedAt: Date;
}