feat(P5): IHF Phase 5 complete — agent-assisted distillation
Some checks failed
Test / test (push) Has been cancelled

Adds bounded AI support to the IHF governance loop. All AI outputs are
attributed (model_ref), reviewable (AgentReviewRecord), and reversible.
No autonomous decisions; no silent requirement promotion.

- T01: Schema — agent_proposals, agent_review_records,
  confidence_annotations (migration 1743379200)
- T02: AgentProposalsController (index/show/accept/reject, idempotent
  review guard), global nav "Agent" link
- T03: SummarizeClusterAction — Claude API cluster summary on widget show
- T04: DraftRequirementAction — AI requirement draft; acceptance creates
  RequirementCandidate (human-gated)
- T05: DetectDuplicatesAction — duplicate_flag proposal on candidate show
- T06: DetectPolicySensitivityAction — policy_flag with
  ConfidenceAnnotations per concern scope
- T07: ProposeImplementationAction — impl_proposal from decision show
- T08: AgentAuditDashboardAction — autoRefresh; KPI row, unreviewed queue,
  recent proposals, attribution log matrix
- T09: integration tests, SCOPE.md updated, phase5-summary.md, flake.nix
  adds http-conduit/aeson/string-conversions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-29 15:54:33 +00:00
parent 1862bb295a
commit 2605c1c977
23 changed files with 1284 additions and 21 deletions

View File

@@ -9,7 +9,8 @@ import Generated.Types
import IHP.Prelude
import IHP.ControllerPrelude
import Data.Aeson (toJSON, object, (.=))
import Application.Helper.Controller (isInRegression, widgetCycleCounts)
import Application.Helper.Controller (isInRegression, widgetCycleCounts, callClaudeApi)
import Data.List (intercalate)
instance Controller WidgetsController where
beforeAction = ensureIsUser
@@ -121,3 +122,65 @@ instance Controller WidgetsController where
|> createRecord
setSuccessMessage "Widget updated"
redirectTo ShowWidgetAction { widgetId = widget.id }
-- T03: Summarize feedback cluster via Claude API
action SummarizeClusterAction { widgetId } = do
annotations <- query @Annotation
|> filterWhere (#widgetId, widgetId)
|> orderByDesc #createdAt
|> limit 20
|> fetch
threads <- query @AnnotationThread
|> filterWhere (#widgetId, widgetId)
|> orderByDesc #createdAt
|> limit 20
|> fetch
let annLines = map (\a -> "[" <> a.category <> "/" <> a.severity <> "] " <> a.body) annotations
threadLines = map (\t -> "[thread] " <> t.title <> ": " <> fromMaybe "" t.description) threads
userMsg = intercalate "\n" (annLines <> threadLines)
result <- liftIO $ callClaudeApi
"You are a distillation assistant for a governed interaction hub. Summarize the following user feedback cluster into a concise, actionable summary (2\x20134 sentences). Be factual and neutral."
userMsg
300
case result of
Left err -> do
setErrorMessage ("AI summarization failed: " <> err)
redirectTo ShowWidgetAction { widgetId }
Right content -> do
newRecord @AgentProposal
|> set #proposalType "summary"
|> set #sourceWidgetId (Just widgetId)
|> set #content content
|> set #modelRef "claude-sonnet-4-6"
|> set #status "pending"
|> createRecord
setSuccessMessage "Summary proposal created"
redirectTo ShowWidgetAction { widgetId }
-- T04: Draft a requirement candidate via Claude API
action DraftRequirementAction { widgetId } = do
annotations <- query @Annotation
|> filterWhere (#widgetId, widgetId)
|> orderByDesc #createdAt
|> limit 20
|> fetch
let annLines = map (\a -> "[" <> a.category <> "/" <> a.severity <> "] " <> a.body) annotations
userMsg = intercalate "\n" annLines
result <- liftIO $ callClaudeApi
"You are a requirements analyst. Given these friction annotations, draft a single structured requirement candidate. Respond with JSON: {\"title\": \"...\", \"description\": \"...\"}."
userMsg
400
case result of
Left err -> do
setErrorMessage ("AI draft failed: " <> err)
redirectTo ShowWidgetAction { widgetId }
Right content -> do
newRecord @AgentProposal
|> set #proposalType "requirement_draft"
|> set #sourceWidgetId (Just widgetId)
|> set #content content
|> set #modelRef "claude-sonnet-4-6"
|> set #status "pending"
|> createRecord
setSuccessMessage "Requirement draft proposal created"
redirectTo ShowWidgetAction { widgetId }