Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ date, grouped by `Added` / `Changed` / `Fixed` / `Removed`.

## [Unreleased]

## [2026-07-14] — v0.14.0 · Proactive completion handoff

### Added

- **Verified work can now become one actionable close-out decision.** EXECUTE agents can explicitly recommend issue completion, while merged implementation PRs and completed GitHub checks feed the same idempotent evidence card in issue detail, Command Center, Dashboard, Inbox, and notifications.
- **Completion behavior is policy-driven.** Workspaces can disable, recommend, or safely automate completion against a chosen DONE status, and projects can inherit or override that policy.

### Changed

- **Automatic completion is guarded centrally.** Live runs, pending review gates, unresolved decisions, blocking issues, unmerged implementation PRs, and unconfirmed checks hold automation and remain visible as evidence instead of silently closing work.
- **Closed, unmerged PRs produce a recovery decision.** Review-stage work can return to the configured active status or link a replacement PR from the native GitHub panel.

### Fixed

- **Repeated lifecycle signals no longer stack cards or notifications.** Producer-owned dedupe keys refresh one durable request, identical refreshes are no-ops, and a periodic reconciler repairs missed transitions and completes newly-unblocked safe candidates.

## [2026-07-14] — v0.13.1 · Reliable agent wake boundaries

### Changed
Expand Down
37 changes: 37 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12764,3 +12764,40 @@ build passed; and all 38 Playwright journeys passed serially. The canonical
gate first encountered the known cross-suite stale-work redispatch race; that
9-test file passed immediately in isolation and the full suite then passed
cleanly.

---

## 2026-07-14 — Proactive issue completion and PR recovery

Added a settings-driven completion policy with workspace OFF, RECOMMEND, and
AUTO_WHEN_SAFE modes, a configurable DONE status, and optional project-level
overrides. Verified EXECUTE runs may now explicitly recommend the issue itself
for completion. Native GitHub implementation links, manual sync, PR webhooks,
and completed check-suite outcomes converge on the same evaluator.

The evaluator blocks automatic completion while live runs, review gates,
unresolved decisions, blockers, unmerged PRs, or unconfirmed checks remain.
It emits one durable TRANSITION ActionRequest with compact evidence and clear
Mark done / Keep in review actions, or transitions atomically when the safety
gate is clear. Closed unmerged PRs create a matching recovery decision to
return review-stage work to the configured active status or link a replacement
PR. Dashboard and shared action-request surfaces show animated completion or
recovery state without expanding cards by default.

Added producer-owned ActionRequest dedupe keys with a database partial unique
index. Changed repeated identical refreshes into true no-ops so duplicate
webhook deliveries cannot create activity or notification spam. A five-minute
worker reconciliation pass repairs missed event handoffs and re-evaluates
AUTO_WHEN_SAFE cards after held dependencies clear.

CI also exposed a same-millisecond chat ordering edge case: an agent reply
could remain in the durable inbox when its timestamp tied the preceding user
message. Inbox suppression now uses the canonical timestamp-plus-id ordering,
with a deterministic regression test.

Verification: focused completion and MCP suites passed 133 tests; the full
Vitest suite passed 1,244 tests with 1 intentional live-connector skip;
typecheck and lint passed with only pre-existing repository warnings; and a
fresh production build passed. The full serial Playwright run passed 36 of 38
journeys; its shared-state chat and Command Center failures both passed against
a clean disposable E2E database immediately afterward.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "forge",
"version": "0.13.1",
"version": "0.14.0",
"private": true,
"description": "Forge — a fast, minimalist, keyboard-driven project management platform with pluggable agents.",
"license": "MIT",
Expand Down
35 changes: 35 additions & 0 deletions prisma/migrations/0101_completion_candidates/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
-- Settings-driven completion automation and durable shared candidate dedupe.
CREATE TYPE "CompletionAutomation" AS ENUM ('OFF', 'RECOMMEND', 'AUTO_WHEN_SAFE');

ALTER TABLE "Workspace"
ADD COLUMN "completionAutomation" "CompletionAutomation" NOT NULL DEFAULT 'RECOMMEND',
ADD COLUMN "completionStatusId" TEXT;

ALTER TABLE "Project"
ADD COLUMN "completionAutomation" "CompletionAutomation";

ALTER TABLE "ActionRequest"
ADD COLUMN "dedupeKey" TEXT;

ALTER TABLE "Workspace"
ADD CONSTRAINT "Workspace_completionStatusId_fkey"
FOREIGN KEY ("completionStatusId") REFERENCES "Status"("id")
ON DELETE SET NULL ON UPDATE CASCADE;

CREATE INDEX "ActionRequest_workspaceId_dedupeKey_idx"
ON "ActionRequest"("workspaceId", "dedupeKey");

CREATE UNIQUE INDEX "ActionRequest_open_generic_dedupe"
ON "ActionRequest"("workspaceId", "dedupeKey")
WHERE "status" = 'OPEN' AND "dedupeKey" IS NOT NULL;

-- Existing workspaces already have at least one DONE status. Choose the first
-- configured position as the safe default; admins can change or clear it.
UPDATE "Workspace" w
SET "completionStatusId" = (
SELECT s."id"
FROM "Status" s
WHERE s."workspaceId" = w."id" AND s."category" = 'DONE'
ORDER BY s."position" ASC, s."id" ASC
LIMIT 1
);
68 changes: 46 additions & 22 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,14 @@ enum RunBudgetAction {
STOP
}

/// Controls how verified completion evidence affects an open issue.
/// Project-level null inherits the workspace value.
enum CompletionAutomation {
OFF
RECOMMEND
AUTO_WHEN_SAFE
}

enum ChatRole {
USER
AGENT
Expand Down Expand Up @@ -874,6 +882,13 @@ model Workspace {
/// work should surface for review regardless of prior state. Null
/// disables — no auto-transition on completion.
reviewStatusId String?
/// What Forge should do when an agent or linked implementation PR provides
/// verified completion evidence. RECOMMEND opens one shared actionable
/// card; AUTO_WHEN_SAFE transitions only when the central safety gate passes.
completionAutomation CompletionAutomation @default(RECOMMEND)
/// DONE-category status used by completion recommendations/automation.
/// Null keeps completion candidates disabled until an admin chooses one.
completionStatusId String?
/// Issues with `updatedAt` older than this threshold (in days) are
/// considered "stalled" by the dashboard suggestions bucket. Settings-
/// driven so admins can tune the workspace's idle definition without
Expand Down Expand Up @@ -968,6 +983,7 @@ model Workspace {
canvasStyles CanvasStyle[]
startedStatus Status? @relation("WorkspaceStartedStatus", fields: [startedStatusId], references: [id], onDelete: SetNull)
reviewStatus Status? @relation("WorkspaceReviewStatus", fields: [reviewStatusId], references: [id], onDelete: SetNull)
completionStatus Status? @relation("WorkspaceCompletionStatus", fields: [completionStatusId], references: [id], onDelete: SetNull)

@@index([slug])
@@index([defaultIssueAssigneeUserId])
Expand Down Expand Up @@ -997,34 +1013,37 @@ model Membership {
// ----------------------------------------------------------------------------

model Project {
id String @id @default(cuid())
workspaceId String
key String
name String
description String? @db.Text
icon String?
color String?
archived Boolean @default(false)
startDate DateTime?
targetDate DateTime?
id String @id @default(cuid())
workspaceId String
key String
name String
description String? @db.Text
icon String?
color String?
archived Boolean @default(false)
startDate DateTime?
targetDate DateTime?
/// Optional parent initiative that groups related projects.
initiativeId String?
createdById String
initiativeId String?
createdById String
/// Backlink for a project created via `notes.promote`. Mirrors
/// `Issue.sourceNoteId`.
sourceNoteId String?
sourceNoteId String?
/// Git repository this project's work lives in. When set, a runtime
/// provisions (clone-or-pulls) it so an agent dispatched to one of this
/// project's issues lands in the right checkout — letting one runtime serve
/// many codebases. `repoUrl` is an https (token/app-auth) or ssh remote;
/// `repoBranch` null = the remote's default branch. The clone path is
/// derived from the repo name. Auth comes from the runtime's GitHub App or
/// secrets (see `docs/agents/runtime-credentials.md`).
repoUrl String?
repoBranch String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
repoUrl String?
repoBranch String?
/// Optional override for issue completion automation. Null inherits the
/// workspace policy so projects can opt into stricter or quieter behavior.
completionAutomation CompletionAutomation?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?

workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
createdBy User @relation("ProjectCreator", fields: [createdById], references: [id])
Expand All @@ -1051,10 +1070,11 @@ model Status {
position Int
isDefault Boolean @default(false)

workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
issues Issue[]
workspaceStartedFor Workspace[] @relation("WorkspaceStartedStatus")
workspaceReviewFor Workspace[] @relation("WorkspaceReviewStatus")
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
issues Issue[]
workspaceStartedFor Workspace[] @relation("WorkspaceStartedStatus")
workspaceReviewFor Workspace[] @relation("WorkspaceReviewStatus")
workspaceCompletionFor Workspace[] @relation("WorkspaceCompletionStatus")

@@unique([workspaceId, name])
@@index([workspaceId, category])
Expand Down Expand Up @@ -3475,6 +3495,9 @@ model ActionRequest {
/// Polymorphic source — chat-message, comment, agent-run, etc.
sourceType String?
sourceId String?
/// Stable producer-owned key for idempotent shared recommendations. A
/// partial unique index permits at most one OPEN request per workspace/key.
dedupeKey String?
/// Optional pointer at the issue this request relates to. Lets the
/// inbox surface group requests under their parent issue without
/// re-deriving from sourceType.
Expand Down Expand Up @@ -3507,6 +3530,7 @@ model ActionRequest {
@@index([assignedAgentId, status])
@@index([issueId])
@@index([sourceType, sourceId])
@@index([workspaceId, dedupeKey])
@@index([supersededById])
}

Expand Down
34 changes: 26 additions & 8 deletions src/app/(app)/w/[slug]/command-center/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { Picker, useConfirm } from "@/components/ui/modal";
import { EmptyState, SkeletonList } from "@/components/ui";
import { AgentAttentionPanel } from "@/components/agent-attention-panel";
import { RunApprovalCard } from "@/components/agents/run-approval-card";
import { ActionRequestCard } from "@/components/action-requests/action-request-card";
import { WorkspaceActivityTimeline } from "@/components/workspace-activity-timeline";
import { trpc } from "@/lib/trpc";
import { useWorkspace } from "@/hooks/use-workspace";
Expand Down Expand Up @@ -224,14 +225,24 @@ export default function CommandCenterPage() {
count={data.counts.actionRequests}
empty="No agent asks."
>
{data.actionRequests.map((row) => (
<ActionRequestDecisionCard
key={row.id}
request={row}
slug={ws.slug}
onResolved={() => dropActionRequest(row.id)}
/>
))}
{data.actionRequests.map((row) =>
completionIntent(row.payload) && row.issue ? (
<ActionRequestCard
key={row.id}
requestId={row.id}
issueId={row.issue.id}
canResolve={canRecoverRuns}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve watcher access to completion decisions

For completion/recovery rows in Command Center, this passes only the admin-only canRecoverRuns flag into ActionRequestCard. The server ACL still allows issue watchers, and issue assignees are auto-watchers, to accept or decline unassigned action requests, but the card hides both buttons when canResolve is false, so non-admin watchers/assignees see a ready-to-close ask they are allowed to resolve but cannot act on from this surface. Pass a per-row watcher/assignee permission signal or keep the legacy action card for those users.

Useful? React with 👍 / 👎.

onResolved={() => dropActionRequest(row.id)}
/>
) : (
<ActionRequestDecisionCard
key={row.id}
request={row}
slug={ws.slug}
onResolved={() => dropActionRequest(row.id)}
/>
),
)}
</AttentionGroup>
) : null}
{data.runtimeApprovals.length > 0 ? (
Expand Down Expand Up @@ -526,6 +537,7 @@ type CCActionRequest = {
severity: string;
/** ActionRequestKind — FREE_FORM asks need an answer, not just Accept. */
kind: string;
payload: unknown;
issue: {
id: string;
number: number;
Expand All @@ -540,6 +552,12 @@ type CCActionRequest = {
requestedByUser: { name: string | null } | null;
};

function completionIntent(payload: unknown): "COMPLETE" | "RECOVER" | null {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
const intent = (payload as Record<string, unknown>).intent;
return intent === "COMPLETE" || intent === "RECOVER" ? intent : null;
}

type CCRuntimeApproval = {
id: string;
currentStep: string | null;
Expand Down
Loading
Loading