Skip to content
Closed
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
61 changes: 61 additions & 0 deletions .agent/context/20260909T111228Z-gate-p5-frontend-acceptance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Session Context: Gate P5 Frontend Acceptance

## Date/time

- UTC: 2026-09-09T11:12:28Z

## User goal

Implement project Gate P5 according to `plan.md`, after ensuring the Recovery
Agent decision exists before frontend Graph MCP composition.

## Key decisions

- Compose A05/B05/C05 in the A-owned shell using the configured frozen API.
- Return the latest persisted Recovery Agent and deterministic-core result from
the recovery view; never hard-code an Agent decision.
- Keep Subgraph MCP and model output advisory with settlement permission
`NEVER`.
- Hide unsupported operator escalation in production instead of simulating a
successful external effect.
- Scope B05/C05 CSS at package boundaries to prevent cross-slice overrides.

## Files/components touched

- `apps/web`: composed shell, recovery API projection, Playwright acceptance,
responsive/token safety checks.
- `packages/contracts`: additive recovery decision and Graph observation view.
- `packages/reconciliation`: persist Agent boundary acceptance and explanation.
- `packages/storage-postgres`: project latest durable recovery command pack.
- `packages/recovery-ui`, `packages/settlement-ui`: scoped composition styles;
recovery escalation capability flag.
- CI, plan/status documentation, and Gate P5 evidence.

## Commands/checks

- Full workspace build/test: 58 files and 904 tests PASS.
- Lint, typecheck, generated contracts, fixtures: PASS.
- Web unit/component: 31 PASS.
- Reconciliation: 84 PASS.
- Storage unit: 8 PASS.
- Playwright Chromium: 4 PASS.
- Cloud Run `/health/ready`: HTTP 200, `{"status":"ok"}`.
- Docker is unavailable locally; PostgreSQL integration test is added for CI.

## Git and PR state

- Branch: `feat/gate-p5-frontend-acceptance`
- Base: `origin/develop` at `779c6cf`
- Commit: uncommitted
- PR: not created
- CI: not run

## Review gates

- Gate A: NOT RUN
- Gate B: NOT RUN

## Handoff/next steps

1. Run full validation and FreePi Gate A.
2. Push a draft PR, await exact-head CI, run Gate B, and hand to a human.
8 changes: 8 additions & 0 deletions .github/workflows/stack-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ jobs:
if: steps.workspace.outputs.enabled == 'true'
run: pnpm test

- name: Install Chromium
if: steps.workspace.outputs.enabled == 'true'
run: pnpm --filter @oneshot/web exec playwright install --with-deps chromium

- name: Run Gate P5 browser acceptance
if: steps.workspace.outputs.enabled == 'true'
run: pnpm --filter @oneshot/web test:browser

- name: Run PostgreSQL integration tests
if: steps.workspace.outputs.enabled == 'true'
env:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ dist/
site-dist/
build/
coverage/
playwright-report/
test-results/
node_modules/
.venv/
venv/
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,9 @@ Under active development. **Testnet only.**
| --------------------------------------------- | ----------------------------------------------------------------------------------- |
| Durable intent ledger, API, worker | Implemented |
| Settlement adapters and error taxonomy | Implemented; simulator-tested and live-verified on Arc Testnet through Privy |
| Recovery evidence and safety core | Implemented against simulators |
| Subgraph MCP discovery and LLM recovery agent | Implemented boundary; live path not verified |
| Operator frontend | Intent/status UI and synthetic recovery viewer implemented; live API wiring pending |
| Recovery evidence and safety core | Live Graph/Vertex path implemented; deterministic core remains authoritative |
| Subgraph MCP discovery and LLM recovery agent | Live Subgraph MCP and Vertex AI path verified; deterministic core remains final |
| Operator frontend | Gate P5 candidate composes A05/B05/C05 against the frozen API with browser coverage |

**One live testnet settlement has been executed.** A Privy-controlled execution
wallet and scoped policy authorized one 1.00 USDC Arc Testnet transfer; live
Expand All @@ -213,7 +213,7 @@ drill entered `UNKNOWN` and reconciled to that original settlement without a
replacement payment. Privy and Arc are `QUALIFIED` for the documented testnet
claim; see `docs/settlement/LIVE_EVIDENCE.md` and
`packages/reconciliation/docs/c06/QUALIFICATION_REPORT.md`. The Graph live
Subgraph MCP and recovery-agent path remains `NOT VERIFIED`.
Subgraph MCP and recovery-agent path is also `QUALIFIED` by the latter report.

Arc Mainnet is not configured. Its profile carries no chain ID, RPC, explorer,
or token value by design, and enabling it requires published official values
Expand Down
14 changes: 12 additions & 2 deletions apps/web/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# OneShot web

Minimal operator UI for creating or replaying a Business Intent and reading its authoritative status.
Gate P5 operator UI composing intent creation/status, Privy and Arc settlement
details, and Recovery Agent/Subgraph MCP evidence.

The Cloudflare asset deployment serves this app at the domain root and the
recovery fixture viewer from `@oneshot/recovery-ui` at `/recovery/`. Deploy it
Expand All @@ -22,4 +23,13 @@ pnpm build:frontend
This emits the main app to `apps/web/dist` and the recovery viewer to
`apps/web/dist/recovery`, matching the Wrangler asset directory.

The client consumes generated `@oneshot/contracts` types from frozen OpenAPI v1. Tests use deterministic fetch responses matching that contract.
The clients consume generated `@oneshot/contracts` types from frozen OpenAPI v1.
The service token remains in React memory and is never written to browser
storage. Run the Chromium acceptance suite with:

```powershell
pnpm --filter @oneshot/web test:browser
```

See [`../../docs/GATE_P5_CHECKLIST.md`](../../docs/GATE_P5_CHECKLIST.md) for the
covered states and safety boundary.
2 changes: 1 addition & 1 deletion apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OneShot — Intent & Authoritative Status</title>
<title>OneShot — Operator Control</title>
</head>
<body>
<div id="root"></div>
Expand Down
6 changes: 5 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@
"build": "tsc -b && vite build",
"clean": "tsc -b --clean",
"dev": "vite",
"lint": "eslint src test",
"lint": "eslint src test playwright.config.ts",
"preview": "vite preview",
"test": "vitest run --config vitest.config.ts",
"test:browser": "playwright test",
"typecheck": "tsc -b --pretty false"
},
"dependencies": {
"@oneshot/contracts": "workspace:*",
"@oneshot/recovery-ui": "workspace:*",
"@oneshot/settlement-ui": "workspace:*",
"react": "19.2.8",
"react-dom": "19.2.8"
},
"devDependencies": {
"@playwright/test": "1.63.0",
"@testing-library/react": "16.3.3",
"@testing-library/user-event": "14.6.7",
"@types/node": "24.13.3",
Expand Down
18 changes: 18 additions & 0 deletions apps/web/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './test',
testMatch: '**/*.spec.ts',
fullyParallel: true,
reporter: 'line',
use: {
baseURL: 'http://127.0.0.1:4173',
trace: 'retain-on-failure',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: {
command: 'pnpm dev --host 127.0.0.1 --port 4173',
url: 'http://127.0.0.1:4173',
reuseExistingServer: false,
},
});
124 changes: 90 additions & 34 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,68 @@
import { useMemo, useState } from 'react';
import {
createSettlementClient,
SettlementDetailsRoute,
type SettlementClient,
} from '@oneshot/settlement-ui';
import { RecoveryRoute, type RecoveryClient } from '@oneshot/recovery-ui';
import '@oneshot/recovery-ui/styles.css';
import '@oneshot/settlement-ui/styles.css';

import { OneShotApiClient } from './api/client.js';
import { createApiRecoveryClient } from './api/recovery-client.js';
import { ErrorBoundary } from './components/ErrorBoundary.js';
import { IntentForm } from './components/IntentForm.js';
import { IntentStatusView } from './components/IntentStatusView.js';
import { ReadinessBanner } from './components/ReadinessBanner.js';
import './styles.css';

type Tab = 'create' | 'status';
type Tab = 'create' | 'status' | 'settlement' | 'recovery';

export function App() {
export interface AppProps {
readonly apiClient?: OneShotApiClient;
readonly settlementClient?: SettlementClient;
readonly recoveryClient?: RecoveryClient;
}

const TABS: readonly { readonly id: Tab; readonly label: string }[] = [
{ id: 'create', label: 'Create or replay' },
{ id: 'status', label: 'Authoritative status' },
{ id: 'settlement', label: 'Settlement details' },
{ id: 'recovery', label: 'Recovery evidence' },
];

export function App(props: AppProps = {}) {
const [activeTab, setActiveTab] = useState<Tab>('create');
const [selectedIntentId, setSelectedIntentId] = useState('');
const [authToken, setAuthToken] = useState('');
const client = useMemo(
const apiBaseUrl = import.meta.env.VITE_ONESHOT_API_BASE_URL ?? '';
const apiClient = useMemo(
() =>
props.apiClient ??
new OneShotApiClient({ baseUrl: apiBaseUrl, getAuthToken: () => authToken.trim() || null }),
[apiBaseUrl, authToken, props.apiClient],
);
const settlementClient = useMemo(
() =>
props.settlementClient ??
createSettlementClient({
baseUrl: apiBaseUrl,
getAuthToken: () => authToken.trim() || null,
}),
[apiBaseUrl, authToken, props.settlementClient],
);
const recoveryClient = useMemo(
() =>
new OneShotApiClient({
baseUrl: import.meta.env.VITE_ONESHOT_API_BASE_URL ?? '',
props.recoveryClient ??
createApiRecoveryClient({
baseUrl: apiBaseUrl,
getAuthToken: () => authToken.trim() || null,
}),
[authToken],
[apiBaseUrl, authToken, props.recoveryClient],
);

function showStatus(intentId: string): void {
function selectIntent(intentId: string): void {
setSelectedIntentId(intentId);
setActiveTab('status');
}

return (
Expand All @@ -34,7 +72,7 @@ export function App() {
<p className="eyebrow">ONESHOT / ARC TESTNET</p>
<h1>One job. Many retries. One settlement.</h1>
<p>Create a stable payment intent and follow its authoritative state.</p>
<ReadinessBanner client={client} />
<ReadinessBanner client={apiClient} />
</header>

<section className="auth-bar" aria-label="Service authorization">
Expand All @@ -49,34 +87,52 @@ export function App() {
<small>Memory only. Sent as Bearer authorization.</small>
</section>

<nav className="tabs" aria-label="Application sections" role="tablist">
<button
type="button"
role="tab"
aria-selected={activeTab === 'create'}
aria-controls="create-panel"
onClick={() => setActiveTab('create')}
>
Create or replay
</button>
<button
type="button"
role="tab"
aria-selected={activeTab === 'status'}
aria-controls="status-panel"
onClick={() => setActiveTab('status')}
>
Authoritative status
</button>
<section className="intent-context" aria-label="Selected business intent">
<label htmlFor="selected-intent-input">Active Business Intent ID</label>
<input
id="selected-intent-input"
value={selectedIntentId}
onChange={(event) => setSelectedIntentId(event.target.value)}
placeholder="Create an intent or enter its stable ID"
/>
</section>

<nav className="tabs" aria-label="Application sections">
{TABS.map((tab) => (
<button
key={tab.id}
type="button"
aria-pressed={activeTab === tab.id}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</nav>

<main id={activeTab === 'create' ? 'create-panel' : 'status-panel'} role="tabpanel">
{activeTab === 'create' ? (
<IntentForm client={client} onIntentCreatedOrSelected={showStatus} />
) : (
<IntentStatusView client={client} initialIntentId={selectedIntentId} />
<div>
{activeTab === 'create' && (
<IntentForm client={apiClient} onIntentCreatedOrSelected={selectIntent} />
)}
{activeTab === 'status' && (
<IntentStatusView client={apiClient} initialIntentId={selectedIntentId} />
)}
</main>
{activeTab === 'settlement' &&
(selectedIntentId.trim() ? (
<SettlementDetailsRoute
businessIntentId={selectedIntentId.trim()}
client={settlementClient}
/>
) : (
<p className="notice">Enter a Business Intent ID to read settlement details.</p>
))}
{activeTab === 'recovery' &&
(selectedIntentId.trim() ? (
<RecoveryRoute businessIntentId={selectedIntentId.trim()} client={recoveryClient} />
) : (
<p className="notice">Enter a Business Intent ID to read recovery evidence.</p>
))}
</div>
</div>
</ErrorBoundary>
);
Expand Down
Loading
Loading