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 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
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "private": true,
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "type": "commonjs"
5
5
  }
@@ -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 resolved = explicitCandidate && candidateUsable(explicitCandidate, s, usable, providers)
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
- expect(sent[0]?.subagentType).toBe(rule === 'ui' ? 'gpt-6-astra' : rule === 'backend' ? 'gpt-6-1-sol' : 'deepseek-v4-1-flash')
667
- expect(JSON.parse(b.files.get('/home/test/.clauderipple/logs/worker-picks.jsonl')!)).toMatchObject({ rule, domain, rung: 'light' })
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) => {