feat: writes via chat — Reginald proposes, you approve inline (P4)
Completes "chat is the write-path" (decisions.md D1). Ask Reginald to re-estimate
or reprioritize an issue; it formulates a proposal, you approve it inline, and the
write runs through the same guarded apply_changes engine the Issue screen uses.
The model never writes — it proposes; the app owns approval + execution.
core (@commitea/core):
- propose_change tool declaration + REGINALD_SYSTEM updated ("never claim a change
is applied; you propose, the human approves").
- proposalsFor(args, currentLabels, title): pure — builds the concrete, non-noop
ChangeProposal(s) (change + label diff) for a propose_change request, dropping
invalid/unchanged axes. ChangeProposal / ProposeChangeArgs types.
app:
- model bridge executes propose_change by planning against the issue's current
labels (no write) and returns the proposals with the turn.
- useChat surfaces pending proposals + approve/dismiss; approve calls onApplyChange
(AppShell's guarded handler → PUT + board/forecast refetch), dismiss leaves it.
- ChatPanel renders each proposal as a propose-approve card (diff + Approve/Dismiss).
Verified: 101 core tests green (4 proposalsFor added), desktop typecheck clean,
14 fixture e2e green. Gated live e2e against gemma-4-26b: "Set the estimate on #3
to est/5d" → Reginald proposes "est/2d → est/5d" as an inline card, says it's
*proposed* not done; Dismiss leaves the repo untouched. The approve→write path is
the #41 engine (separately verified change→revert).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,4 +30,27 @@ test.describe('live Reginald', () => {
|
||||
|
||||
await app.close()
|
||||
})
|
||||
|
||||
test('proposes an estimate change for inline approval (writes via chat)', async () => {
|
||||
test.skip(!process.env.GITEA_LIVE || !process.env.COMMITEA_MODEL_LIVE, 'live model test — opt-in')
|
||||
test.setTimeout(300_000)
|
||||
const app = await electron.launch({ args: [MAIN], env: { ...process.env } })
|
||||
const win = await app.firstWindow()
|
||||
await win.waitForLoadState('domcontentloaded')
|
||||
await expect(win.getByText(/· local$/)).toBeVisible({ timeout: 20000 })
|
||||
|
||||
const composer = win.getByPlaceholder(/Tell me what to do/)
|
||||
await composer.fill('Set the estimate on issue #3 to est/5d.')
|
||||
await composer.press('Enter')
|
||||
|
||||
// propose_change → an inline propose-approve card (never an auto-write)
|
||||
await expect(win.getByText('Proposed · #3')).toBeVisible({ timeout: 240_000 })
|
||||
await expect(win.getByText(/→ est\/5d/)).toBeVisible()
|
||||
await win.screenshot({ path: join(here, '.artifacts', 'screens', 'live-reginald-propose.png'), fullPage: true, animations: 'disabled' })
|
||||
// Dismiss so the live run never mutates the repo (the write path itself is #41-tested)
|
||||
await win.getByRole('button', { name: 'Dismiss' }).click()
|
||||
await expect(win.getByText(/Left #3 as it was/)).toBeVisible()
|
||||
|
||||
await app.close()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user