feat: capture_work — braindump → decomposed issues → filed in gitea (P4)
The last big agent capability. In the Capture screen, a rough braindump runs real
big-model decomposition into a small, estimated issue set; you review/edit the
labels and approve, and the issues are opened in gitea. This is the one place the
big model earns its keep (docs/agent-tools.md).
core (@commitea/core):
- capture-work: PROPOSE_ISSUES_TOOL + CAPTURE_SYSTEM; captureWork(complete, dump)
forces a single structured decomposition and returns validated issues; parseCaptureArgs
drops blank titles + invalid est/p labels. ProposedIssue / CaptureProposal.
- gitea client: createIssue({title, body?, labelIds?}) → POST /issues, normalized.
app:
- model bridge model:capture runs captureWork on the (loaded) big model.
- gitea bridge gitea:createIssues opens each approved issue with its est/* + p/*
labels (reusing the #41 label-id resolver — zero-pollution, no invented labels).
- Capture screen: when a model is configured, "Brew tickets" runs real capture and
"Approve all" files the set; otherwise the scripted demo interview runs. Fixed a
race — the brew handler re-checks model status at click time so a configured
model never falls into the scripted path before status resolves.
Verified: 108 core tests green (7 capture + createIssue added), desktop typecheck
clean, 14 fixture e2e green. Gated live e2e against gemma-4-26b: the auth braindump
→ 3 real tickets ("Resolve token refresh + session staleness" est/3d p/1, "Fix
webhook double-firing" est/2d p/2, "Write auth setup docs" est/1d p/3), reviewable
and editable; Discard so the test files nothing (createIssue POST is unit-tested).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
65
packages/core/src/agent/capture-work.test.ts
Normal file
65
packages/core/src/agent/capture-work.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { CompletionResult } from './chat-client.js'
|
||||
import { captureWork, parseCaptureArgs } from './capture-work.js'
|
||||
|
||||
describe('parseCaptureArgs', () => {
|
||||
it('validates issues and keeps only real titles + valid labels', () => {
|
||||
const p = parseCaptureArgs({
|
||||
issues: [
|
||||
{ title: 'Fix token refresh', body: 'dies silently', estimate: 'est/2d', priority: 'p/1' },
|
||||
{ title: ' ', body: 'blank title dropped' },
|
||||
{ title: 'Docs', estimate: 'est/9d', priority: 'urgent' }, // invalid labels → dropped to undefined
|
||||
],
|
||||
consequence: 'Beta slips a day',
|
||||
})
|
||||
expect(p.issues).toHaveLength(2)
|
||||
expect(p.issues[0]).toEqual({ title: 'Fix token refresh', body: 'dies silently', estimate: 'est/2d', priority: 'p/1' })
|
||||
expect(p.issues[1]).toEqual({ title: 'Docs', body: '', estimate: undefined, priority: undefined })
|
||||
expect(p.consequence).toBe('Beta slips a day')
|
||||
})
|
||||
|
||||
it('tolerates a missing/!array issues field', () => {
|
||||
expect(parseCaptureArgs({}).issues).toEqual([])
|
||||
expect(parseCaptureArgs({ issues: 'nope' }).issues).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('captureWork', () => {
|
||||
it('forces propose_issues and returns the validated set', async () => {
|
||||
const result: CompletionResult = {
|
||||
content: '',
|
||||
toolCalls: [
|
||||
{
|
||||
id: 'c1',
|
||||
name: 'propose_issues',
|
||||
arguments: JSON.stringify({
|
||||
issues: [{ title: 'Retry token refresh with backoff', body: '', estimate: 'est/2d', priority: 'p/2' }],
|
||||
consequence: 'no material shift',
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
let sawTool = ''
|
||||
const proposal = await captureWork(async (_m, tools) => {
|
||||
sawTool = tools?.[0]?.name ?? ''
|
||||
return result
|
||||
}, 'auth is flaky, token refresh dies')
|
||||
expect(sawTool).toBe('propose_issues')
|
||||
expect(proposal.issues[0].title).toBe('Retry token refresh with backoff')
|
||||
expect(proposal.consequence).toBe('no material shift')
|
||||
})
|
||||
|
||||
it('returns an empty set when the model answers without the tool', async () => {
|
||||
const proposal = await captureWork(async () => ({ content: 'I need more detail.', toolCalls: [] }), 'vague')
|
||||
expect(proposal.issues).toEqual([])
|
||||
})
|
||||
|
||||
it('survives malformed tool arguments', async () => {
|
||||
const proposal = await captureWork(
|
||||
async () => ({ content: '', toolCalls: [{ id: 'c1', name: 'propose_issues', arguments: '{not json' }] }),
|
||||
'x',
|
||||
)
|
||||
expect(proposal.issues).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user