diff --git a/DEVLOG.md b/DEVLOG.md index a967e092..1ae7a557 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -11,6 +11,25 @@ expandable Live Trace progress. The immutable tag, production deployment, and live verification remain separate delivery facts until their respective release steps complete. +## 2026-07-22 — AXI-143 issue-local attention and human-readable references + +Made issue-bound free-form action requests directly answerable from the issue +detail instead of forcing an operator through Command Center. Delivery +connection conflicts retain their richer Command Center workflow and now use +an explicit fallback label on the issue. + +Added a shared issue-reference treatment that pairs the stable identifier with +the issue title. Applied it across dashboard attention, Inbox asks and waiting +items, Activity drawer asks, active runs, failed-run recovery, and running +timers so operational queues provide meaning without requiring operators to +memorize issue numbers. + +Updated the release gate policy to use exact-head GitHub CI as the normal +release authority. Fast local checks remain the pre-push feedback loop; the +complete Docker and Playwright gate is now supplemental evidence for CI +outages, failure diagnosis, or environment-sensitive changes instead of a +routine duplicate of the hosted matrix. + ## 2026-07-20 — v0.29.4 release preparation Prepared the serialized patch release for AXI-139 and merged PR #81. Release diff --git a/RELEASE.md b/RELEASE.md index 5f105e6e..ecba41e6 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -56,13 +56,21 @@ Forge's explicit branch contract: ## Release Process -Use the fast quality gate while iterating: +Use the fast quality gate while iterating and before pushing a release candidate: ```bash pnpm ci:local:quality ``` -Before declaring a PR release-ready, run the complete local gate once: +The required release gate is GitHub CI on the exact PR head. Its lint, +typecheck, unit/integration, build, and Playwright jobs must finish successfully +before merge. After any new commit, wait for CI to rerun against that new SHA; +results from an earlier head do not qualify the release. + +Do not routinely duplicate the complete CI matrix locally. Run the complete +local gate when GitHub Actions is unavailable, when diagnosing a CI failure, or +when a database, migration, browser, concurrency, or environment-sensitive +change warrants local integration evidence: ```bash pnpm ci:local @@ -74,7 +82,8 @@ the Prisma client, and runs lint, typecheck, and Vitest once. It then serializes Playwright behind a portable machine-local lock and selects an available port. `pnpm ci:local:e2e` runs only the E2E phase; add `-- --reuse-e2e-build` only when the existing production build is known to match the current source. These -local commands complement, but do not replace, CI on the exact PR head. +local commands provide fast feedback or supplemental evidence; they never +replace exact-head CI when GitHub Actions is available. ### Authorized single-PR patch releases @@ -101,10 +110,10 @@ an ordinary feature PR. notes, and merges the release commit. Feature PRs must not race independent version bumps or tags. -> CI note: GitHub-hosted Actions require billing to be enabled. If Actions are -> unavailable, gate the release on the equivalent local run -> (`pnpm lint && pnpm typecheck && pnpm test` + `pnpm test:e2e` against the -> `docker/docker-compose.yml` stack) and record that in the release notes. +> CI outage fallback: if GitHub Actions is unavailable, gate the release on +> `pnpm ci:local` against the guarded local service stack and record the exact +> command, commit SHA, result, and reason for using the fallback in the release +> notes. A local result is not a substitute merely because CI is slow or queued. ### 2. Tag the release diff --git a/src/app/(app)/w/[slug]/command-center/page.tsx b/src/app/(app)/w/[slug]/command-center/page.tsx index 08f9cb88..eb1aa361 100644 --- a/src/app/(app)/w/[slug]/command-center/page.tsx +++ b/src/app/(app)/w/[slug]/command-center/page.tsx @@ -42,6 +42,7 @@ import { } from "@/components/action-requests/attention-request-model"; import { MarkdownWithAttachments } from "@/components/markdown/attachment-renderer"; import { WorkspaceActivityTimeline } from "@/components/workspace-activity-timeline"; +import { IssueReference } from "@/components/issue-reference"; import { trpc } from "@/lib/trpc"; import { useWorkspace } from "@/hooks/use-workspace"; import { useRealtime } from "@/hooks/use-realtime"; @@ -429,9 +430,10 @@ export default function CommandCenterPage() { @{row.agent.profileKey} - - {row.issue.workspace.key}-{row.issue.number} - + {row.currentStep ? ( {row.currentStep} @@ -477,9 +479,10 @@ export default function CommandCenterPage() { href={`/w/${ws.slug}/i/${data.runningTimer.issue.workspace.key}-${data.runningTimer.issue.number}`} className="flex flex-col gap-1 rounded-md border border-ember/40 bg-ember/5 p-2 hover:border-ember" > - - {data.runningTimer.issue.workspace.key}-{data.runningTimer.issue.number} - + Started {new Date(data.runningTimer.startedAt).toLocaleTimeString()} @@ -1679,12 +1682,11 @@ function RunFailureCard({
- - {run.issue.workspace.key}-{run.issue.number} - + className="max-w-full text-sm font-medium" + /> {presentation.badge} diff --git a/src/app/(app)/w/[slug]/inbox/page.tsx b/src/app/(app)/w/[slug]/inbox/page.tsx index e4c22ed1..01a974d2 100644 --- a/src/app/(app)/w/[slug]/inbox/page.tsx +++ b/src/app/(app)/w/[slug]/inbox/page.tsx @@ -30,6 +30,7 @@ import { Button } from "@/components/ui/button"; import { Badge, Card, EmptyState, Kbd, MOTION, Section, SkeletonList } from "@/components/ui"; import { Picker } from "@/components/ui/modal"; import { AgentPresenceDot } from "@/components/agent-presence-dot"; +import { IssueReference } from "@/components/issue-reference"; import { presenceAvailability } from "@/lib/transport-display"; import { BulkBar, type BulkBarAction } from "@/components/bulk-bar"; import { BulkProjectPicker, BulkCyclePicker } from "@/components/bulk-move-pickers"; @@ -1263,16 +1264,18 @@ function NeedsInputSection({
- - {issue ? formatIssueId(issue.workspace.key, issue.number) : "Workspace ask"} - - - {request.title} - + {issue ? ( + + ) : ( + + Workspace ask + + )} {request.issueOpenCount > 1 && ( +{request.issueOpenCount - 1} more )}
+
{request.title}
{request.body && (
{request.body}
)} @@ -1297,15 +1300,11 @@ function NeedsInputSection({
- - {formatIssueId(row.issue.workspace.key, row.issue.number)} - - - {row.issue.title} - + className="max-w-full flex-1" + /> } - title={request.title} - meta={ - request.requestedByAgent - ? `@${request.requestedByAgent.profileKey} · ${relativeTime(request.createdAt)}` - : relativeTime(request.createdAt) - } + title={issue?.title ?? request.title} + meta={[ + issue ? request.title : null, + request.requestedByAgent ? `@${request.requestedByAgent.profileKey}` : null, + relativeTime(request.createdAt), + ] + .filter(Boolean) + .join(" · ")} /> ); })} diff --git a/src/components/dashboard/needs-you-tile.tsx b/src/components/dashboard/needs-you-tile.tsx index 92a35d36..2e5ce2c3 100644 --- a/src/components/dashboard/needs-you-tile.tsx +++ b/src/components/dashboard/needs-you-tile.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import { ArrowRight, AtSign, Inbox, Shield } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { IssueReference } from "@/components/issue-reference"; import { trpc } from "@/lib/trpc"; /** @@ -48,7 +49,12 @@ export function NeedsYouTile({ slug }: { slug: string }) { title: string; handle: string | null; avatar: string | null; - issueLabel: string | null; + issue: { + id: string; + number: number; + title: string; + workspace: { key: string; slug: string }; + } | null; completionIntent: "COMPLETE" | "RECOVER" | null; } | { @@ -67,7 +73,7 @@ export function NeedsYouTile({ slug }: { slug: string }) { title: r.title, handle: r.requestedByAgent?.profileKey ?? null, avatar: r.requestedByAgent?.avatar ?? null, - issueLabel: r.issue ? `${r.issue.workspace.key}-${r.issue.number}` : null, + issue: r.issue, completionIntent: readCompletionIntent(r.payload), }); } @@ -130,10 +136,17 @@ export function NeedsYouTile({ slug }: { slug: string }) { {!card.completionIntent && "asks: "} “{card.title}”
-
- {card.issueLabel ? {card.issueLabel} : null} - {card.issueLabel ? " · " : ""}action request -
+ {card.issue ? ( + + ) : ( +
+ Workspace action request +
+ )}
{requests.map((request) => - request.kind === "FREE_FORM" || request.kind === "DELIVERY_CONNECTION_CONFLICT" ? ( + request.kind === "FREE_FORM" ? ( + + ) : request.kind === "DELIVERY_CONNECTION_CONFLICT" ? (
-
{request.title}
+
+ Delivery decision required +
+
{request.title}
{request.body ? (

{request.body} @@ -378,9 +397,7 @@ function IssueActionRequests({ issueId, canResolve }: { issueId: string; canReso href={`/w/${ws.slug}/command-center`} className="text-meta mt-2 inline-flex text-ember hover:underline" > - {request.kind === "DELIVERY_CONNECTION_CONFLICT" - ? "Choose a delivery action in Command Center" - : "Answer in Command Center"} + Review delivery options in Command Center

) : ( @@ -396,6 +413,127 @@ function IssueActionRequests({ issueId, canResolve }: { issueId: string; canReso ); } +function IssueFreeFormRequest({ + request, + issueId, + canResolve, +}: { + request: { id: string; title: string; body: string | null }; + issueId: string; + canResolve: boolean; +}) { + const ws = useWorkspace(); + const utils = trpc.useUtils(); + const [replying, setReplying] = useState(false); + const [answer, setAnswer] = useState(""); + const visibleCanResolve = canResolve || ws.role === "OWNER" || ws.role === "ADMIN"; + const settle = () => { + void utils.actionRequest.list.invalidate(); + void utils.issue.byId.invalidate({ id: issueId }); + void utils.issue.activity.invalidate({ issueId }); + void utils.comment.listForIssue.invalidate({ issueId }); + void utils.commandCenter.summary.invalidate(); + void utils.commandCenter.decisionsCount.invalidate(); + }; + const respond = trpc.actionRequest.accept.useMutation({ + onSuccess: () => { + settle(); + toast.success("Response sent on this issue."); + }, + onError: (error) => toast.error(error.message), + }); + const decline = trpc.actionRequest.decline.useMutation({ + onSuccess: () => { + settle(); + toast.success("Request declined."); + }, + onError: (error) => toast.error(error.message), + }); + const pending = respond.isPending || decline.isPending; + + return ( +
+
Response requested
+
{request.title}
+ {request.body ? ( +

{request.body}

+ ) : null} + {visibleCanResolve ? ( + replying ? ( +
{ + event.preventDefault(); + const resolution = answer.trim(); + if (!resolution) { + toast.error("Write a response first."); + return; + } + respond.mutate({ id: request.id, resolution }); + }} + > + +