feat(phase2-E): multi-provider routing via secutools delegation

Adds optional delegation of agent-queue tasks to the SecuAAS secutools
AI platform (GPU / Gemini / Claude API) instead of dispatching to a
local Claude Code tmux session. Per-task opt-in via YAML frontmatter
fields preferred_ai, allow_delegation, complexity_hint — absence keeps
the Phase 1 behaviour exactly (zero breaking change).

Go side:
- internal/secutools: HTTP client with exponential-backoff retries
  (SubmitJob/GetJob/WaitForResult), DecideProvider map adapter for CLI
  use, table tests.
- internal/router: struct-typed Decide() with strict precedence
  (needs_claude_code > preferred_ai=claude-code > allow_delegation=false
  > preferred_ai > fail-safe local on unknown).
- internal/delegation: Manager submits jobs, writes .md.delegated
  markers for on-restart recovery, runs a periodic reaper that moves
  completed jobs into done/ with provider/cost footer and failed jobs
  into failed/.
- internal/dispatcher: WithDelegation() opt-in, routeTask hook before
  findFreeSession, skips .md.delegated in assignNextTask.
- internal/api: /api/delegated/status (active jobs + counters),
  /watchdog/status extended with delegation counters.
- cmd/ccl-delegate: small CLI exposing submit/get/result/decide so the
  bash dispatcher can call the same contract without duplicating logic.
- cmd/claude-failover: delegation wired opt-in via SECUTOOLS_API_KEY.

Tests:
- 29+ new unit tests across router, secutools, delegation, dispatcher,
  api packages. go test -race -count=1 clean.
- tests/phase2-E-integration.sh: bash end-to-end against a Python
  stdlib mock HTTP server, exercising the dev-management scripts.

Forward-compat with watchdog (Phase 1 B1 already ignores
state=delegated_to_secutools) so delegated tasks aren't flagged stale.
This commit is contained in:
Ubuntu 2026-04-17 02:17:19 +00:00
parent 47ab86eef9
commit 3e20085204
18 changed files with 2819 additions and 22 deletions

View file

@ -1,4 +1,92 @@
# Version actuelle : 0.3.8
# Version actuelle : 0.4.0
## [0.4.0] - 2026-04-17
**Type:** Minor — Phase 2 chantier E : multi-provider routing (delegation to secutools)
### Ajouté
- `internal/secutools` — HTTP client (SubmitJob, GetJob, WaitForResult)
pour la plateforme centralisée IA SecuAAS. Interface `Client` mockable
pour les tests, implémentation `HTTPClient` avec polling 2s + timeout
+ propagation `context.Context`.
- `internal/router` — Décide ProviderClaudeCode (Phase 1) vs ProviderGPU /
Gemini / ClaudeAPI / Auto (delegation secutools) à partir des nouveaux
champs frontmatter `preferred_ai`, `allow_delegation`, `complexity_hint`.
Précédence stricte : `needs_claude_code` > `preferred_ai=claude-code` >
`allow_delegation=false` (default) > `preferred_ai=...` > fail-safe Claude
Code sur valeur inconnue.
- `internal/delegation` — Manager qui submit les tâches non-Claude vers
secutools, écrit un marker `inbox/<id>.md.delegated` (rebuild-on-restart),
et fait tourner un reaper périodique qui finalise les jobs vers `done/`
(succès) ou `failed/` (échec). Compteurs atomiques (Active /
CompletedTotal / FailedTotal) exposés via Snapshot().
- Dispatcher: méthode `WithDelegation()` opt-in, branchement router avant
`findFreeSession()`, fallback automatique vers Claude Code si le submit
secutools échoue. Skip des `*.md.delegated` dans `assignNextTask`.
- Frontmatter: `TaskFrontmatter` étendu avec `PreferredAI`, `AllowDelegation`,
`ComplexityHint`. Tous optionnels — un .md sans ces champs garde
exactement le comportement Phase 1.
- API HTTP : nouveau `GET /api/delegated/status` (jobs en cours +
compteurs), `GET /watchdog/status` qui inclut les compteurs delegation.
`WithDelegation()` opt-in — endpoints renvoient 404 si désactivés.
- main.go : Manager initialisé seulement si `SECUTOOLS_API_KEY` est set
(zéro changement pour les déploiements existants). `LoadFromDisk()`
réhydrate les markers en attente après un restart.
### Tests ajoutés (29 nouveaux cas)
- `internal/router` (8 tests) : matrice de décision complète, contrats
IsDelegated, fail-safe sur provider inconnu.
- `internal/secutools` (5 tests) : httptest.Server qui mock le contrat,
HappyPath, HTTPError, polling jusqu'à completed, ErrJobFailed, cancel
via context.
- `internal/delegation` (7 tests) : Submit + marker, rejet provider non
délégué, reap success/fail, LoadFromDisk, Active(), end-to-end.
- `internal/api` (4 tests) : /health, /api/delegated/status désactivé/
activé, /watchdog/status inclut bien les counters.
- `internal/dispatcher` (5 nouveaux tests + 8 existants intacts) :
routeTask parse les nouveaux champs, dispatchProject délègue le GPU,
dispatchProject garde Claude Code en backward-compat, needs_claude_code
bypass, end-to-end inbox→submit→reap→done/.
### Tests effectués
- `go build ./...` : ok
- `go vet ./...` : clean
- `go test -race ./...` : tous les packages passent (~10s)
- `go mod tidy` : aucun changement nécessaire
### Ajouté — complément 2026-04-17 (bash wiring)
- `cmd/ccl-delegate` — CLI Go tiny wrapper autour d'`internal/secutools`
utilisable depuis bash. Sous-commandes `submit`, `get`, `result`,
`decide`. Env `CCL_SECUTOOLS_API_KEY` (preferred) ou `SECUTOOLS_API_KEY`.
`CCL_SECUTOOLS_URL` ou `CCL_SECUTOOLS_MOCK_URL` pour tests.
- `internal/secutools/routing.go``DecideProvider(map[string]any) string`
(adapter signature requis par le spec pour usage depuis CLI). Map-input
permissif (bool|string|int coerced). Fail-safe retourne `"local"`.
Table-driven tests couvrent 14 cas incluant coercions.
- `internal/secutools/client.go` — retries exponential backoff (max 3
retries, 500ms base, x2 chaque attempt). Retry sur 5xx + transport
errors, PAS sur 4xx. `SetRetryPolicy(maxRetries, baseDelay)` exposé
pour tests. Tests : `TestSubmitJob_RetriesOn5xx`,
`TestSubmitJob_DoesNotRetry4xx`.
- `tests/phase2-E-integration.sh` — test bout-en-bout bash côté
dev-management avec mock HTTP secutools (Python stdlib).
Scénarios : decide, delegate (marker + status.json +
state=delegated_to_secutools), drive mock → completed, poll-reaper,
assertions done/ body + footer, cleanup inbox, budget tracker,
rejet tâche legacy (rc=2).
### Notes / décisions
- Pas d'appel réseau réel à secutools dans les tests — fakeClient +
httptest côté Go, serveur mock Python stdlib côté integration.
- Le client secutools est branché sur `SECUTOOLS_API_KEY` env var pour
garder un path par défaut "Phase 1 only".
- Smoke test prod restera nécessaire (test E2E avec `preferred_ai: gpu`
réel) — pas effectué dans ce chantier comme demandé (pas de push).
- Décision : `DecideProvider(map[string]any)` vit dans `secutools`
(adapter CLI, signature du spec) ET `router.Decide(Task)` vit dans
`router` (struct-typed, utilisé par le dispatcher Go). Les deux partagent
la même matrice de précédence — pas de duplication de logique car le
router consomme `TaskFrontmatter` typé via yaml.Unmarshal, alors que
la CLI parse manuellement pour rester zero-dep.
## [0.3.8] - 2026-04-16
**Type:** Patch — Bug #1 (A3 flip+ensure inconsistency) + Bug #10 (requiredShared contract test)