Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
## Validation

- [ ] `npm test`
- [ ] `npm run test:e2e`
- [ ] `npm run lint`
- [ ] `npm run typecheck`
- [ ] `npm run build`
Expand Down
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ jobs:
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 22
cache: npm
Expand All @@ -37,6 +37,9 @@ jobs:
- name: Test
run: npm test

- name: End-to-end test
run: npm run test:e2e

- name: Lint
run: npm run lint

Expand Down
17 changes: 15 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,26 @@ All notable changes to HiveForge are documented here.

## [Unreleased]

- Continue the V0.1 local-first MVP toward a production-readiness review.
- Reconcile the authoritative Master Source V0.1.4 when it is supplied.

## [0.1.0-alpha.2]

- Close the real Codex turn-to-validation-to-review loop with durable SQLite turn state.
- Align the adapter with codex-cli 0.152.0 App Server stdio, account/read and turn/completed.
- Add canonical RCP v0.1, immutable base/head Git evidence, validation persistence, recovery fencing, dynamic mailbox watchers and cross-process SSE replay.
- Add dashboard evidence, integration coverage and a real Codex App Server smoke path.

## [0.1.0-alpha.3]

- Keep the canonical Review ZIP finalizer compatible with Windows PowerShell 5.1.

## [0.1.0-alpha.1]

- Establish the GitHub professional baseline for the V0.1 MVP.
- Add CI, contribution guidance, security reporting, issue forms, and release governance.
- Add a Windows-first execution finalizer for repeatable validation and publication.

[Unreleased]: https://github.com/KayzenRoot/hiveforge/compare/v0.1.0-alpha.1...HEAD
[Unreleased]: https://github.com/KayzenRoot/hiveforge/compare/v0.1.0-alpha.2...HEAD
[0.1.0-alpha.1]: https://github.com/KayzenRoot/hiveforge/releases/tag/v0.1.0-alpha.1
[0.1.0-alpha.2]: https://github.com/KayzenRoot/hiveforge/releases/tag/v0.1.0-alpha.2
[0.1.0-alpha.3]: https://github.com/KayzenRoot/hiveforge/releases/tag/v0.1.0-alpha.3
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Install dependencies with `npm ci`, then run the full validation set before open

```powershell
npm test
npm run test:e2e
npm run lint
npm run typecheck
npm run build
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ npm run worker

```powershell
npm test
npm run test:e2e
npm run lint
npm run typecheck
npm run build
Expand Down
23 changes: 20 additions & 3 deletions app/api/events/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { RunEvent } from "../../../src/core/types";
export const runtime = "nodejs";

function encode(event: RunEvent | Record<string, unknown>): Uint8Array {
return new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`);
return new TextEncoder().encode("data: " + JSON.stringify(event) + "\n\n");
}

export function GET(request: NextRequest) {
Expand All @@ -17,17 +17,34 @@ export function GET(request: NextRequest) {

let unsubscribe: () => void = () => undefined;
let heartbeat: ReturnType<typeof setInterval> | undefined;
let poller: ReturnType<typeof setInterval> | undefined;
let closed = false;
let lastCreatedAt = "";
let lastEventId = "";
const seen = new Set<string>();

const stream = new ReadableStream<Uint8Array>({
start(controller) {
for (const event of db.listEvents(runId)) controller.enqueue(encode(event));
unsubscribe = events.subscribe(runId, (event) => { if (!closed) controller.enqueue(encode(event)); });
const enqueue = (event: RunEvent) => {
if (closed || seen.has(event.id)) return;
seen.add(event.id);
lastCreatedAt = event.createdAt;
lastEventId = event.id;
controller.enqueue(encode(event));
};
for (const event of db.listEvents(runId, 250)) enqueue(event);
unsubscribe = events.subscribe(runId, enqueue);
poller = setInterval(() => {
if (closed) return;
for (const event of db.listEventsAfter(runId, lastCreatedAt, lastEventId, 250)) enqueue(event);
}, 750);
heartbeat = setInterval(() => { if (!closed) controller.enqueue(new TextEncoder().encode(": heartbeat\n\n")); }, 15_000);
},
cancel() {
closed = true;
unsubscribe();
if (heartbeat) clearInterval(heartbeat);
if (poller) clearInterval(poller);
}
});
return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", Connection: "keep-alive" } });
Expand Down
3 changes: 2 additions & 1 deletion app/api/projects/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export function GET() {
const { db } = getRuntime();
const projects = db.listProjects().map((project) => {
const run = db.getLatestRun(project.id);
return { ...project, latestRun: run ? { id: run.id, state: run.state, iterationCount: run.iterationCount, updatedAt: run.updatedAt } : undefined };
const overview = run ? db.getRunOverview(run.id) : undefined;
return { ...project, latestRun: overview ?? undefined };
});
return NextResponse.json({ projects });
}
Expand Down
12 changes: 12 additions & 0 deletions app/api/runs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ import type { AutonomyMode } from "../../../src/core/types";

export const runtime = "nodejs";

export function GET(request: NextRequest) {
const { db } = getRuntime();
const runId = request.nextUrl.searchParams.get("runId");
const projectId = request.nextUrl.searchParams.get("projectId");
if (runId) {
const overview = db.getRunOverview(runId);
return overview ? NextResponse.json({ overview }) : NextResponse.json({ error: "Run not found" }, { status: 404 });
}
if (projectId) return NextResponse.json({ run: db.getLatestRun(projectId) ?? null });
return NextResponse.json({ error: "runId or projectId is required" }, { status: 400 });
}

export async function POST(request: NextRequest) {
const { db, codex, engine } = getRuntime();
const body = await request.json() as { projectId?: string; prompt?: string };
Expand Down
11 changes: 7 additions & 4 deletions app/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import { FormEvent, useCallback, useEffect, useMemo, useState } from "react";

type Health = { status: string; codex: { available: boolean; version: string | null; appServer: boolean; authentication: string; reason?: string } };
type Project = { id: string; name: string; localPath: string; autonomyMode: string; workingBranch: string; reviewMailboxPath: string; latestRun?: { id: string; state: string; iterationCount: number; updatedAt: string } };
type Validation = { kind: string; command: string | null; status: string; exitCode: number | null; stdout: string; stderr: string };
type RunOverview = { run: { id: string; workOrderId: string; state: string; autonomyMode: string; iterationCount: number; progressPercent: number; currentBlocker: string | null; runBaseSha: string | null; runBaseBranch: string | null; expectedBaseSha: string | null; expectedHeadSha: string | null; lastReviewStatus: string | null; lastReviewVerdict: string | null; lastReviewSummary: string | null; lastCheckpointNote: string | null; updatedAt: string }; gitEvidence: { baseSha: string; headSha: string; branch: string; status: string; isClean: boolean; changedFiles: string[]; diffSummary: string } | null; validations: Validation[]; review: { status: string; review: { reviewId: string; verdict: string; summary: string; progressPercent: number; checkpointNote: string } } | null };
type Project = { id: string; name: string; localPath: string; autonomyMode: string; workingBranch: string; reviewMailboxPath: string; latestRun?: RunOverview };
type RunEvent = { id: string; type: string; state: string | null; message: string; createdAt: string };

const modeCopy: Record<string, string> = {
Expand All @@ -22,7 +24,8 @@ export default function Dashboard() {
const [form, setForm] = useState({ name: "", localPath: "", defaultBranch: "main", workingBranch: "main", testCommand: "npm test", autonomyMode: "AUTONOMOUS", reviewMailboxPath: "" });

const selected = useMemo(() => projects.find((project) => project.id === selectedId) ?? projects[0], [projects, selectedId]);
const latestRun = selected?.latestRun;
const overview = selected?.latestRun;
const latestRun = overview?.run;

const load = useCallback(async () => {
const [projectsResponse, healthResponse] = await Promise.all([fetch("/api/projects"), fetch("/api/health")]);
Expand Down Expand Up @@ -101,10 +104,10 @@ export default function Dashboard() {
<section className="workspace-grid">
<aside className="panel projects-panel"><div className="panel-heading"><div><p className="eyebrow">REGISTRY</p><h2>Projects</h2></div><button className="small-button" onClick={() => setShowForm((value) => !value)}>+ Add</button></div>
{showForm && <form className="project-form" onSubmit={createProject}><input required placeholder="Project name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} /><input required placeholder="Absolute local path" value={form.localPath} onChange={(e) => setForm({ ...form, localPath: e.target.value })} /><input placeholder="Mailbox path (optional)" value={form.reviewMailboxPath} onChange={(e) => setForm({ ...form, reviewMailboxPath: e.target.value })} /><select value={form.autonomyMode} onChange={(e) => setForm({ ...form, autonomyMode: e.target.value })}><option>AUTONOMOUS</option><option>GUARDED</option><option>CONTROLLED</option></select><button className="primary-button" type="submit">Register project</button></form>}
<div className="project-list">{projects.length === 0 ? <div className="empty-state"><span>◎</span><strong>No local projects yet</strong><p>Register a Git repository to give the loop a home.</p></div> : projects.map((project) => <button className={`project-card ${selected?.id === project.id ? "is-selected" : ""}`} key={project.id} onClick={() => setSelectedId(project.id)}><span className="project-glyph">{project.name.slice(0, 1).toUpperCase()}</span><span className="project-card-copy"><strong>{project.name}</strong><small>{project.localPath}</small></span><span className="project-state">{project.latestRun?.state ?? "READY"}</span></button>)}</div>
<div className="project-list">{projects.length === 0 ? <div className="empty-state"><span>◎</span><strong>No local projects yet</strong><p>Register a Git repository to give the loop a home.</p></div> : projects.map((project) => <button className={`project-card ${selected?.id === project.id ? "is-selected" : ""}`} key={project.id} onClick={() => setSelectedId(project.id)}><span className="project-glyph">{project.name.slice(0, 1).toUpperCase()}</span><span className="project-card-copy"><strong>{project.name}</strong><small>{project.localPath}</small></span><span className="project-state">{project.latestRun?.run.state ?? "READY"}</span></button>)}</div>
</aside>

<div className="main-column"><section className="panel run-panel"><div className="panel-heading"><div><p className="eyebrow">ACTIVE WORK ORDER</p><h2>{selected ? selected.name : "No project selected"}</h2></div><span className={`status-pill status-${status.toLowerCase()}`}><i />{status}</span></div><div className="run-meta"><div><span>Autonomy</span><strong>{selected ? modeCopy[selected.autonomyMode] : "—"}</strong></div><div><span>Branch</span><strong>{selected?.workingBranch ?? "—"}</strong></div><div><span>Work order</span><strong>{latestRun ? latestRun.id.slice(0, 12) : "Awaiting run"}</strong></div></div><div className="run-actions"><button className="primary-button" disabled={!selected || health?.status !== "READY" || Boolean(latestRun && !["COMPLETE", "FAILED", "BLOCKED"].includes(latestRun.state))} onClick={() => void startRun()}>Start run <span>↗</span></button><button disabled={!latestRun || !["QUEUED", "CODEX_RUNNING", "WAITING_REVIEW", "WAITING_APPROVAL"].includes(status)} onClick={() => void controlRun("pause")}>Pause</button><button disabled={!latestRun || status !== "PAUSED"} onClick={() => void controlRun("resume")}>Resume</button><button disabled={!latestRun || ["COMPLETE", "FAILED"].includes(status)} onClick={() => void controlRun("stop")}>Stop</button></div>{message && <p className="inline-message">{message}</p>}</section>
<div className="main-column"><section className="panel run-panel"><div className="panel-heading"><div><p className="eyebrow">ACTIVE WORK ORDER</p><h2>{selected ? selected.name : "No project selected"}</h2></div><span className={`status-pill status-${status.toLowerCase()}`}><i />{status}</span></div><div className="run-meta"><div><span>Autonomy</span><strong>{selected ? modeCopy[selected.autonomyMode] : "—"}</strong></div><div><span>Branch</span><strong>{latestRun?.runBaseBranch ?? selected?.workingBranch ?? "—"}</strong></div><div><span>Work order</span><strong>{latestRun?.workOrderId ?? "Awaiting run"}</strong></div></div><div className="run-actions"><button className="primary-button" disabled={!selected || health?.status !== "READY" || Boolean(latestRun && !["COMPLETE", "FAILED", "BLOCKED"].includes(latestRun.state))} onClick={() => void startRun()}>Start run <span>↗</span></button><button disabled={!latestRun || !["QUEUED", "CODEX_RUNNING", "WAITING_REVIEW", "WAITING_APPROVAL"].includes(status)} onClick={() => void controlRun("pause")}>Pause</button><button disabled={!latestRun || status !== "PAUSED"} onClick={() => void controlRun("resume")}>Resume</button><button disabled={!latestRun || ["COMPLETE", "FAILED"].includes(status)} onClick={() => void controlRun("stop")}>Stop</button></div>{overview && <div className="evidence-strip"><div><span>Git evidence</span><strong>{overview.gitEvidence?.isClean ? "CLEAN" : overview.gitEvidence ? "DIRTY" : "—"}</strong><small>{overview.gitEvidence?.headSha?.slice(0, 12) ?? "not captured"}</small></div><div><span>Validation</span><strong>{overview.validations.length ? overview.validations.map((item) => item.kind + ":" + item.status).join(" · ") : "pending"}</strong><small>{overview.review ? overview.review.status + " / " + overview.review.review.verdict : "review pending"}</small></div><div><span>Progress</span><strong>{latestRun?.progressPercent ?? 0}%</strong><small>{latestRun?.currentBlocker ?? latestRun?.lastCheckpointNote ?? "No active blocker"}</small></div></div>}{message && <p className="inline-message">{message}</p>}</section>
<section className="panel timeline-panel"><div className="panel-heading"><div><p className="eyebrow">PERSISTED EVENT STREAM</p><h2>Timeline</h2></div><span className="live-badge"><i /> live</span></div><div className="timeline">{events.length === 0 ? <div className="empty-timeline">Events will appear here as the worker moves the run.</div> : events.slice().reverse().map((event) => <div className="timeline-item" key={event.id}><span className="timeline-rail" /><div><div className="timeline-top"><strong>{event.type.replaceAll("_", " ")}</strong><time>{new Date(event.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</time></div><p>{event.message}</p>{event.state && <small>{event.state}</small>}</div></div>)}</div></section></div>
</section>
<footer className="footer"><span>HIVEFORGE / runtime truth lives in SQLite</span><span>{health?.codex.version ?? "codex —"} · evidence lock enabled</span></footer>
Expand Down
1 change: 1 addition & 0 deletions app/globals.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions docs/CHECKPOINT.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Checkpoint — Prompt 001
# Checkpoint — Prompt 001B

HiveForge V0.1 has a local Next.js dashboard, SQLite runtime truth, explicit workflow transitions, Git evidence capture, RCP validation, mailbox consumption, a real Codex App Server JSONL adapter, FakeCodexAdapter tests, SSE event streaming and restart recovery checks.
Status: CORRECTION_IN_PROGRESS

The authoritative Master Source was not supplied. The repository keeps a clearly marked bootstrap note at `docs/HIVEFORGE-MASTER-SOURCE-V0.1.md`; reconcile it before starting the next increment.
The closed loop now has a production completion path: Codex terminal turn events advance into persisted validation, immutable Git evidence, canonical RCP validation, Evidence Lock, autonomy gates and durable next dispatches. SQLite-backed event replay and dynamic mailbox reconciliation cover process boundaries and newly registered projects.

Validation completed on 2026-09-01: tests, E2E, lint, typecheck and production build passed. Proposed checkpoint: `READY_FOR_SOL_AUDIT`.
The authoritative HIVEFORGE-MASTER-SOURCE-V0.1.4.md required by the 001B governance finding was not present in the supplied Downloads folder or repository. It was not invented or replaced with a local approximation. The checkpoint therefore remains blocked until the authoritative source is supplied and reconciled.

Functional correction checkpoint: READY_FOR_SOURCE_RECONCILIATION.
28 changes: 28 additions & 0 deletions docs/CODEX-APP-SERVER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Codex App Server integration

The correction increment was verified against the installed `codex-cli 0.152.0`.

Regenerate the official protocol artifacts locally with:

```powershell
.\scripts\verify-codex-protocol.ps1
```

The adapter uses the official stdio lifecycle:

- launch: `codex app-server` (stdio is the default);
- initialization: `initialize`, `initialized`;
- authentication/account: `account/read` with `refreshToken: false`;
- thread: `thread/start` with `cwd` and `ephemeral`;
- turn: `turn/start` with `threadId`, `input` and `cwd`;
- terminal event: `turn/completed`, mapped only when the returned turn status is `completed`, `failed` or `interrupted`.

No legacy `getAuthStatus` call, undocumented project field or raw authentication token is used. Every returned turn ID is persisted before the run can advance, and a missing ID blocks the dispatch.

The real smoke command is:

```powershell
npx tsx scripts/codex-smoke.ts
```

It uses a temporary Git repository and asks Codex to reply without modifying files.
2 changes: 1 addition & 1 deletion docs/HIVEFORGE-GITHUB-GOVERNANCE-V0.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The approved bootstrap may land directly on an empty `main`. All later changes u

## Required validation

The CI workflow runs `npm ci`, `npm test`, `npm run lint`, `npm run typecheck`, and `npm run build` on pushes to `main` and work branches and on pull requests targeting `main`. A configured command failing blocks acceptance.
The CI workflow runs `npm ci`, `npm test`, `npm run test:e2e`, `npm run lint`, `npm run typecheck`, and `npm run build` on pushes to `main` and work branches and on pull requests targeting `main`. A configured command failing blocks acceptance.

## Protection and permissions

Expand Down
Loading