SLOPSHOPPER

smart-compact

/smart-compact: write custom /compact instructions from the session, then compact with them

newcommandstatusmodeltimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · smart-compact
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /smart-compact ⎿ smart-compact: compacting with these instructions: ⎿ smart-compact: ⎿ smart-compact: OK ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ smart-compact: smart-compact: compacting
README

<!-- generated by scripts/readme/build.mjs. do not edit by hand. -->

<img alt="Harness Console: one local app for Harness Firmware repositories, with a usage tracker for every Claude Code and Codex login in CLIProxyAPI. The app runs on port 43127, the tracker on port 4545. The tracker reads 4 Claude windows and 2 Codex windows through the proxy at startup, on every page load and on Refresh now, and rescans token totals every 300 seconds. It writes no credentials and refreshes no tokens." src="assets/readme/masthead-light.svg" width="100%">

Harness Console is one local app for working with Harness Firmware repositories on Windows, with a usage tracker built in. It clones and updates your GitHub repositories, creates new projects from the Harness-Firmware template, brings template skill changes into Harness clones you already have, installs Harness skills into DSH, and shows rate-limit and token usage for every Claude Code and Codex login held by CLIProxyAPI. Both servers listen on 127.0.0.1 only. Node only, no dependencies.

This repository was formerly UsageTracker; the tracker now lives in usage/.

Run

Node.js 24, Git and the GitHub CLI (gh) must be on PATH. Nothing is installed from npm.

Double-click Launch Harness Console.vbs, or run from a terminal:

node launcher.mjs

The launcher serves the app at http://127.0.0.1:43127, starts the usage tracker on port 4545 when nothing answers there, and opens the app in your browser. The USAGE_PORT environment variable changes the tracker port for both the launcher and usage/server.mjs.

Every launch runs GitHub's latest main. The launcher first stops a running Harness Console, then fetches origin/main, fast-forwards this folder, and starts the updated code. It skips the update and runs the copy on disk when the folder is on another branch, has uncommitted changes, holds commits GitHub does not, or GitHub is unreachable; the page footer shows the running build and, after a skip, the reason.

Every launch then starts fresh. A Harness Console already on port 43127 is asked to quit and is replaced, and any usage tracker on port 4545 (a node server.mjs answering /api/usage, including one started from an older checkout) is stopped and started again from usage/. When the running app is in the middle of an operation (clone, update, create, skill sync or DSH install), the launcher keeps it, leaves the folder as it is, and only opens a new tab. Another service on port 43127 is left running and the launcher exits with an error; any other service answering on port 4545 is reused.

Select Quit app at the bottom of the page to stop Harness Console, and with it the tracker if this launch started it. Closing the browser tab leaves both running. In a terminal, Ctrl+C stops the launcher.

If the app asks you to sign in, Sign in runs gh auth login --web and shows the one-time code in the activity log. Credentials stay with the GitHub CLI, and an existing login is reused.

App tabs

The app has five tabs (Usage, Repositories, New project, Skill sync, DSH skills) and opens on Usage. The sidebar lists every repository your GitHub account can reach, private ones included; its ↻ button reloads the list. Organization access depends on your GitHub authorization and any organization SSO requirements.

Usage

Embeds the usage tracker from port 4545. Until the tracker answers, the tab shows a waiting message.

Repositories

Choose a repository, check its destination (~/CoreWise/<name>), then select Clone main. The app creates a normal Git working folder with origin and main tracking origin/main; a repository without main shows an error. Open on GitHub, next to the repository name, opens the repository's page.

When the destination already holds a clone of the same repository, the button reads Update main: it fetches origin/main, switches to main if another branch is checked out, and fast-forwards. It refuses, changing nothing, when tracked files have uncommitted changes, when local main has commits GitHub lacks, or when the folder points at a different origin. Repositories with the same name share a destination, so move an existing folder before cloning another repository with that name.

New project

Name the repository, add an optional description, choose private or public, and open Skills to leave out optional Harness skills. The skill names are read live from the template on GitHub, and their groups from the template's .agents/template-manifest.json. Skills the manifest marks required always stay, and a skill that a kept skill needs cannot be left out. Create repository runs gh repo create <name> --template ryanportfolio/Harness-Firmware --clone inside ~/CoreWise, then follows the manifest inside the new clone: it deletes the paths listed as template-only, writes README.md from the manifest's stub, and checks that every required file is still there. A clone without a valid manifest stops before anything is committed or pushed; the GitHub repository already exists by then. Deselected skill folders are removed, marked off in .claude/settings.json and listed in .agents/removed-skills.json. The app then commits and pushes. The project ends up on GitHub and cloned locally with main tracking origin/main. Creation needs a signed-in GitHub CLI.

Skill sync

Brings template skill changes into the Harness repositories cloned in ~/CoreWise. Opening the tab, or Check repositories, fetches origin/main for every clone and compares each skill folder with the template's main, using a template copy cached in ~/.corewise-cloner/harness-firmware.git. A skill folder that matches some earlier template version was never edited in that repository, so it is offered as an update. Skills the template added are offered as additions, and skills it dropped as removals. Skills turned off in .claude/settings.json are not added back. Skills the template ships only for Codex, a .agents/skills folder with no Claude copy, get their own Codex only row for additions and updates. Only repositories with .agents/skill-modes.json are offered them, and a skill set to disabled there is not added.

Nothing starts selected. Tick single skills, use a repository's All updates and additions box, or Select all updates, additions and edited for every repository, which also ticks skills edited in the repository; Unselect all clears everything. Removals are ticked one by one. Skills edited in the repository are listed with the files that differ; ticking one replaces the edited copy with the template's and loses those edits. To merge edits by hand instead, use the sync-starter skill inside that repository. Clicking a skill name opens a comparison of the repository's checked main with the template copy a sync would write, including the .agents copy where the sync writes one. It says in plain words what a sync does to that skill, when each side last changed it and in which commits, and when both copies last matched. Each difference is labelled by who made it: an edit in the repository (a sync undoes it), a newer template change (a sync brings it in), or both; the changed words are highlighted, and the raw diff sits at the bottom. The comparison changes nothing; only the checkbox beside the name selects the skill.

Sync and merge asks for a second click. For each repository it then checks the skip list and GitHub again, checks the choices against a fresh fetch, builds the change in a temporary Git worktree on origin/main, and copies the skill folders and registry entries: .agents/skill-modes.json, keeping any Codex mode the repository already chose; .agents/skill-sources.json, which takes the template's reviewed hash for each native skill the sync writes; and .agents/CODEX-SKILL-COMPATIBILITY.md, where an added skill the table does not list goes into the row the template gives it and a removed skill leaves its row. It runs the repository's own checks where they exist: sync-codex-skills.mjs --check and test-codex-contract.mjs before and after the change, and removed-skills.mjs, which only warns. A check that passed on main and fails after the change stops that repository with nothing committed or pushed; a check already failing on main is reported in the pull request. No dependencies are installed and no test suites run, and the merge does not wait for the repository's CI. The commit goes to a new branch, harness-sync/<template commit> (with a numbered suffix when that branch already exists); gh pr create opens a pull request into main that lists the changes and each check result, gh pr merge --squash merges it straight away, and the branch is deleted. A merge GitHub refuses (branch protection, required checks, a conflict with a newer main) leaves the pull request open and marks that repository failed. Links to the pull requests appear under the log. An edited skill that changed again on GitHub since the check is skipped. Your local folders, branches and uncommitted work are never changed; use Update main on the Repositories tab to pull the result.

A repository can lock a skill so syncs leave it alone. Lock next to a skill writes its name and an optional reason to .agents/skill-locks.json in that repository and merges the change the same way a sync does. A locked skill moves to the Locked row with its reason and has no checkbox, so neither Select all updates, additions and edited nor a repository's box can pick it, and a sync started from a page shown before the lock skips it too. Unlock removes the entry. A lock file that cannot be read stops that repository with an error rather than unlocking its skills.

Some repositories are never synced. Each one is paused under projects in the settings file with a reason, such as a frozen job take-home or a repository with no firmware files; the first start imports the list Harness Console kept in sync-skip.json. Repositories archived on GitHub are skipped too; each Harness clone is looked up with gh api repos/<owner>/<name>. Skipped repositories appear on the tab with their reason, offer no skills to pick, and the server refuses them. When that lookup fails, or the repository's default branch is not main, the repository shows the error and cannot be synced.

Stale in worktrees lists each checkout of a clone, the clone folder itself and every linked Git worktree, whose committed skill copies are an older template version than the repository's main. A session loads skills from its own checkout, so a branch cut before a skill sync keeps the old copies even when main is current. The list only reads Git metadata. Merge main into the branch to update it; copies edited in the branch are not listed.

DSH skills

Installs skills from the Harness-Firmware clone at ~/CoreWise/Harness-Firmware into DSH's global skill folder, ~/.dsh/skills (the DSH_HOME environment variable moves ~/.dsh). Skills are read from that clone's origin/main after a fetch, so uncommitted and untracked files in the working folder never travel. Files that may hold secrets, caches and generated output are reported and skipped.

Check DSH skills previews every skill without changing anything, grouped as Add, Update, Changed in DSH, Current and Cannot install. Each one is checked with DSH's own skill parser, loaded from the DSH install, so start DSH once before the first check. A skill changed in DSH since Harness Console installed it, or installed there by hand, gets a Compare button that shows the diff and a Back up the DSH copy box, ticked by default. With the box ticked the old copy moves to ~/.corewise-cloner/dsh-backups; cleared, it is replaced without a backup.

Install in DSH asks for a second click. Each skill is written to a staging folder beside the skill folder, checked with DSH's parser, swapped in by rename and checked again in place. Any failure puts the previous copy back, so a skill ends up either fully installed or unchanged.

Where state lives

  • settings.json in the app-data folder (%APPDATA%\Harness Firmware on Windows): the workspace folder, paused projects and the last selected repository. No credentials. The first start imports ~/CoreWise, ~/.corewise-cloner/preferences.json and sync-skip.json from an existing install. A file that cannot be read stops every sync and lock, and the app never overwrites it.
  • The workspace folder (~/CoreWise/ for an existing install): repository clones, including the Harness-Firmware clone the DSH tab reads.
  • ~/.corewise-cloner/harness-firmware.git: the template cache for Skill sync.
  • ~/.corewise-cloner/dsh-installs.json and dsh-backups/: what the DSH tab installed, and the copies it replaced.
  • usage/.state/: the tracker's snapshot cache (usage-cache.json), token cache and price table.

The CoreWise and .corewise-cloner names come from the app's earlier name and are kept so existing clones and state carry over. GitHub credentials stay with the GitHub CLI.

The app accepts requests only for its exact loopback host and browser origin, and every change needs a per-session token. Clone URLs are built from repository identities the GitHub API returned; the browser cannot supply a URL or a command. Git and gh run with argument arrays and no shell. The tracker answers only 127.0.0.1 or localhost on its own port. Its one change, POST /api/refresh (sent by Refresh now, a page load and the sign-in watch), needs a same-origin request with a custom header, so another website cannot trigger it. Sign in again and Add account only open CLIProxyAPI's own sign-in page; the tracker server has no sign-in route.

Tests

node --test test/*.test.mjs

The tests use temporary local Git repositories and an injected GitHub adapter, so they do not prove live GitHub access or private repository authorization. For an isolated manual run, start the app server alone with its own folders; it does not start the tracker:

node server.mjs --port 43128 --root "C:\path\to\test-clones" --settings "C:\path\to\test-settings.json" --open

Usage tracker

The tracker in usage/ serves one local page for every Claude Code and Codex login held by CLIProxyAPI: how much of each rate-limit window is used, when it resets, and the tokens each provider used over the last 7, 30 or 90 days. CLIProxyAPI is required. It holds the logins and their tokens and makes each usage call for the tracker; the token totals come from the transcripts the CLIs leave on this machine. 7 source files, no dependencies.

Setup

Install and sign in to CLIProxyAPI; the tracker finds it. Sign each Claude and Codex account in through the proxy, either in its own panel or with Add account on the tracker page. The tracker has no settings file and no account list of its own: every login the proxy lists appears on the page.

The tracker reads two files from %LOCALAPPDATA%\CLIProxyAPI (the USAGE_PROXY_DIR environment variable points it at another folder):

  • config.yaml: only the top-level host and port lines. Without them the proxy is expected at 127.0.0.1:8317; a host that is not a loopback address still means 127.0.0.1.
  • dashboard-key.txt: the management key, sent as a bearer token to the proxy's management API.

Both are read again on every call, so a proxy restart or a new key needs no tracker restart. When the key file is missing or the proxy does not answer, a banner says so and the page keeps showing the last saved numbers.

Run the tracker alone

The launcher starts it. To run it without the app:

node usage/server.mjs

Open http://127.0.0.1:4545. The page rereads the tracker's snapshot every 30 seconds and the reset countdowns tick every 15; every page load and "Refresh now" ask the tracker to poll the proxy again and start a transcript rescan.

What it reads and writes

The tracker reads the proxy's login list, the usage of each login through the proxy, the transcripts on this machine, and LiteLLM's public price table. It writes only inside usage/.state/ (USAGE_STATE_DIR moves it):

  • usage-cache.json: the last good snapshot of every login, so the page has numbers right after a restart or while the proxy is down. A login the proxy no longer lists is dropped from the page and from this file.
  • tokens/: parsed transcript records.
  • pricing.json: the price table.

On its first start after an upgrade it moves state an older version left in the repository root's .state/ into usage/.state/, and deletes the per-account snapshot files older versions wrote there.

It never reads or writes a CLI credential file and never refreshes a token; CLIProxyAPI owns the logins and their refresh. Each vendor call goes out as POST /v0/management/api-call, naming the login and one of three fixed upstream URLs, with the header Authorization: Bearer $TOKEN$. The proxy puts that login's token in place of $TOKEN$ and makes the call. Codex usage calls also send a chatgpt-account-id header with the account id from the login's id_token, so a login in a ChatGPT workspace, or with several accounts, gets that account's usage and not the default one's.

What it shows

The page opens with a ledger, one row per proxy login, titled by its email. Columns are Session (5h) and Weekly, then any other window a login reports, then Status. Each cell shows the percentage used, a bar and the reset time; bars darken as they fill: tan under 70% used, brown from 70% and near-black from 90%. A line above the table gives the Codex weekly room left and names one Claude login: the first to reset among those at 90% weekly or more, else the next to reset.

ProviderWindowsExtras
Claude CodeSession (5h), Weekly, Weekly Opus, Weekly Sonnet (the last two when the plan reports them), plus any model-scoped cap the API lists in limits, labelled Weekly or Session and the model nameplan tier; extra-usage credits when enabled
Codexthe windows the API reports as active, labelled by length (Session (5h), Weekly)plan; credit balance; number of banked rate-limit resets

The Status column reads the first of these that applies:

  • Disabled in proxy: the login is disabled in CLIProxyAPI. The tracker does not fetch it and keeps any numbers it had.
  • Sign-in needed: the proxy is running and the login's credential was rejected (the rules are below). This wins over old numbers and a rate limit, because signing in is the only fix; the account line still shows the age of any numbers the row keeps.
  • As of and an age: the row has earlier numbers but the last fetch failed, the proxy is down, or the tracker has just started from its saved snapshot. This holds even when the proxy flags the login for a cooldown or an upstream error. The row keeps the last numbers it had, each shown as "Last: N%", with the error under them, or in the banner when the proxy is down.
  • Proxy unreachable: the tracker could not list the proxy's logins, and the row has no earlier numbers. Any failure of that list counts: the proxy does not answer, the management key is missing or refused, the proxy answers with another HTTP error, or the list it returns is malformed. The row is greyed and offers no sign-in.
  • Rate limited: the usage API answered 429 and the row has no earlier numbers. Under it: the time the login can be fetched again. Nothing fetches it at that time on its own; the next page load, Refresh now or sign-in watch does.
  • Fetch failed: the usage call failed for another reason, such as an upstream error, and the row has no earlier numbers. The error is shown under it.
  • Live: the last fetch worked and the login needs no new sign-in. CLIProxyAPI also marks a login unavailable during a quota cooldown (quota exhausted) or after an upstream error (transient upstream error); such a login stays Live with its current numbers, and the proxy's message shows under them.

A login's credential counts as rejected, and the row reads Sign-in needed, when any of these marks it: the usage API answered 401 or 403; the proxy could not produce a token for the login; the proxy's status message starts with unauthorized, payment_required, invalid_grant or token expired; or a proxy cooldown on the login has the reason unauthorized, payment_required or invalid_grant, or HTTP status 401 or 403. Failing those, a login with any other cooldown, or whose status message is one of the proxy's cooldown or upstream-error markers such as quota exhausted, does not need a sign-in. Otherwise two more cases mark it: a status message that mentions 401, 403, unauthorized, authentication, permission, forbidden, invalid grant, revoked or expired; and a login the proxy marks unavailable with no status message and no retry time.

A window whose reset time has passed shows its last number as "Last: N%" and "current reset unknown" until the next fetch. Two proxy logins for the same account both appear, each warned "Same account as another login in the proxy."

Signing in

When a row reads Sign-in needed, the row shows Sign in again, even when the usage API has also rate limited the login; its rate-limit line then leaves out the Refresh now hint. A login that is only rate limited, a login in a proxy cooldown or after an upstream error, and a fetch that failed for another reason get no link: signing in again does not fix them. That button and Add account in the header open the proxy's sign-in page, /management.html#/oauth on the proxy's address, in a new tab. Sign in there. The tracker page then refreshes every 5 seconds for up to 10 minutes until a new login, or the one being signed in again, fetches fine with no auth failure reported (a cooldown flag does not hold it back), and reports "Signed in as" and the email, or "No new sign-in seen; try again". One watch runs at a time, and Add account stays off while the proxy is down.

Polling

  • There is no poll timer. Each login is fetched at startup, on every page load, on Refresh now and every 5 seconds during a sign-in watch. Refreshes that arrive while a poll runs share it. One that arrives after the poll has published its accounts, while it only saves them, waits for it and then polls again, so a refresh never answers with numbers fetched before it was asked.
  • A login that fetched fine less than 60 seconds ago, with no auth failure reported, keeps its numbers instead of being fetched again (a quota cooldown or upstream-error flag from the proxy does not change that), so a sign-in watch does not refetch every account every 5 seconds.
  • A 429 from a usage API parks that login for the response's Retry-After time, or 5 minutes without one. Polls in that time do not call the API for it; the first poll after it does. If the proxy rewrites the login's file in that time (a new sign-in, or a token refresh), the next poll calls the API once; another 429 parks it again.
  • The Claude plan comes from the profile endpoint, fetched after a login's usage call succeeds and then kept for as long as the tracker runs. A failed profile fetch is not kept, so the login's next successful usage fetch asks again.
  • Transcripts are rescanned every 300 seconds and on every refresh. One scan runs at a time, and a refresh does not wait for it; while it runs, the token panels say "scanning...".

Token totals

Neither provider reports token counts over an

Source 1 files
hooks/register.ts 86 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3// Asked of a fork of the session, which sees the whole transcript. Its answer is
4// passed to /compact as the custom instructions the summarizer follows.
5const REVIEW_PROMPT = `Review this whole conversation so far and write the custom instructions to pass to /compact. The summarizer reads them as directions for what to keep, so write imperatives to it.
6
7Keep, in priority order:
8
91. The user's goal for the session and any standing instructions or preferences they gave in it (style, scope limits, things to never do).
102. Current state: repo, branch, worktree path, PR URL, last commit SHA, deploy or preview URL, files created or changed.
113. Decisions made and the reason for each, including options the user rejected.
124. What is verified and how, and what is still unverified.
135. Open work: the next concrete step, pending questions to the user, background tasks still running.
146. Failed approaches and gotchas that would otherwise be retried.
157. Exact identifiers to keep verbatim: paths, commands, error strings, IDs, numbers.
16
17Tell the summarizer to drop resolved tangents, raw tool output, superseded plans, and anything re-readable from files or CLAUDE.md. Never invent a fact; if a state item is unknown, leave it out. Keep the block under about 300 words; cut from the bottom of the priority list first.
18
19Return exactly one fenced \`text\` block and nothing else. When the session's replies follow a named style or output style (caveman ultra, for example), make the first line \`Always use <that style>.\` so it survives the compaction; otherwise leave that line out.
20
21\`\`\`text
22Always use <reply style>.
23Goal: <one line>.
24Keep: <standing user instructions>.
25State: <branch, worktree, PR, SHA, files>.
26Decisions: <decision, why>; ...
27Verified: <what, how>. Unverified: <what>.
28Next: <next step>; open questions: <...>.
29Don't retry: <failed approach, cause>.
30Verbatim: <paths, commands, errors>.
31Drop tool output, resolved tangents, and superseded plans.
32\`\`\`
33
34Omit any line with nothing to say.`
35
36// The fork answers with one fenced `text` block; take its body.
37const fencedBody = (reply: string) => {
38  const match = reply.match(/```(?:text)?\r?\n([\s\S]*?)\r?\n```/)
39  return (match ? match[1] : reply).trim()
40}
41
42export const register: Register = on => {
43  on('session.start', async ($, e, next) => {
44    await $.command.register({
45      name: 'smart-compact',
46      description: 'Write custom /compact instructions from this session, then compact with them',
47    })
48    return next(e)
49  })
50
51  on('command.run', { command: 'smart-compact' }, async $ => {
52    $.ui.status('smart-compact: writing instructions')
53    const reply = await $.model.fork({ prompt: REVIEW_PROMPT })
54    if (!reply.isAnswered) {
55      $.ui.status(undefined)
56      return { text: `review failed (${reply.reason}); nothing compacted. Run /compact yourself.` }
57    }
58
59    const instructions = fencedBody(reply.text)
60    $.ui.status('smart-compact: compacting')
61    // The engine refuses to run a command from inside this hook, which still
62    // holds the command's turn; run it from a timer once the command finishes.
63    $.clock.after(500, () => void compactWhenIdle($, instructions, 10))
64
65    return { text: `compacting with these instructions:\n\n${instructions}` }
66  })
67}
68
69const compactWhenIdle = async ($: EngineInterface, instructions: string, triesLeft: number) => {
70  try {
71    // `$.session.compact` is refused in SDK sessions (the desktop app's Code
72    // tab); running `/compact` itself works in those and in the terminal.
73    await $.command.run({ command: 'compact', args: instructions })
74    $.ui.status(undefined)
75  } catch (err) {
76    // Retry only the refusal for a command run from inside a hook that still
77    // holds the turn; a cancelled or interrupted /compact stays cancelled.
78    if (triesLeft > 1 && /hook/i.test(String(err))) {
79      $.clock.after(1000, () => void compactWhenIdle($, instructions, triesLeft - 1))
80      return
81    }
82    $.ui.status(undefined)
83    $.ui.log(`smart-compact: compaction failed (${String(err)}). Paste the instructions above after /compact.`)
84  }
85}
86