clauderipple 0.12.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/app/package.json +1 -1
- package/dist/mod/hooks/register.tsx +9 -5
- package/dist/mod/hooks/workers.ts +14 -0
- package/dist/mod/tests/register.test.tsx +19 -3
- package/dist/router/src/proxy.js +801 -711
- package/dist/router/src/version.js +1 -1
- package/dist/router/src/webfetch.js +56 -0
- package/dist/ui/app.js +1 -1
- package/dist/ui/i18n.js +12 -6
- package/docs/ARCHITECTURE.md +50 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.1 — 2026-10-21
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- **One web setting covers page reading too.** WebFetch downloads a page and has the small, fast
|
|
8
|
+
model answer from it; that answer now comes from the model picked for web search, as WebSearch
|
|
9
|
+
already did, instead of Haiku on Claude quota. The Clients row is "Web search & page reading". If
|
|
10
|
+
that model fails before answering, the request goes where it went before.
|
|
11
|
+
- **Design work takes the heavy level of the frontend + design ladder**, whatever tier it was
|
|
12
|
+
given: a task judged design runs on the heavy workers, since its result depends on the model's
|
|
13
|
+
taste rather than on difficulty. Frontend tasks still pick by tier.
|
|
14
|
+
|
|
3
15
|
## 0.12.0 — 2026-10-21
|
|
4
16
|
|
|
5
17
|
### Added
|
package/dist/app/package.json
CHANGED
|
@@ -8,7 +8,7 @@ import { rungOf } from './ladder'
|
|
|
8
8
|
import { debugText, langFrom, t } from './i18n'
|
|
9
9
|
import type { Lang } from './i18n'
|
|
10
10
|
import { EFFORT_JUDGE_SYSTEM, finalEffort, isCacheSafeModel, isShortFollowUp, jevEffortRequest, jevFailure, jevUsage, jevWorkerRequest, judgeContext, parseEffortVerdict, parseJevEffort, parseJevKey, parseJevWorker, shouldJudge } from './judge'
|
|
11
|
-
import { AUTO_DESCRIPTION, WORKER_JUDGE_SYSTEM, agentProvider, candidateUsable, chatgptUsable, claudeCandidates, ladderOptionsFrom, ladderPick, parseWorkerVerdict, readMarker, routerModelOptions, spawnModel, spawnTitle, takesOver, TIER_MARKERS, tierOf, workerJudgeInput, workerRule, zenJev } from './workers'
|
|
11
|
+
import { AUTO_DESCRIPTION, WORKER_JUDGE_SYSTEM, agentProvider, candidateUsable, chatgptUsable, claudeCandidates, domainVerdict, ladderOptionsFrom, ladderPick, parseWorkerVerdict, readMarker, routerModelOptions, spawnModel, spawnTitle, takesOver, TIER_MARKERS, tierOf, workerJudgeInput, workerRule, zenJev } from './workers'
|
|
12
12
|
import { MARKER, agentModel, fixSpawn } from './worker'
|
|
13
13
|
import { activeModeOf, chatgptProviders, claudeWindow, eagerNote, gptLimitsFrom, modeKey, modeOver, quotaWindows, resetProvider, routingSettings } from './modes'
|
|
14
14
|
import type { QuotaWindow } from './modes'
|
|
@@ -760,11 +760,15 @@ async function requestPick($: EngineInterface, e: AgentSpawnInput, s: Settings):
|
|
|
760
760
|
const providers = options && options.until > now ? Object.fromEntries(Object.entries(options.providers ?? {}).filter(([, p]) => p.exhaustedUntil === undefined || p.exhaustedUntil > now)) : {}
|
|
761
761
|
const explicitCandidate = target ? pinnedCandidates.find(c => c.target.agent === target!.agent) ?? (target.agent === 'general-purpose' ? { target, model: target.model ?? 'sonnet', provider: (await read($, routerModels)).find(m => m.value.toLowerCase() === target!.model)?.provider ?? 'anthropic' } : undefined) : undefined
|
|
762
762
|
if (explicitCandidate && s.providers[explicitCandidate.provider] === false) return { deny: t(language, 'workers.disabled', { provider: explicitCandidate.provider }) }
|
|
763
|
-
// An active mode replaces the whole set; an explicit marker still names its own model.
|
|
763
|
+
// An active mode replaces the whole set; an explicit marker still names its own model. Design under
|
|
764
|
+
// the ui ladder takes the heavy level even past a tier marker: the main chat tiers by difficulty,
|
|
765
|
+
// and a design task is "standard" by that measure while its result depends on the model's taste.
|
|
764
766
|
const { rule, ladder } = workerRule(verdict.domain, routing)
|
|
765
|
-
const
|
|
767
|
+
const explicitUsable = !!explicitCandidate && candidateUsable(explicitCandidate, s, usable, providers)
|
|
768
|
+
const domained: ReturnType<typeof domainVerdict> = explicitUsable ? { verdict } : domainVerdict(verdict, rule)
|
|
769
|
+
const resolved = explicitUsable
|
|
766
770
|
? { target: target!, effort: verdict.effort }
|
|
767
|
-
: ladderPick(verdict, ladder, routing, pinnedCandidates, usable, language, routing.allow, providers, routing.allowKnown)
|
|
771
|
+
: ladderPick(domained.verdict, ladder, routing, pinnedCandidates, usable, language, routing.allow, providers, routing.allowKnown)
|
|
768
772
|
const selected = resolved.target
|
|
769
773
|
if (!selected) return { deny: t(language, 'workers.none') }
|
|
770
774
|
const fallbackReason = resolved.fallbackReason
|
|
@@ -775,7 +779,7 @@ async function requestPick($: EngineInterface, e: AgentSpawnInput, s: Settings):
|
|
|
775
779
|
const value: WorkerPick = { ...verdict, ...resolved, rule, target: selected, by, ms, id, ...(fallbackReason ? { fallbackReason } : {}) }
|
|
776
780
|
// The verdict behind a pick is otherwise only in the agents panel; one debug line per pick keeps
|
|
777
781
|
// it checkable: what the judge said, the rung it landed on and what ran.
|
|
778
|
-
const entry = { task: e.description.slice(0, 200), domain: value.domain, level: value.level, kind: value.kind ?? 'do', sure: value.sure, rule: value.rule, rung: value.rung, position: value.position, skipped: value.skipped, target: value.target, model: model ?? null, effort: value.effort, by, ...(mode ? { mode: mode.name } : {}), ...(fallbackReason ? { fallbackReason } : {}) }
|
|
782
|
+
const entry = { task: e.description.slice(0, 200), domain: value.domain, level: value.level, kind: value.kind ?? 'do', sure: value.sure, rule: value.rule, ...(domained.raised ? { raised: domained.raised } : {}), rung: value.rung, position: value.position, skipped: value.skipped, target: value.target, model: model ?? null, effort: value.effort, by, ...(mode ? { mode: mode.name } : {}), ...(fallbackReason ? { fallbackReason } : {}) }
|
|
779
783
|
await $.ui.log('ClaudeRipple worker pick: ' + JSON.stringify(entry), { to: 'debug' })
|
|
780
784
|
// The brief itself is never logged (it can hold anything); its length is. To re-judge a pick,
|
|
781
785
|
// its brief is in the session's own transcript under the same description.
|
|
@@ -171,6 +171,20 @@ export function workerRule(domain: Domain, settings: Settings): { rule: WorkerPi
|
|
|
171
171
|
return ladder ? { rule: key, ladder } : { rule: 'general', ladder: settings.rules.general }
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Visual design depends on the model's taste, not on how hard the task is (2026-10-21: a store
|
|
176
|
+
* screenshot redesign judged "design, standard" ran on the light pair). Under the ui ladder a design
|
|
177
|
+
* verdict on the light or standard rung is raised to the hard rung; frontend, other ladders and
|
|
178
|
+
* verdicts already on hard or plan are untouched.
|
|
179
|
+
*/
|
|
180
|
+
export function domainVerdict(verdict: WorkerVerdict, rule: WorkerPick['rule']): { verdict: WorkerVerdict; raised?: 'design' } {
|
|
181
|
+
if (rule !== 'ui' || verdict.domain !== 'design') return { verdict }
|
|
182
|
+
const rung = rungOf(verdict)
|
|
183
|
+
if (rung !== 'light' && rung !== 'standard') return { verdict }
|
|
184
|
+
const { sure: _sure, ...rest } = verdict
|
|
185
|
+
return { verdict: { ...rest, ...TIER_MARKERS.hard! }, raised: 'design' }
|
|
186
|
+
}
|
|
187
|
+
|
|
174
188
|
export type Resolution = { target?: TierTarget; effort: Effort; rung?: Rung; position?: number; skipped?: { rung: Rung; position: number; model: string; reason: string }[]; fallbackReason?: string }
|
|
175
189
|
|
|
176
190
|
/** Claude Code's own agent with no model: the subagent runs on the session's model. */
|
|
@@ -654,7 +654,7 @@ for (const [tier, rung, agent, model] of [
|
|
|
654
654
|
|
|
655
655
|
for (const domain of ['design', 'frontend', 'backend', 'docs'] as const) test(`auto selects the ${domain} task's domain ladder or its own general fallback`, async ($, on) => {
|
|
656
656
|
const b = boundaries(on, { domainRules: {
|
|
657
|
-
ui: { mode: 'ladder', rungs: { light: [{ target: { agent: 'gpt-6-astra' }, effort: 'high' }] } },
|
|
657
|
+
ui: { mode: 'ladder', rungs: { light: [{ target: { agent: 'gpt-6-astra' }, effort: 'high' }], hard: [{ target: { agent: 'gpt-6-1-sol' }, effort: 'high' }] } },
|
|
658
658
|
backend: { mode: 'ladder', rungs: { light: [{ target: { agent: 'gpt-6-1-sol' }, effort: 'medium' }] } },
|
|
659
659
|
} })
|
|
660
660
|
http(on)
|
|
@@ -663,8 +663,24 @@ for (const domain of ['design', 'frontend', 'backend', 'docs'] as const) test(`a
|
|
|
663
663
|
await start($)
|
|
664
664
|
await $.agent.spawn(SPAWN)
|
|
665
665
|
const rule = domain === 'design' || domain === 'frontend' ? 'ui' : domain === 'backend' ? 'backend' : 'general'
|
|
666
|
-
|
|
667
|
-
|
|
666
|
+
// A light design verdict is raised to the heavy pair (visual taste, not difficulty); frontend keeps its level.
|
|
667
|
+
const raised = domain === 'design'
|
|
668
|
+
expect(sent[0]?.subagentType).toBe(rule === 'ui' ? (raised ? 'gpt-6-1-sol' : 'gpt-6-astra') : rule === 'backend' ? 'gpt-6-1-sol' : 'deepseek-v4-1-flash')
|
|
669
|
+
const row = JSON.parse(b.files.get('/home/test/.clauderipple/logs/worker-picks.jsonl')!)
|
|
670
|
+
expect(row).toMatchObject({ rule, domain, level: 'light', rung: raised ? 'hard' : 'light' })
|
|
671
|
+
expect(row.raised).toBe(raised ? 'design' : undefined)
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
test('a design task under the ui ladder takes the heavy level past a tier marker', async ($, on) => {
|
|
675
|
+
const b = boundaries(on, { domainRules: { ui: { mode: 'ladder', rungs: { light: [{ target: { agent: 'gpt-6-astra' }, effort: 'high' }], hard: [{ target: { agent: 'gpt-6-1-sol' }, effort: 'high' }] } } } })
|
|
676
|
+
http(on)
|
|
677
|
+
judgeStub(on, JSON.stringify({ domain: 'design', level: 'deep', kind: 'do', effort: 'high', sure: 0.9, why: 'domain' }))
|
|
678
|
+
const sent = spawnStub(on)
|
|
679
|
+
await start($)
|
|
680
|
+
await $.agent.spawn({ ...SPAWN, prompt: '[[ripple: light]]\nTweak a color.' })
|
|
681
|
+
expect(sent[0]?.subagentType).toBe('gpt-6-1-sol')
|
|
682
|
+
const row = JSON.parse(b.files.get('/home/test/.clauderipple/logs/worker-picks.jsonl')!.trim().split('\n').find(line => line.includes('"type":"pick"'))!)
|
|
683
|
+
expect(row).toMatchObject({ by: 'session', rule: 'ui', domain: 'design', rung: 'hard', raised: 'design' })
|
|
668
684
|
})
|
|
669
685
|
|
|
670
686
|
for (const domains of [false, true]) test(`tier marker ${domains ? 'awaits' : 'does not await'} one pending shadow judge with domain ladders ${domains ? 'on' : 'off'}`, async ($, on) => {
|