SLOPSHOPPER

effort-cycle

/effort-next raises effort low → medium → high → xhigh → low, skipping max; bind it to a key with command:effort-next

newcommandtoasttimer
v0.1.0no licenseupdated 2026-09-29daanqq/agent-config/claude/skills/effort-cycle
A shopper browsing a rack in a slop shop
README

Agent configuration platform

One repository for Pi, Claude Code, and portable Agent Skills. The clients keep their usual HOME paths; links.json connects selected configuration files and skill directories to this checkout.

Layout

  • common/skills/: portable skills with closed relative dependencies.
  • common/skills-maintenance/: one source lock, scoped inventory, checker, and semantic overlays.
  • pi/: Pi configuration, extensions, prompts, and client-specific skills.
  • claude/: Claude Code configuration, hooks, themes, local plugin source, and client-specific skills.
  • scripts/install.py: selective links, private backups, and conservative restore.

Client skill directories contain repository-relative links to the shared skills. HOME receives individual shared skill links in .agents/skills/ too; existing installer-owned skills such as Plannotator are not replaced.

retro stays client-specific even though its text matches: it depends on the client-specific writing-for-agents skill. mr-echat remains a Pi extension; the Claude skill is retained only in the Claude configuration.

Prepare and check

Requirements: Git, Python 3.10+, and the clients' existing dependencies. Node.js and npm are needed for Pi extensions. No new dependencies were introduced.

cd ~/agent-config
make check
python3 common/skills-maintenance/check.py --check-sources
python3 scripts/install.py --backup-existing

The last command is a dry run. --backup-existing lets it plan replacing occupied paths; no HOME paths are changed without --apply.

--check-sources reads locked Git revisions from ~/.cache/checkouts/. It does not fetch. Missing source caches are reported, not silently reconstructed.

Exact upstream copies retain their original formatting. .gitattributes permits upstream trailing whitespace only in the DnD and Vercel skill trees; source checks still require their complete contents to match the locked commits.

Connect HOME

This checkout is prepared separately; the original clients are not switched automatically. Review the dry run and stop Pi and Claude Code before connecting to avoid concurrent settings writes. Install the existing locked extension dependencies in the new checkout first:

make dep
python3 scripts/install.py --backup-existing --apply

Dependencies are not copied from the old clients, and make dep needs network access. The installer also links each declared Pi extension package's node_modules into HOME so the extension loader can resolve dependencies from the linked files. These directories stay local and untracked; existing HOME dependency directories are backed up and can be restored like configuration. Installing links before make dep is supported: dependency links remain dangling until npm creates the directories. Afterwards start new client sessions, or reload Pi's resources.

Existing files and skill directories are moved, not deleted, into a private run directory under ~/.local/state/agent-config/backups/. The installer prints its restore journal path. Correct links are left unchanged on repeated runs.

For an isolated HOME, use --home /absolute/path/to/existing/home. The same manifest is used; unknown files and runtime directories are left alone.

Restore

python3 scripts/install.py --restore <printed-run-directory>
python3 scripts/install.py --restore <printed-run-directory> --apply

Restore is also a dry run by default. It refuses to overwrite an installed link that another process replaced or changed. Backups and their journal remain available after restoration; they are not removed automatically.

Local data and credentials

The repository does not import authentication files, secrets, sessions, plugin caches, history, or the old repositories' .git directories. The original checkouts and their uncommitted changes remain available until you explicitly retire them. No history was rewritten and no remote was configured.

The existing Pi models.json contains a literal API key and is deliberately not linked or copied into this repository. pi/agent/models.example.json uses ${CLIPROXY_API_KEY} instead. Existing local ~/.pi/agent/models.json continues to work; on another machine, copy the example there and configure the variable privately. The installer does not perform that copy.

Claude and the Pi example use CLIProxyAPI through Tailscale at https://new.tail354056.ts.net; Pi's Codex provider retains the /backend-api suffix. Connect Tailscale with MagicDNS enabled before starting the clients. Existing private ~/.pi/agent/models.json files must be updated separately; changing the example does not change them. API keys stay unchanged. The SSH tunnel remains available as a fallback, with URLs changed back to localhost.

Claude's installed-plugin state and Pi's old checkout-specific .pi/settings.json are also excluded. Package/plugin declarations are retained in client settings; their installers own runtime installation state.

After activation, edit the files in this checkout. A client or third-party installer may replace a HOME symlink with a regular file. Re-run the installer in dry-run mode to detect it and reconcile those changes before replacing it.

Claude quota footer

The quota refresher uses ~/.pi/agent/secrets/cliproxy-management.json when that file exists. It must contain managementUrl and managementKey. If the file is absent, it reads CLIPROXY_MANAGEMENT_URL and CLIPROXY_MANAGEMENT_KEY from the environment inherited by Claude Code. An invalid existing file is reported as an error; it does not fall back to environment credentials.

The key must be the plaintext CLIProxyAPI management key, not a model API key or the bcrypt hash in the proxy configuration. Keep it outside this repository. Use https://new.tail354056.ts.net for managementUrl or CLIPROXY_MANAGEMENT_URL, not the separate panel's HTTPS port 18443. The management endpoint must be reachable and expose an enabled Claude account. The scripts require jq, curl, and setsid for background refreshes.

Skills

python3 common/skills-maintenance/check.py
python3 common/skills-maintenance/check.py --scope pi
python3 common/skills-maintenance/check.py --scope claude

Use the shared update-skills skill for upstream reconciliation. The global source lock is shared; client-specific behavior is preserved in scoped overlays. Refresh hashes only after classifying accepted changes, never to hide unrelated drift. See common/skills-maintenance/README.md.

Source 1 files
hooks/register.ts 81 lines
1import type { EngineInterface, On } from 'claude-code'
2
3/**
4 * The cycle; `max` is left out on purpose, so `max` and anything unknown
5 * step to `low`.
6 */
7export const LEVELS = ['low', 'medium', 'high', 'xhigh'] as const
8
9export const COMMAND = 'effort-next'
10
11export function nextLevelOf(current: unknown): string {
12  const index = LEVELS.findIndex(level => level === current)
13
14  return LEVELS[(index + 1) % LEVELS.length] ?? LEVELS[0]
15}
16
17export function register(on: On): void {
18  // /effort does not report the level back, so the last one set through it or
19  // seen on a model request is the current one; before either, and after
20  // /effort's picker, settings decide.
21  let current: unknown
22
23  on('session.start', async ($, e, next) => {
24    await $.command.register({
25      name: COMMAND,
26      description: 'Raise effort: low → medium → high → xhigh → low',
27      immediate: true,
28    })
29
30    return next(e)
31  })
32
33  on('turn.step', async function* ($, e, next) {
34    if (e.agentId === undefined) {
35      current = e.effort
36    }
37
38    return yield* next(e)
39  })
40
41  on('command.run', { command: 'effort' }, async ($, e, next) => {
42    const result = await next(e)
43    current = e.args.trim() || undefined
44
45    return result
46  })
47
48  on('command.run', { command: COMMAND }, async $ => {
49    current ??= await settingsLevelOf($)
50    const level = nextLevelOf(current)
51    current = level
52    // The engine refuses $.command.run inside a command.run hook, so the
53    // built-in /effort runs once this hook has answered; its output is the
54    // only line the press prints.
55    $.clock.after(0, () => {
56      $.command
57        .run({ command: 'effort', args: level })
58        .catch((error: unknown) => $.ui.toast(`/effort ${level}: ${error}`))
59    })
60
61    return {}
62  })
63}
64
65type EffortSettings = {
66  effortLevel?: unknown
67  modelSettings?: Record<string, { effortLevel?: unknown } | undefined>
68}
69
70// /effort saves the level per model (`claude-opus-5-5`, no `[1m]`), over the
71// top-level one.
72async function settingsLevelOf($: EngineInterface): Promise<unknown> {
73  const [settings, model]: [EffortSettings, string] = await Promise.all([
74    $.settings.read(),
75    $.session.model(),
76  ])
77  const perModel = settings.modelSettings?.[model.replace(/\[.*\]$/, '')]
78
79  return perModel?.effortLevel ?? settings.effortLevel
80}
81