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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ tests/e2e/.auth/
playwright-report/
test-results/
.next-e2e/
.next-lifecycle/
playwright-report-lifecycle/
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ date, grouped by `Added` / `Changed` / `Fixed` / `Removed`.

## [Unreleased]

## [2026-07-14] — v0.13.0 · Reliable lifecycle operations

### Added

- **A deterministic Lifecycle Lab now covers the operator journey end to end.** Eight isolated, frozen states exercise ready, assigned, active, waiting, approval, review, completed, and stalled work without touching shared development or production data.

### Changed

- **Requests for operator input now stay visible across the product.** Action requests and conversational agent questions share one deduplicated “Needs your input” stream in Inbox and the notification drawer, with matching unread badge behavior and less empty-section scrolling.
- **Command Center carries four attention lanes cleanly at wide widths.** Asks, runtime approvals, stalled runs, and review gates remain visible together, while issue breadcrumbs, notification landmarks, headings, and mention autocomplete use clearer accessibility semantics.

### Fixed

- **AI triage no longer asks Hermes to execute an unknown `submit_triage` tool.** Hermes receives a compatible structured response contract, provider credentials resolve consistently, stale triage claims can be retried safely, and accepted suggestions follow Forge’s normal label, assignment, dispatch, audit, and wake lifecycle.

## [2026-07-14] — v0.12.0 · Calmer issue operations

### Added
Expand Down
59 changes: 59 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12669,3 +12669,62 @@ focused database suites passed (156 tests). The parallel full Vitest run passed
9-test file passed immediately in isolation. A fresh production build passed
the Command Center and issue-flow Playwright specs, and the notification drawer
focus-containment/restore regression also passed (3 focused browser tests).

---

## 2026-07-14 — AXI-104 AI triage audit and repair

Audited the full AI-triage path after AXI-104 exposed a Hermes response saying
`Unknown tool: submit_triage`. Hermes now receives an exact plain-JSON response
contract instead of a request-scoped function that its server-side tool
registry cannot execute. Direct OpenAI-compatible providers retain forced tool
output. The compatibility parser now safely resolves unique label names, agent
profile keys, and agent names, while restricting name-based assignments to
explicit assignment clauses and stripping provider/tool failure prose from the
operator-facing rationale.

Unified Triage, Coach, description assist, plan generation, and AI settings
health around the same workspace-first provider credential resolver. Added
atomic triage claims and apply decisions, rejected duplicate in-flight reruns,
added a five-minute claim lease plus stale-only Retry so process exits cannot
strand the card in `PENDING`, and made Apply run the standard assignment lifecycle: label audit/events,
manual dispatch attribution, engagement-mode metadata, previous-run cleanup,
agent templates, and a single current wake target.

Verification: focused parser, lease, and database router suites passed 17 tests; lint
passed with existing repository warnings; typecheck passed; and the full
Vitest suite passed 1,223 tests with 1 skip.

---

## 2026-07-14 — Deterministic lifecycle lab and operator journey audit

Added an isolated Lifecycle Lab for rapid, repeatable issue-lifecycle testing.
It uses the dedicated `forge_lifecycle` database, Redis database 14, port 3300,
and `.next-lifecycle` build output, with guarded deterministic fixtures for
ready, assigned/unacknowledged, active, waiting for user input, runtime
approval, review, completed, and stalled states. The lab disables its in-process
worker so those states remain frozen for inspection and supports both dev and
production-mode runs.

Added a four-journey Playwright audit that verifies Command Center continuity,
navigate-away/reload persistence, issue waiting and completion handoffs, the
Inbox, the shared notifications drawer, mobile overflow, screenshots, and axe
results. Unified ActionRequests and conversational agent waits into one
deduplicated “Needs your input” stream in both Inbox and the notification
drawer, included new ActionRequests in the Inbox badge, and made agent-authored
mentions robust when an agent API key shares the owner's author ID. Empty Inbox
buckets now stay hidden, Command Center attention cards use a four-column wide
layout, and landmark, heading, dialog, breadcrumb, and mention-combobox
semantics were repaired.

Verification: lint passed with existing repository warnings; typecheck passed;
the full Vitest suite passed 1,225 tests with 1 skip; a fresh production
Lifecycle Lab build passed; all 4 lifecycle Playwright journeys passed; and all
6 accepted desktop/mobile audit states reported zero axe violations (with
color-contrast excluded from automation and reviewed visually). The remaining
low-priority mobile observation is that the persistent agent-status pill can
cover card content while scrolling. The repository-wide browser run passed 36
of 38 tests in parallel; the two failures (shared chat state and a composer
locator exposed by the accessibility change) both passed independently after
the composer name was corrected.
72 changes: 44 additions & 28 deletions docs/agents/ai-triage-and-coach.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,17 @@ When enabled, two columns control routing:

### Provider matrix

| Provider | Default model | Env vars |
| --- | --- | --- |
| `hermes` | `claude-haiku-4-5-20251001` | `HERMES_GATEWAY_URL`, `HERMES_GATEWAY_TOKEN` |
| `openai` | `gpt-4o-mini` | `OPENAI_API_KEY`, `OPENAI_BASE_URL` |
| `anthropic` | `claude-haiku-4-5-20251001` | `ANTHROPIC_API_KEY` |
| `custom` | `gpt-4o-mini` | `FORGE_AI_BASE_URL`, `FORGE_AI_API_KEY` |
| Provider | Default model | Env vars |
| ----------- | --------------------------- | -------------------------------------------- |
| `hermes` | `claude-haiku-4-5-20251001` | `HERMES_GATEWAY_URL`, `HERMES_GATEWAY_TOKEN` |
| `openai` | `gpt-4o-mini` | `OPENAI_API_KEY`, `OPENAI_BASE_URL` |
| `anthropic` | `claude-haiku-4-5-20251001` | `ANTHROPIC_API_KEY` |
| `custom` | `gpt-4o-mini` | `FORGE_AI_BASE_URL`, `FORGE_AI_API_KEY` |

OpenAI, Anthropic, and custom providers can also use an enabled encrypted
`ProviderCredential` stored for the workspace. Stored credentials take
precedence over environment variables and are shared by Triage, Coach,
description assist, and plan generation.

`custom` exists for OpenAI-compatible endpoints that aren't OpenAI
themselves — Together, Groq, vLLM behind a proxy, a local LLM gateway, etc.
Expand All @@ -47,8 +52,8 @@ Suggests priority, labels, and an assignee on issue create.

### Knob

| Column | Purpose |
| --- | --- |
| Column | Purpose |
| ---------------------------- | -------------------------------------------------------------------------------------- |
| `Workspace.aiTriageOnCreate` | Default `true`. When `aiEnabled` is also true, every new issue triggers a triage call. |

### How it runs
Expand All @@ -66,8 +71,8 @@ The call loads four pieces of context:
capabilities, role).
- A small recent-issues sample (titles only) for tone calibration.

It then issues a structured tool-use call against a single tool named
`submit_triage`:
Direct OpenAI-compatible providers receive a structured tool-use call against
a single tool named `submit_triage`:

```ts
const tool = {
Expand All @@ -86,24 +91,33 @@ const tool = {
};
```

The tool input is validated, mapped against the workspace's actual labels
and agents, and persisted onto the issue:

| Column | Set to |
| --- | --- |
| `aiTriageStatus` | `READY` on success; `ERROR` on failure. Starts at `PENDING` while in flight. |
| `aiSuggestedPriority` | The model's `priority` choice. |
| `aiSuggestedLabelIds[]` | The model's `label_ids`, filtered to ids that exist in the workspace. |
| `aiSuggestedAgentId` | The model's `agent_id`, or null. |
| `aiTriageReasoning` | The model's free-text rationale. |
| `aiTriagedAt` | Timestamp of completion. |
| `aiTriageDecidedAt` | Set when a human applies or dismisses the suggestion. |
Hermes receives the same schema as a plain-JSON response contract. Forge does
not register `submit_triage` with Hermes because the gateway owns its own
server-side tool registry; sending a request-scoped function there would turn
the schema into an executable agent tool and fail as unknown.

The response is validated, mapped against the workspace's actual labels and
agents, and persisted onto the issue. Exact ids are preferred. Unique label
names, agent profile keys, and agent names are accepted as a compatibility
fallback; ambiguous names are ignored. Name-based assignment is only parsed
from an explicit agent/assignee clause, never from free-form reasoning.

| Column | Set to |
| ----------------------- | ---------------------------------------------------------------------------- |
| `aiTriageStatus` | `READY` on success; `ERROR` on failure. Starts at `PENDING` while in flight. |
| `aiSuggestedPriority` | The model's `priority` choice. |
| `aiSuggestedLabelIds[]` | The model's `label_ids`, filtered to ids that exist in the workspace. |
| `aiSuggestedAgentId` | The model's `agent_id`, or null. |
| `aiTriageReasoning` | The model's free-text rationale. |
| `aiTriagedAt` | Timestamp of completion. |
| `aiTriageDecidedAt` | Set when a human applies or dismisses the suggestion. |

### Idempotency

Triage skips any issue with `aiTriageStatus IS NOT NULL`. So if you
manually re-trigger triage you have to clear the status first; otherwise
the call is a no-op.
The runner atomically claims an issue by changing a null status to `PENDING`.
Concurrent create hooks or rerun requests therefore produce at most one model
call. The public rerun mutation clears a completed/failed/decided state before
starting a new claim and rejects a duplicate request while one is pending.

### What the user sees

Expand All @@ -112,7 +126,9 @@ proposed priority, labels, and agent, plus the reasoning. Two buttons:

- **Apply** — writes the suggested values onto the issue's actual
`priority`, `labels[]`, and `assignedAgentId`. Sets `aiTriageStatus =
APPLIED` and `aiTriageDecidedAt`.
APPLIED` and `aiTriageDecidedAt`. Label changes are audited, and agent
changes run the standard manual-assignment lifecycle (dispatch reason,
engagement mode, run cleanup, template application, and watcher wake target).
- **Dismiss** — leaves the issue alone. Sets `aiTriageStatus = DISMISSED`
and `aiTriageDecidedAt`.

Expand All @@ -125,8 +141,8 @@ Posts a diagnostic comment when work goes off-track.

### Knob

| Column | Purpose |
| --- | --- |
| Column | Purpose |
| -------------------------- | ------------------------------------------------------------------------ |
| `Workspace.aiCoachEnabled` | Default `true`. Requires a `COACH`-role agent to exist in the workspace. |

### How it runs
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "forge",
"version": "0.12.0",
"version": "0.13.0",
"private": true,
"description": "Forge — a fast, minimalist, keyboard-driven project management platform with pluggable agents.",
"license": "MIT",
Expand All @@ -12,6 +12,7 @@
"dev:live:lan": "./scripts/dev-live-stack.sh --lan",
"dev:isolated": "next dev --turbo",
"dev:local": "./scripts/dev-local.sh",
"dev:lifecycle": "./scripts/lifecycle-lab.sh",
"dev:host": "DEV_HOST=0.0.0.0 ./scripts/dev-live.sh",
"db:clone-prod": "./scripts/db-clone-prod.sh",
"build": "./scripts/build-docs.sh && next build",
Expand All @@ -24,6 +25,7 @@
"prisma:migrate": "prisma migrate dev",
"prisma:deploy": "prisma migrate deploy",
"prisma:seed": "tsx prisma/seed.ts",
"seed:lifecycle": "FORGE_LIFECYCLE_LAB=1 tsx scripts/seed-lifecycle-lab.ts",
"seed:agents": "tsx scripts/seed-agents.ts",
"prisma:studio": "prisma studio",
"test": "vitest run",
Expand All @@ -43,7 +45,8 @@
"forge": "node tools/forge-cli/dist/index.js",
"changelog": "tsx tools/scripts/changelog-scaffold.ts",
"e2e": "fuser -k 3200/tcp 2>/dev/null; E2E_FORCE_BUILD=1 playwright test",
"e2e:web": "bash scripts/e2e-web.sh"
"e2e:web": "bash scripts/e2e-web.sh",
"test:lifecycle": "playwright test --config=playwright.lifecycle.config.ts"
},
"dependencies": {
"@auth/prisma-adapter": "^2.7.4",
Expand Down
32 changes: 32 additions & 0 deletions playwright.lifecycle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { defineConfig, devices } from "@playwright/test";

const PORT = Number(process.env.LIFECYCLE_PORT ?? 3300);
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? `http://localhost:${PORT}`;

export default defineConfig({
testDir: "./tests/lifecycle",
fullyParallel: false,
workers: 1,
retries: 0,
reporter: [["list"], ["html", { open: "never", outputFolder: "playwright-report-lifecycle" }]],
globalSetup: "./tests/e2e/global-setup.ts",
outputDir: "test-results/lifecycle",
timeout: 90_000,
expect: { timeout: 15_000 },
use: {
baseURL: BASE_URL,
storageState: "tests/e2e/.auth/owner.json",
trace: "retain-on-failure",
screenshot: "only-on-failure",
video: "off",
},
projects: [{ name: "chromium", use: devices["Desktop Chrome"] }],
webServer: {
command: "bash scripts/lifecycle-lab.sh --fresh --production",
url: BASE_URL,
reuseExistingServer: !process.env.CI,
timeout: 420_000,
stdout: "pipe",
stderr: "pipe",
},
});
97 changes: 97 additions & 0 deletions scripts/lifecycle-lab.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Deterministic issue-lifecycle lab for product iteration and browser audits.
# Uses its own Postgres database, Redis logical DB, Next build directory, and
# port. It cannot read or mutate production or the shared dev:local database.
#
# pnpm dev:lifecycle --fresh # HMR server at http://localhost:3300
# pnpm dev:lifecycle # reseed lab scenarios, preserve schema
# pnpm dev:lifecycle --production # deterministic next start (Playwright)

set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
COMPOSE_FILE="$ROOT/docker/docker-compose.yml"
PORT="${LIFECYCLE_PORT:-3300}"
FRESH=0
PRODUCTION=0

for arg in "$@"; do
case "$arg" in
--fresh) FRESH=1 ;;
--production) PRODUCTION=1 ;;
*) echo "[lifecycle] Unknown arg: $arg" >&2; exit 1 ;;
esac
done

# Deliberately ignore inherited application URLs. A shell pointed at staging or
# production must still land on the disposable local lab stores.
export DATABASE_URL="postgresql://forge:forge@localhost:55432/forge_lifecycle?schema=public"
export REDIS_URL="redis://localhost:56379/14"
export S3_ENDPOINT="http://localhost:59000"
export S3_PUBLIC_ENDPOINT="http://localhost:59000"
export S3_REGION="${S3_REGION:-us-east-1}"
export S3_ACCESS_KEY="${S3_ACCESS_KEY:-forgeminio}"
export S3_SECRET_KEY="${S3_SECRET_KEY:-forgeminio-dev-password}"
export S3_FORCE_PATH_STYLE="true"

export AUTH_SECRET="${AUTH_SECRET:-lifecycle-lab-secret-changeme-32bytes}"
export AUTH_URL="http://localhost:${PORT}"
export NEXT_PUBLIC_APP_URL="$AUTH_URL"
export AUTH_TRUST_HOST="true"
export ADMIN_EMAIL="${ADMIN_EMAIL:-owner@forge.local}"
export ADMIN_PASSWORD="${ADMIN_PASSWORD:-forge-dev}"
export ADMIN_NAME="${ADMIN_NAME:-Forge Owner}"
export ADMIN_HANDLE="${ADMIN_HANDLE:-forge}"
export PLUGIN_JWT_SECRET="${PLUGIN_JWT_SECRET:-lifecycle-plugin-signing-key}"
export PLUGIN_JWT_ISSUER="${PLUGIN_JWT_ISSUER:-forge}"
export PLUGIN_JWT_AUDIENCE="${PLUGIN_JWT_AUDIENCE:-forge-plugins}"

# Reuse the in-process mock runtime, but keep the lab opt-in independently.
export FORGE_E2E=1
export FORGE_LIFECYCLE_LAB=1
# The lab is a frozen UX state matrix. Background dispatch/watchdog workers
# would correctly advance those states, which would make visual audits racey.
export FORGE_DISABLE_IN_PROCESS_WORKER=1
export NEXT_DIST_DIR=".next-lifecycle"

echo "[lifecycle] Ensuring isolated docker stack is up…"
docker compose -f "$COMPOSE_FILE" up -d >/dev/null

echo "[lifecycle] Waiting for Postgres…"
for i in $(seq 1 30); do
docker compose -f "$COMPOSE_FILE" exec -T postgres pg_isready -U forge >/dev/null 2>&1 && break
[[ "$i" == "30" ]] && { echo "[lifecycle] Postgres not ready" >&2; exit 1; }
sleep 1
done

docker compose -f "$COMPOSE_FILE" exec -T postgres psql -U forge -d forge -tAc \
"SELECT 1 FROM pg_database WHERE datname='forge_lifecycle'" 2>/dev/null | grep -q 1 \
|| docker compose -f "$COMPOSE_FILE" exec -T postgres createdb -U forge forge_lifecycle

if [[ "$FRESH" == "1" ]]; then
echo "[lifecycle] --fresh: recreating the lab schema…"
docker compose -f "$COMPOSE_FILE" exec -T postgres \
psql -U forge -d forge_lifecycle -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;" >/dev/null 2>&1
fi

echo "[lifecycle] Applying migrations and deterministic fixtures…"
pnpm exec prisma migrate deploy >/dev/null
pnpm exec prisma generate >/dev/null
pnpm exec tsx prisma/seed.ts >/dev/null
pnpm exec tsx scripts/seed-lifecycle-lab.ts

echo
echo "[lifecycle] Lab: http://localhost:${PORT}/w/forge/command-center"
echo "[lifecycle] Sign in: $ADMIN_EMAIL / $ADMIN_PASSWORD"
echo "[lifecycle] Data: forge_lifecycle · Redis /14 · $NEXT_DIST_DIR"
echo

if [[ "$PRODUCTION" == "1" ]]; then
if [[ "${LIFECYCLE_FORCE_BUILD:-0}" == "1" || ! -f "$ROOT/$NEXT_DIST_DIR/BUILD_ID" ]]; then
echo "[lifecycle] Building deterministic production bundle…"
pnpm exec next build
fi
exec pnpm exec next start -p "$PORT"
fi

exec pnpm exec next dev --turbo -p "$PORT"
Loading
Loading