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
144 changes: 144 additions & 0 deletions docs/cockpit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# AgentFlow Cockpit

AgentFlow Cockpit is the planned optional UI for goal-oriented SDLC delivery. It presents GitHub issues, epics, comments, PRs, role-pass evidence, validation, and follow-ups as AgentFlow workflow state instead of a generic issue list.

Cockpit is optional. The CLI/GitHub workflow remains authoritative and fully usable without Cockpit.

## Product principles

- GitHub is durable truth: issues, comments, PR bodies, commits, closure metadata.
- Local or runner telemetry is optional and non-authoritative.
- Single-agent, multi-role execution remains default.
- Helpers are advisory unless role attribution proves a valid multi-agent handoff.
- High-assurance work still requires human security/acceptance review.
- Follow-up issues are first-class; hidden TODOs are not.
- No raw model transcripts, prompts, tool inputs, secrets, or hidden runner state in the UI.

## MVP authentication

Remote-capable Cockpit uses GitHub OAuth as the MVP authentication option.

Required controls:

- GitHub OAuth login.
- Allowlisted users, orgs, or teams.
- Repository permission checks for each managed repo.
- Secure sessions and logout.
- CSRF and origin checks for write-capable actions.
- Audit log for every guarded action.
- Remote mode defaults read-only until write actions are explicitly enabled.

## Project linking

For MVP, a Cockpit project is a registered GitHub repository, for example `smota/agentflow-sdlc`.

Cockpit reads:

- `AGENTS.md`
- `agent-workflow.config.json`
- issue bodies and labels
- issue comments and markers
- PR bodies and checks
- commits and closure metadata

Cockpit does not require a mounted repo checkout in remote mode. Future Docker deployment should use repository allowlists and GitHub API first; local runner telemetry can be added later as sanitized optional events.

## Opinionated views

### Goal Board

Shows goals grouped by workflow state: Intake, Scoped, Planned, Implementation, Validation, Review, PR Ready, Done, and Blocked.

### Epic View

Rolls up child issues, dependencies, acceptance matrix, risk, active workstreams, PRs, follow-ups, and decisions.

### Issue SDLC View

Shows phases 0-8:

1. Product manager / JTBD
2. Analyst
3. Architect
4. Developer planning
5. Developer
6. Tester
7. Review
8. Tech writer
9. PR readiness

Each phase shows role-pass completeness, evidence, validation, handover, and next role contract.

### Evidence Health

Reports governance completeness, not model confidence:

- Required policy read state.
- Acceptance criteria.
- Role-pass fields.
- Validation status.
- Review independence.
- Human gate requirements.
- PR manifest readiness.
- Follow-up disposition.

### Comments lanes

Comments are grouped by workflow purpose using markers/templates:

- Workflow Status
- Handover
- Decisions
- Clarifications
- Review Findings
- Validation
- Follow-ups

## Guarded actions

Write-capable actions are structured and auditable:

- add clarification
- request checkpoint
- request human review
- draft follow-up issue
- update issue section
- post handover
- start next phase

Forbidden actions include remote gate bypass, marking validation/review passed, weakening acceptance criteria silently, deleting evidence, and merge-by-chat.

## Running the MVP server

Cockpit currently ships as a minimal Node service:

```bash
AGENTFLOW_REPOSITORIES=smota/agentflow-sdlc \
GITHUB_TOKEN=ghp_readonly_or_fine_grained_token \
pnpm cockpit
```

Remote mode requires GitHub OAuth configuration:

```bash
COCKPIT_REMOTE=true \
COCKPIT_PUBLIC_URL=https://cockpit.example.com \
COCKPIT_SESSION_SECRET=32-plus-character-secret \
GITHUB_CLIENT_ID=... \
GITHUB_CLIENT_SECRET=... \
GITHUB_ALLOWED_USERS=samue \
AGENTFLOW_REPOSITORIES=smota/agentflow-sdlc \
pnpm cockpit
```

Available surfaces:

- `/healthz` — health check.
- `/` — Goal Board for registered repository.
- `/issues/<number>` — issue SDLC view.
- `/login`, `/oauth/callback`, `/logout` — GitHub OAuth flow for remote mode.
- `POST /actions` — guarded write gateway when `COCKPIT_WRITE_ACTIONS=true` and request includes `X-Cockpit-Confirm: true`.

## Future Docker deployment

Docker is a second-pass packaging option, not an MVP dependency. Current implementation stays Docker-ready by using environment configuration, stateless server boundaries, GitHub API as primary data source, `/healthz`, and no mandatory local repo mount.
50 changes: 50 additions & 0 deletions lib/__tests__/cockpit-config-github.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest'
import { loadCockpitConfig, validateCockpitConfig } from '../cockpit-config.mjs'
import { createGitHubClient, loadRepositoryPermission } from '../cockpit-github.mjs'

describe('cockpit config', () => {
it('requires OAuth/session config for remote mode', () => {
const config = loadCockpitConfig({
COCKPIT_REMOTE: 'true',
AGENTFLOW_REPOSITORIES: 'smota/agentflow-sdlc',
})
expect(validateCockpitConfig(config).errors).toEqual(
expect.arrayContaining([
'GITHUB_CLIENT_ID required for remote mode',
'GITHUB_CLIENT_SECRET required for remote mode',
]),
)
})

it('accepts local read mode with registered repositories', () => {
const result = validateCockpitConfig(
loadCockpitConfig({ AGENTFLOW_REPOSITORIES: 'smota/agentflow-sdlc' }),
)
expect(result.ok).toBe(true)
})
})

describe('cockpit github client', () => {
it('sends bearer token and parses repo permission', async () => {
const calls = []
const client = createGitHubClient({
token: 'test-token',
fetchImpl: async (url, options) => {
calls.push({ url, options })
return response({ permissions: { pull: true, push: false } })
},
})
await expect(loadRepositoryPermission({ client, repo: 'smota/agentflow-sdlc' })).resolves.toBe(
'read',
)
expect(calls[0].options.headers.Authorization).toBe('Bearer test-token')
})
})

function response(data, ok = true, status = 200) {
return {
ok,
status,
text: async () => JSON.stringify(data),
}
}
149 changes: 149 additions & 0 deletions lib/__tests__/cockpit-domain.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { describe, expect, it } from 'vitest'
import {
classifyIssue,
createCockpitProject,
derivePhaseState,
missingRolePassFieldsFor,
} from '../cockpit-domain.mjs'
import {
classifyCommentLane,
extractAgentFlowIssueSections,
groupCommentsByLane,
} from '../cockpit-markdown.mjs'
import { authorizeCockpitUser, createAuthPolicy, validateAllowedOrigin } from '../cockpit-auth.mjs'
import {
evaluateGuardedAction,
formatDurableActionBody,
parseCockpitIntent,
} from '../cockpit-actions.mjs'

describe('cockpit domain', () => {
it('creates github-backed project without local checkout requirement', () => {
expect(
createCockpitProject({ repo: 'https://github.com/smota/agentflow-sdlc.git' }),
).toMatchObject({
id: 'smota/agentflow-sdlc',
durableTruth: 'github',
localTelemetry: 'optional',
})
})

it('classifies AgentFlow-managed epic issues', () => {
expect(
classifyIssue({
labels: ['epic', 'drafted-by:pi'],
body: '## Feature Tracking\n- [ ] #120',
}),
).toMatchObject({ isEpic: true, isAgentFlowManaged: true })
})

it('derives next safe action from evidence and review state', () => {
expect(derivePhaseState({ rolePasses: [{ phase: '5', role: 'tester' }] }).nextSafeAction).toBe(
'complete-role-pass-evidence',
)
expect(
derivePhaseState({
rolePasses: [completeRolePass()],
validations: [{ status: 'failed' }],
}).nextSafeAction,
).toBe('return-to-developer')
expect(
derivePhaseState({
rolePasses: [completeRolePass()],
validations: [{ status: 'passed' }],
reviewFindings: [{ severity: 'blocker' }],
}).nextSafeAction,
).toBe('resolve-review-findings')
})

it('checks required role-pass fields', () => {
expect(missingRolePassFieldsFor(completeRolePass())).toEqual([])
})
})

describe('cockpit markdown mapping', () => {
it('extracts canonical issue sections', () => {
const sections = extractAgentFlowIssueSections(
'## Requirements\n- one\n\n## Test plan\n- run tests',
)
expect(sections.Requirements).toContain('- one')
expect(sections['Test plan']).toContain('run tests')
})

it('groups workflow comments into AgentFlow lanes', () => {
const grouped = groupCommentsByLane([
{ body: '<!-- agent-handover -->\nnext role' },
{ body: '<!-- agentflow:validation-summary -->\npassed' },
])
expect(grouped.Handover).toHaveLength(1)
expect(grouped.Validation).toHaveLength(1)
expect(classifyCommentLane({ body: 'Decision: keep GitHub truth' })).toBe('Decisions')
})
})

describe('cockpit auth and guarded actions', () => {
it('enforces GitHub allowlist and repo permission', () => {
const policy = createAuthPolicy({
allowedUsers: ['samue'],
repositories: ['smota/agentflow-sdlc'],
})
expect(
authorizeCockpitUser({
user: { login: 'samue' },
repo: 'smota/agentflow-sdlc',
repoPermission: 'read',
policy,
}).ok,
).toBe(true)
expect(
authorizeCockpitUser({
user: { login: 'other' },
repo: 'smota/agentflow-sdlc',
repoPermission: 'read',
policy,
}).reasons,
).toContain('user-not-allowed')
})

it('validates allowed origins', () => {
expect(
validateAllowedOrigin({
origin: 'https://cockpit.example.com',
publicUrl: 'https://cockpit.example.com/',
}),
).toBe(true)
expect(
validateAllowedOrigin({
origin: 'https://evil.example.com',
publicUrl: 'https://cockpit.example.com',
}),
).toBe(false)
})

it('blocks remote gate bypass and requires confirmations', () => {
expect(
evaluateGuardedAction({ action: 'merge-pr', userRole: 'admin', confirmation: true }).reasons,
).toContain('forbidden-by-policy')
expect(
evaluateGuardedAction({ action: 'post-handover', userRole: 'operator' }).reasons,
).toContain('confirmation-required')
expect(parseCockpitIntent({ type: 'draft-follow-up', issue: 123 })).toMatchObject({
ok: true,
intent: { requiresDurableEvidence: true },
})
expect(formatDurableActionBody({ type: 'post-handover', body: 'Next: review' })).toContain(
'<!-- agent-handover -->',
)
})
})

function completeRolePass() {
return {
phase: '5',
role: 'tester',
read: ['AGENTS.md'],
decisions: ['run validation'],
uncertainties: ['none'],
nextRoleContract: 'review findings',
}
}
Loading
Loading