diff --git a/packages/db/src/migrations/0049_flawless_abomination.sql b/packages/db/src/migrations/0049_flawless_abomination.sql index adca835c..a48c468e 100644 --- a/packages/db/src/migrations/0049_flawless_abomination.sql +++ b/packages/db/src/migrations/0049_flawless_abomination.sql @@ -10,6 +10,7 @@ CREATE TABLE "issue_relations" ( "updated_at" timestamp with time zone DEFAULT now() NOT NULL ); --> statement-breakpoint +ALTER TABLE "issue_relations" ADD CONSTRAINT "issue_relations_type_check" CHECK ("type" IN ('blocks'));--> statement-breakpoint ALTER TABLE "issue_relations" ADD CONSTRAINT "issue_relations_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint ALTER TABLE "issue_relations" ADD CONSTRAINT "issue_relations_issue_id_issues_id_fk" FOREIGN KEY ("issue_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint ALTER TABLE "issue_relations" ADD CONSTRAINT "issue_relations_related_issue_id_issues_id_fk" FOREIGN KEY ("related_issue_id") REFERENCES "public"."issues"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint @@ -17,4 +18,4 @@ ALTER TABLE "issue_relations" ADD CONSTRAINT "issue_relations_created_by_agent_i CREATE INDEX "issue_relations_company_issue_idx" ON "issue_relations" USING btree ("company_id","issue_id");--> statement-breakpoint CREATE INDEX "issue_relations_company_related_issue_idx" ON "issue_relations" USING btree ("company_id","related_issue_id");--> statement-breakpoint CREATE INDEX "issue_relations_company_type_idx" ON "issue_relations" USING btree ("company_id","type");--> statement-breakpoint -CREATE UNIQUE INDEX "issue_relations_company_edge_uq" ON "issue_relations" USING btree ("company_id","issue_id","related_issue_id","type"); \ No newline at end of file +CREATE UNIQUE INDEX "issue_relations_company_edge_uq" ON "issue_relations" USING btree ("company_id","issue_id","related_issue_id","type"); diff --git a/packages/db/src/schema/issue_relations.ts b/packages/db/src/schema/issue_relations.ts index ed7a0e7f..ee0150b1 100644 --- a/packages/db/src/schema/issue_relations.ts +++ b/packages/db/src/schema/issue_relations.ts @@ -10,7 +10,7 @@ export const issueRelations = pgTable( companyId: uuid("company_id").notNull().references(() => companies.id), issueId: uuid("issue_id").notNull().references(() => issues.id, { onDelete: "cascade" }), relatedIssueId: uuid("related_issue_id").notNull().references(() => issues.id, { onDelete: "cascade" }), - type: text("type").notNull(), + type: text("type").$type<"blocks">().notNull(), createdByAgentId: uuid("created_by_agent_id").references(() => agents.id, { onDelete: "set null" }), createdByUserId: text("created_by_user_id"), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 3bef40fa..a223e962 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -819,6 +819,13 @@ export function issueService(db: Db) { } if (deduped.length > 0) { + const lockedIssueIds = [issueId, ...deduped].sort(); + await dbOrTx.execute( + sql`SELECT ${issues.id} FROM ${issues} + WHERE ${and(eq(issues.companyId, companyId), inArray(issues.id, lockedIssueIds))} + ORDER BY ${issues.id} + FOR UPDATE`, + ); const relatedIssues = await dbOrTx .select({ id: issues.id }) .from(issues) diff --git a/skills/paperclip/SKILL.md b/skills/paperclip/SKILL.md index 43a69b41..c260fa32 100644 --- a/skills/paperclip/SKILL.md +++ b/skills/paperclip/SKILL.md @@ -128,6 +128,8 @@ Paperclip fires automatic wakes in two scenarios: 1. **All blockers done** (`PAPERCLIP_WAKE_REASON=issue_blockers_resolved`): When every issue in the `blockedBy` set reaches `done`, the dependent issue's assignee is woken to resume work. 2. **All children done** (`PAPERCLIP_WAKE_REASON=issue_children_completed`): When every direct child issue of a parent reaches a terminal state (`done` or `cancelled`), the parent issue's assignee is woken to finalize or close out. +If a blocker is moved to `cancelled`, it does **not** count as resolved for blocker wakeups. Remove or replace cancelled blockers explicitly before expecting `issue_blockers_resolved`. + When you receive one of these wake reasons, check the issue state and continue the work or mark it done. ## Project Setup Workflow (CEO/Manager Common Path) diff --git a/skills/paperclip/references/api-reference.md b/skills/paperclip/references/api-reference.md index 60f02803..e9a089b2 100644 --- a/skills/paperclip/references/api-reference.md +++ b/skills/paperclip/references/api-reference.md @@ -189,6 +189,8 @@ The response also includes `blockedBy` and `blocks` arrays showing first-class d } ``` +Blocker wake semantics are strict: `issue_blockers_resolved` only fires when every blocker reaches `done`. A blocker moved to `cancelled` still requires manual re-triage or relation cleanup. + --- ## Worked Example: IC Heartbeat