v3 workflow: intent, phases, review, submission, enforcement, burn-in
The v3 stack, previously an uncommitted working tree, plus this session's two units and the burn-in instrument. This commit is the burn-in build identity: coordinator and worker must both report this revision before a task is created. Workflow (earlier sessions, uncommitted until now): human decision events and reduction, source cursors and reconcile-before-launch, turn-boundary reconciliation, internal/agentctx as the single renderer, ace-fca phases with sealed artifacts, the trajectory gate, bounded grilling, independent review, task pr enforcement, and human review reflection. Capability restrictions at the agent boundary: an authz.Agent surface at GatedWrite may ask and may not act. It also fixes two bugs the unit exposed -- gated surfaces could not reach the two endpoints written for them, and RequestHumanDecision would block an unowned task while rejecting a question from the session that did own it. Turn-boundary reconcile-failure escalation: a streak of consecutive failures asks the session to hand off, fenced on the lease epoch, with reconcile_failure as a real handoff reason. The worker was dropping the coordinator's verdict on the floor; it now acts on it. Burn-in: herdr.WriteLaunchContext dumps the exact agentctx.Build result to <worktree>/.orchestra/launch.md at every launch, local and federated. BURNIN.md is the runbook. deploy/build.sh stamps both binaries from one commit. go build, go vet and go test ./... pass, 20 packages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+87
-8
@@ -1,8 +1,87 @@
|
||||
import type { Detail, Overview } from './types'
|
||||
function sessionExpired(r:Response){if(r.status===401)window.dispatchEvent(new Event('orchestra:unauthorized'))}
|
||||
async function request<T>(path:string, init?:RequestInit):Promise<T>{const r=await fetch(path,{credentials:'same-origin',headers:{'Content-Type':'application/json',...init?.headers},...init});if(!r.ok){sessionExpired(r);throw new Error(await r.text())}return r.json() as Promise<T>}
|
||||
async function text(path:string){const r=await fetch(path);if(!r.ok)throw new Error(await r.text());return r.text()}
|
||||
async function upload(body:string){const r=await fetch('/v1/artifacts',{method:'POST',headers:{'Content-Type':'text/markdown'},body});if(!r.ok)throw new Error(await r.text());return (await r.json() as {ref:string}).ref}
|
||||
async function login(username:string,password:string){const r=await fetch('/v1/ui/session',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json'},body:JSON.stringify({username,password})});if(!r.ok)throw new Error(await r.text())}
|
||||
async function logout(){const r=await fetch('/v1/ui/session',{method:'DELETE',credentials:'same-origin'});if(!r.ok)throw new Error(await r.text())}
|
||||
export const api={login,logout,overview:()=>request<Overview>('/v1/ui/overview'),detail:(id:string)=>request<Detail>('/v1/ui/tasks/'+id),artifact:(ref:string)=>text('/v1/ui/artifacts/'+ref),upload,create:(body:unknown)=>request('/v1/ui/tasks',{method:'POST',body:JSON.stringify(body)}),action:(id:string,action:string,body={})=>request<Detail>(`/v1/ui/tasks/${id}/actions/${action}`,{method:'POST',body:JSON.stringify(body)})}
|
||||
import type { CreatedEvent, Detail, Overview } from './types'
|
||||
|
||||
function sessionExpired(response: Response) {
|
||||
if (response.status === 401) {
|
||||
window.dispatchEvent(new Event('orchestra:unauthorized'))
|
||||
}
|
||||
}
|
||||
|
||||
async function responseError(response: Response) {
|
||||
const message = (await response.text()).trim()
|
||||
return new Error(message || `${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(path, {
|
||||
credentials: 'same-origin',
|
||||
...init,
|
||||
headers: init?.body
|
||||
? { 'Content-Type': 'application/json', ...init.headers }
|
||||
: init?.headers,
|
||||
})
|
||||
if (!response.ok) {
|
||||
sessionExpired(response)
|
||||
throw await responseError(response)
|
||||
}
|
||||
return response.json() as Promise<T>
|
||||
}
|
||||
|
||||
async function text(path: string) {
|
||||
const response = await fetch(path, { credentials: 'same-origin' })
|
||||
if (!response.ok) {
|
||||
sessionExpired(response)
|
||||
throw await responseError(response)
|
||||
}
|
||||
return response.text()
|
||||
}
|
||||
|
||||
async function upload(body: string) {
|
||||
const response = await fetch('/v1/artifacts', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'text/markdown' },
|
||||
body,
|
||||
})
|
||||
if (!response.ok) {
|
||||
sessionExpired(response)
|
||||
throw await responseError(response)
|
||||
}
|
||||
return ((await response.json()) as { ref: string }).ref
|
||||
}
|
||||
|
||||
async function login(username: string, password: string) {
|
||||
const response = await fetch('/v1/ui/session', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
})
|
||||
if (!response.ok) throw await responseError(response)
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
const response = await fetch('/v1/ui/session', {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
if (!response.ok) throw await responseError(response)
|
||||
}
|
||||
|
||||
export const api = {
|
||||
login,
|
||||
logout,
|
||||
overview: () => request<Overview>('/v1/ui/overview'),
|
||||
detail: (id: string) => request<Detail>(`/v1/ui/tasks/${id}`),
|
||||
artifact: (ref: string) => text(`/v1/ui/artifacts/${ref}`),
|
||||
upload,
|
||||
create: (body: unknown) =>
|
||||
request<CreatedEvent>('/v1/ui/tasks', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
action: (id: string, action: string, body: object = {}) =>
|
||||
request<Detail>(`/v1/ui/tasks/${id}/actions/${action}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
}
|
||||
|
||||
+140
-12
@@ -1,12 +1,140 @@
|
||||
export type TaskState='queued'|'leased'|'blocked'|'completed'|'failed'
|
||||
export type BlockReason='lease_failure'|'worker_offline'|'lease_expired'|'approval'|'handoff_validation'|'operator_block'|'system_error'|'unknown'
|
||||
export interface SessionEvidence { pane_id?:string; harness_id?:string; pane_state?:string; source?:string; captured_at?:string; checked_at?:string }
|
||||
export interface Task { id:string; source:string; external_id:string; project:string; title?:string; description?:string; state:TaskState; version:number; lease?:{harness_id:string;until:string}; handoff_ref?:string; blocker?:string; block_reason?:BlockReason; blocked_at?:string; last_pane_id?:string; last_harness_id?:string; pane_state?:string; last_session?:SessionEvidence }
|
||||
export interface PendingApproval { kind:'shell'|'opencode_once'|'edit'|'unknown'; summary:string; command?:string; diff?:string; pane_id:string; capture_revision:number; detected_at:string }
|
||||
export interface Capture { task_id:string; source:string; text:string; revision:number; at:string; truncated:boolean }
|
||||
export interface Session { pane_id?:string; harness_id?:string; agent_status?:string; blocker?:string; lease_until?:string; capture?:Capture; pending_approval?:PendingApproval }
|
||||
export interface Action { id:string; enabled:boolean; reason?:string; needs?:string[] }
|
||||
export interface Detail { task:Task; events:Array<{id:string;type:string;at:string;payload:unknown}>; session?:Session; handoff_ref?:string; report_ref?:string; actions:Action[] }
|
||||
export interface WorkerHealth { herdr_status:'reachable'|'unreachable'|'unknown'; checked_at?:string; active_task_id?:string; active_pane_id?:string; last_error?:string; error_at?:string }
|
||||
export interface Worker { id:string; capacity:number; last_seen:string; online:boolean; health:WorkerHealth }
|
||||
export interface Overview { tasks:Task[]; workers:Worker[]; sessions:Session[]; updated_at:string }
|
||||
export type TaskState = 'queued' | 'leased' | 'blocked' | 'completed' | 'failed'
|
||||
|
||||
export type BlockReason =
|
||||
| 'lease_failure'
|
||||
| 'worker_offline'
|
||||
| 'lease_expired'
|
||||
| 'approval'
|
||||
| 'handoff_validation'
|
||||
| 'operator_block'
|
||||
| 'system_error'
|
||||
| 'unknown'
|
||||
|
||||
export interface SessionEvidence {
|
||||
pane_id?: string
|
||||
harness_id?: string
|
||||
pane_state?: string
|
||||
source?: string
|
||||
captured_at?: string
|
||||
checked_at?: string
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: string
|
||||
source: string
|
||||
external_id: string
|
||||
project: string
|
||||
capability?: string[]
|
||||
parent?: string
|
||||
inherent_priority?: number
|
||||
due?: string
|
||||
state: TaskState
|
||||
version: number
|
||||
title?: string
|
||||
description?: string
|
||||
acceptance?: string[]
|
||||
quality_gate?: string
|
||||
lease?: { harness_id: string; epoch?: string; until: string }
|
||||
handoff_ref?: string
|
||||
blocker?: string
|
||||
block_reason?: BlockReason
|
||||
blocked_at?: string
|
||||
last_pane_id?: string
|
||||
last_harness_id?: string
|
||||
pane_state?: string
|
||||
last_session?: SessionEvidence
|
||||
attempt?: number
|
||||
next_retry_at?: string
|
||||
failure_class?: string
|
||||
lifecycle_phase?: string
|
||||
last_error?: string
|
||||
}
|
||||
|
||||
export interface PendingApproval {
|
||||
kind: 'shell' | 'opencode_once' | 'edit' | 'unknown'
|
||||
summary: string
|
||||
command?: string
|
||||
diff?: string
|
||||
pane_id: string
|
||||
capture_revision: number
|
||||
detected_at: string
|
||||
}
|
||||
|
||||
export interface Capture {
|
||||
task_id: string
|
||||
source: string
|
||||
text: string
|
||||
revision: number
|
||||
at: string
|
||||
truncated: boolean
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
pane_id?: string
|
||||
harness_id?: string
|
||||
agent_status?: string
|
||||
blocker?: string
|
||||
lease_until?: string
|
||||
capture?: Capture
|
||||
pending_approval?: PendingApproval
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
id: string
|
||||
enabled: boolean
|
||||
reason?: string
|
||||
needs?: string[]
|
||||
}
|
||||
|
||||
export interface Event {
|
||||
seq?: number
|
||||
id: string
|
||||
type: string
|
||||
task_id?: string
|
||||
version?: number
|
||||
at: string
|
||||
payload: unknown
|
||||
surface?: string
|
||||
}
|
||||
|
||||
export interface Detail {
|
||||
task: Task
|
||||
events: Event[]
|
||||
session?: Session
|
||||
handoff_ref?: string
|
||||
report_ref?: string
|
||||
actions: Action[]
|
||||
}
|
||||
|
||||
export interface WorkerHealth {
|
||||
backend?: 'herdr' | 'tmux'
|
||||
herdr_status: 'reachable' | 'unreachable' | 'unknown'
|
||||
checked_at?: string
|
||||
active_task_id?: string
|
||||
active_pane_id?: string
|
||||
last_error?: string
|
||||
error_at?: string
|
||||
}
|
||||
|
||||
export interface Worker {
|
||||
id: string
|
||||
address?: string
|
||||
capacity: number
|
||||
supported_projects?: string[]
|
||||
build?: { revision?: string; time?: string; dirty?: string }
|
||||
last_seen: string
|
||||
online: boolean
|
||||
health: WorkerHealth
|
||||
}
|
||||
|
||||
export interface Overview {
|
||||
tasks: Task[]
|
||||
workers: Worker[]
|
||||
sessions: Session[]
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface CreatedEvent {
|
||||
task_id: string
|
||||
id: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user