clauderipple 0.12.0 → 0.12.2
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 +27 -0
- package/dist/app/package.json +1 -1
- package/dist/mod/hooks/register.tsx +9 -5
- package/dist/mod/hooks/workers.ts +19 -1
- package/dist/mod/tests/register.test.tsx +19 -3
- package/dist/router/src/proxy.js +801 -711
- package/dist/router/src/tether-win.js +24 -2
- package/dist/router/src/tether.js +66 -10
- 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 +107 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.2 — 2026-10-22
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Back on Wi-Fi only when a known Wi-Fi is in range.** After a hotspot failover, the way back
|
|
8
|
+
no longer drops the hotspot for a Wi-Fi that is not there, which cut the connection on the move.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Naming a model on a delegation is now explained to the main chat.** `ripple:auto`'s
|
|
13
|
+
description says that `[[ripple: opus@high]]` (or a worker's name) on the first line runs that
|
|
14
|
+
model past every ClaudeRipple rule and still shows in the agents panel, so a session no longer
|
|
15
|
+
starts models outside it to get one. It also says what a brief should state, to check that the
|
|
16
|
+
report did what was asked, and to resume a worker only within 5 minutes of its last reply.
|
|
17
|
+
|
|
18
|
+
## 0.12.1 — 2026-10-21
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **One web setting covers page reading too.** WebFetch downloads a page and has the small, fast
|
|
23
|
+
model answer from it; that answer now comes from the model picked for web search, as WebSearch
|
|
24
|
+
already did, instead of Haiku on Claude quota. The Clients row is "Web search & page reading". If
|
|
25
|
+
that model fails before answering, the request goes where it went before.
|
|
26
|
+
- **Design work takes the heavy level of the frontend + design ladder**, whatever tier it was
|
|
27
|
+
given: a task judged design runs on the heavy workers, since its result depends on the model's
|
|
28
|
+
taste rather than on difficulty. Frontend tasks still pick by tier.
|
|
29
|
+
|
|
3
30
|
## 0.12.0 — 2026-10-21
|
|
4
31
|
|
|
5
32
|
### 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.
|
|
@@ -11,7 +11,11 @@ export const AUTO_DESCRIPTION = 'Delegate any task here; ClaudeRipple picks the
|
|
|
11
11
|
'If you know how hard the task is, put [[ripple: light]], [[ripple: standard]], [[ripple: hard]] or [[ripple: max]] on the prompt\'s first line: ' +
|
|
12
12
|
'light = lookups, mechanical edits, one small fix; standard = carry out a designed change (even across several files), research, summarize, read-only review; ' +
|
|
13
13
|
'hard = subtle debugging, security review, or work where a wrong call is costly; max = planning, design or architecture where the deliverable is the decision itself. ' +
|
|
14
|
-
'Size, file count or the brief\'s warnings do not by themselves make a task hard. Leave the marker out when unsure, and ClaudeRipple judges it.'
|
|
14
|
+
'Size, file count or the brief\'s warnings do not by themselves make a task hard. Leave the marker out when unsure, and ClaudeRipple judges it. ' +
|
|
15
|
+
'To run one particular model instead (the user asked for it, or a comparison needs it), name it on the first line: [[ripple: opus@high]] for a Claude model (opus, sonnet, haiku), or [[ripple: <worker agent name>@<effort>]]. ' +
|
|
16
|
+
'It runs as named, past every ClaudeRipple rule, and still shows in the agents panel; never start a model another way, such as claude -p, to get around the rules. ' +
|
|
17
|
+
'A brief states the scope, what must not be touched, the done criteria and how to verify them. Read the report for whether it did what was asked before using it. ' +
|
|
18
|
+
'Continue a worker with SendMessage only within 5 minutes of its last reply; later its cache has lapsed, so start a new one with a summary of what is done and what is left.'
|
|
15
19
|
|
|
16
20
|
/**
|
|
17
21
|
* The tier markers the main chat may write on a ripple:auto prompt, as the judge's verdict they
|
|
@@ -171,6 +175,20 @@ export function workerRule(domain: Domain, settings: Settings): { rule: WorkerPi
|
|
|
171
175
|
return ladder ? { rule: key, ladder } : { rule: 'general', ladder: settings.rules.general }
|
|
172
176
|
}
|
|
173
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Visual design depends on the model's taste, not on how hard the task is (2026-10-21: a store
|
|
180
|
+
* screenshot redesign judged "design, standard" ran on the light pair). Under the ui ladder a design
|
|
181
|
+
* verdict on the light or standard rung is raised to the hard rung; frontend, other ladders and
|
|
182
|
+
* verdicts already on hard or plan are untouched.
|
|
183
|
+
*/
|
|
184
|
+
export function domainVerdict(verdict: WorkerVerdict, rule: WorkerPick['rule']): { verdict: WorkerVerdict; raised?: 'design' } {
|
|
185
|
+
if (rule !== 'ui' || verdict.domain !== 'design') return { verdict }
|
|
186
|
+
const rung = rungOf(verdict)
|
|
187
|
+
if (rung !== 'light' && rung !== 'standard') return { verdict }
|
|
188
|
+
const { sure: _sure, ...rest } = verdict
|
|
189
|
+
return { verdict: { ...rest, ...TIER_MARKERS.hard! }, raised: 'design' }
|
|
190
|
+
}
|
|
191
|
+
|
|
174
192
|
export type Resolution = { target?: TierTarget; effort: Effort; rung?: Rung; position?: number; skipped?: { rung: Rung; position: number; model: string; reason: string }[]; fallbackReason?: string }
|
|
175
193
|
|
|
176
194
|
/** 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) => {
|