From 16eac692442ac52fa8bf18a2f3a0760db6aa2906 Mon Sep 17 00:00:00 2001 From: Pratik Bhavsar Date: Tue, 15 Sep 2026 18:56:07 +0530 Subject: [PATCH 1/4] feat: add Tok, the shared token coach, with a truthful live turn Tok is one shared right-side conversation across every dashboard route. It answers with Token Meter evidence through bounded read-only MCP, turns a natural-language intention into a structured local goal contract, and produces opt-in weekly reviews through the user's signed-in Codex CLI. The live turn reports only observed boundaries: an indeterminate orbit, the in-progress read-only MCP tool, a completed-reading count, and a visual timer. Failures surface visibly with a recovery action instead of being announced only to assistive technology, so a missing local prerequisite is actionable rather than silent. Co-Authored-By: Claude Code --- README.md | 59 +- page.html | 360 +++ runtime-manifest.txt | 1 + specs/2026-09-11-goals-coach-design.md | 295 +++ specs/ARCHITECTURE.md | 78 +- specs/SECURITY.md | 45 +- specs/USER_GUIDE.md | 72 + specs/plans/2026-09-11-goals-coach.md | 328 +++ specs/plans/2026-09-13-tok-context-lens.md | 626 +++++ tests/test_coach.py | 2115 +++++++++++++++++ tests/test_coach_dashboard.py | 706 ++++++ tests/test_coach_http.py | 278 +++ tests/test_mcp_server.py | 207 +- token_meter/app.py | 282 ++- token_meter/coach/__init__.py | 19 + token_meter/coach/codex.py | 707 ++++++ token_meter/coach/contracts.py | 249 ++ token_meter/coach/service.py | 609 +++++ .../.agents/skills/token-meter-coach/SKILL.md | 58 + .../token-meter-coach/agents/openai.yaml | 12 + token_meter/services/agent_api.py | 48 +- token_meter_mcp.py | 144 +- 22 files changed, 7220 insertions(+), 78 deletions(-) create mode 100644 specs/2026-09-11-goals-coach-design.md create mode 100644 specs/plans/2026-09-11-goals-coach.md create mode 100644 specs/plans/2026-09-13-tok-context-lens.md create mode 100644 tests/test_coach.py create mode 100644 tests/test_coach_dashboard.py create mode 100644 tests/test_coach_http.py create mode 100644 token_meter/coach/__init__.py create mode 100644 token_meter/coach/codex.py create mode 100644 token_meter/coach/contracts.py create mode 100644 token_meter/coach/service.py create mode 100644 token_meter/coach/workspace/.agents/skills/token-meter-coach/SKILL.md create mode 100644 token_meter/coach/workspace/.agents/skills/token-meter-coach/agents/openai.yaml diff --git a/README.md b/README.md index 73c52e3..d7b36d4 100644 --- a/README.md +++ b/README.md @@ -59,12 +59,13 @@ troubleshooting, see the [User guide](specs/USER_GUIDE.md). | **Review history and spend** | Find expensive or slow work across sessions, projects, runtimes, platforms, and calendar ranges. | | **Compare models and execution** | Compare input, output, pace, wait, and workload shape without presenting weak matches as meaningful results. | | **Investigate tools and skills** | Find high-output, failing, repeated, unobserved, or deferred capabilities while keeping incomplete evidence explicit. | +| **Improve with Tok** | Ask Tok, Token Meter's token coach, questions from any page, turn a natural-language intention into one measurable goal, and opt into evidence-bounded weekly reviews through your signed-in Codex CLI. | | **Manage usage** | Check provider-reported limits, allocate a monthly budget, receive threshold notifications, and let Codex or Claude query bounded evidence through the local MCP. | ## Coverage **Runtimes:** Claude Code and Desktop Agent/Cowork, Codex CLI and desktop, -Cursor Agent/Composer, OpenCode, Kiro, and Pi. +Cursor Agent/Composer, OpenCode, Kiro, and Pi coding-agent sessions. | Platform | Status | Experience | | --- | --- | --- | @@ -79,6 +80,11 @@ Token Meter works when the agent keeps session evidence on your machine in a supported local store. Sessions that exist only in a cloud-hosted service may not be available to Token Meter. +For Pi coding-agent sessions, recorded token/cost and structural tool evidence +remain local and content-free. Wait time +is inferred between user and assistant events. Pi leaves semantic token classification +and context-window size unavailable when its records do not provide them. + ## First Five Minutes 1. Open **Sessions → Current sessions** and select an active run. @@ -86,6 +92,8 @@ not be available to Token Meter. Add a session budget if the run needs an attention limit. 3. After more sessions accumulate, use **Spend**, **Models**, **Tools**, **Efficiency**, and **Git** to review longer-term patterns. +4. Open **Tok** from the header to ask about the current page or draft one + measurable improvement goal. ## Product Tour @@ -139,6 +147,42 @@ evidence stays labelled beside the numbers. Token Meter Efficiency page with output per dollar, reasoning ratio, context load, and output per execution

+### Set a measurable goal with Tok + +Tok is one shared right-side conversation across the dashboard, not a set of +tabs. It can use the current page name and bounded, content-free Token Meter +evidence; messages stay in browser memory and disappear on refresh. A reply +starts with one content-free live line carrying a spinning indicator, the stage, +and a visual-only timer. The stage reads **Starting Tok**, then **Opening +Codex**, the Token Meter reading actually in progress such as **Reading your +usage history**, or **Checking the evidence**—only when those observed +boundaries occur. Once evidence has been read the line also counts the completed +readings, and a longer run says so plainly instead of stalling. After eight +seconds the send control becomes **Stop**, which ends only that active Tok turn; +the sent message remains and the panel offers **Retry**. Provider response time +is variable, and Tok never exposes model reasoning, raw events, or fabricated +progress. + +Failures are visible, not silent. A request that cannot be delivered becomes a +clearly marked error entry with **Retry** rather than words in Tok's voice, and a +missing local prerequisite—such as a Codex CLI that is not installed—appears as a +notice naming what to do, with a quiet attention dot on the closed **Tok** +control. + +Describe an improvement in ordinary language, review the structured metric, +target, evidence window, runtime, and weekday that Codex drafts, then explicitly +activate it. One active goal appears as a compact rail below the composer; open +it for progress, weekly controls, and the saved review. Token Meter stores that +contract and numeric snapshots—not your wording or Codex's prose. Answers show +one collapsed source line with at most three content-free measurements, rather +than an expanded evidence table. + +Weekly reviews are opt-in. When enabled, the signed-in local Codex CLI compares +the saved baseline with current covered evidence once on the selected weekday. +You can pause weekly reviews, run one manually, or clear the goal from the same +panel. Progress stays unavailable while a baseline is being collected or when +required evidence is missing. + ### Git **Git** pairs successful local pushes with covered spend, so you can see code @@ -160,7 +204,7 @@ and automatic installation are separate settings; both are on by default. Token Meter Settings view for local read-only agent connections

-The local MCP exposes seven read-only tools: +The local MCP exposes eight read-only tools: | Tool | Use | | --- | --- | @@ -170,6 +214,7 @@ The local MCP exposes seven read-only tools: | `sessions` | Select content-free session IDs using runtime, client, model, state, or time filters. | | `trace` | Read a standardized trace or sanitized runtime-native structure for one session. | | `stats` | Aggregate selected token, cost, timing, context, attempt, model-call, or tool metrics. | +| `goal` | Read the active structured Tok goal, progress, or latest numeric weekly review. | | `schema` | Discover fields, dimensions, units, limits, and availability semantics. | A comparison harness can call `sessions`, pass one returned ID to `trace`, and @@ -214,6 +259,16 @@ provider under its own terms. See the [User guide](specs/USER_GUIDE.md) for the full evidence semantics and [Security policy](specs/SECURITY.md) for the canonical boundary. +Tok uses the user's existing Codex CLI sign-in. Each turn runs in an +ephemeral, read-only workspace with general shell, file, browser, app, plugin, +memory, and sub-agent tools disabled; only allowlisted read-only Token Meter MCP +tools are exposed. Coach-launched MCP calls can reuse the running local server's +bounded warm evidence path and fall back once to the existing local read when +that optimization is unavailable; the result contract and allowlists do not +expand. The message you send and the bounded MCP results selected for the answer +may be processed by OpenAI under that account's terms. Tok never receives raw +trace content, and Token Meter never stores the conversation or model reasoning. + ## Documentation | Document | Use it for | diff --git a/page.html b/page.html index 38ad81e..61b33fe 100644 --- a/page.html +++ b/page.html @@ -502,6 +502,24 @@ .frustrationSection .signalRankTrack i,.frustrationSection .chatSignalTrack i{background:linear-gradient(90deg,var(--signal-primary),var(--signal-secondary))} .frustrationSection .termPanel .signalRankTrack i{background:linear-gradient(90deg,var(--signal-secondary),var(--signal-primary))} .frustrationSection .signalRateBadge{border-color:var(--signal-edge);background:var(--signal-wash);color:var(--signal-secondary)} +/* global-coach-v1 */ +.coachTrigger{position:relative;display:inline-flex;align-items:center;gap:8px;min-height:34px;border:1px solid rgba(127,219,242,.26);border-radius:9px;background:linear-gradient(180deg,rgba(0,188,235,.13),rgba(27,160,225,.055));padding:6px 10px;color:var(--fg);font:inherit;font-size:12px;font-weight:760;white-space:nowrap;cursor:pointer;box-shadow:var(--inset),0 7px 22px rgba(0,0,0,.18);transition:border-color .16s,background .16s,transform .16s}.coachTrigger:hover,.coachTrigger:focus-visible{border-color:rgba(127,219,242,.56);background:linear-gradient(180deg,rgba(0,188,235,.2),rgba(27,160,225,.08));outline:none}.coachTrigger:active{transform:translateY(1px)}.coachTriggerSigil{position:relative;width:17px;height:17px;flex:none;border:1px solid rgba(127,219,242,.34);border-radius:6px;background:radial-gradient(circle at 35% 28%,rgba(127,219,242,.22),rgba(0,188,235,.06) 62%,transparent)}.coachTriggerSigil:before{content:"";position:absolute;inset:3px;border:1px solid rgba(127,219,242,.62);border-right-color:transparent;border-radius:50%;transform:rotate(-32deg)}.coachTriggerSigil:after{content:"";position:absolute;left:6px;top:6px;width:4px;height:4px;border-radius:1px;background:var(--orange3);box-shadow:0 0 8px rgba(127,219,242,.75);transform:rotate(45deg)}.coachTriggerName{letter-spacing:.015em}.coachTriggerDot{width:6px;height:6px;border-radius:50%;background:var(--faint);box-shadow:0 0 0 3px rgba(125,139,160,.1)}.coachTriggerDot[hidden]{display:none}.coachTriggerDot.warn{background:var(--warn);box-shadow:0 0 0 3px rgba(255,180,87,.14)} +#coach-panel{position:fixed;z-index:70;top:12px;right:12px;bottom:12px;width:min(404px,calc(100vw - 24px));overflow:hidden;border:1px solid rgba(127,219,242,.2);border-radius:14px;background:linear-gradient(180deg,rgba(17,25,34,.99),rgba(8,12,17,.995));box-shadow:0 34px 90px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.035),inset 0 1px rgba(255,255,255,.055);transform:translateX(calc(100% + 28px));transition:transform .25s cubic-bezier(.22,.8,.23,1);display:grid;grid-template-rows:auto minmax(0,1fr);color:var(--fg)} +#coach-panel [hidden]{display:none} +body.coachOpen #coach-panel{transform:translateX(0)} +.coachBackdrop{display:none;position:fixed;z-index:60;inset:0;border:0;background:rgba(0,0,0,.48);padding:0;backdrop-filter:blur(2px)} +.coachHeader{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:11px;align-items:center;padding:15px 15px 12px;border-bottom:1px solid var(--line);background:radial-gradient(circle at 12% -50%,rgba(0,188,235,.22),transparent 52%)}.coachMark{position:relative;display:grid;width:38px;height:38px;place-items:center;overflow:hidden;border:1px solid rgba(127,219,242,.34);border-radius:12px;background:radial-gradient(circle at 35% 30%,rgba(127,219,242,.24),rgba(0,188,235,.1) 44%,rgba(12,19,27,.88) 76%);box-shadow:inset 0 1px rgba(255,255,255,.08),0 8px 20px rgba(0,188,235,.08);isolation:isolate}.coachMark:after{content:"";position:absolute;inset:auto -9px -11px auto;width:28px;height:28px;border-radius:50%;background:rgba(199,167,255,.13);filter:blur(7px)}.coachMarkOrbit{position:absolute;z-index:1;inset:7px;border:1px solid rgba(127,219,242,.72);border-right-color:rgba(199,167,255,.3);border-bottom-color:transparent;border-radius:50%;animation:tokOrbit 7s linear infinite}.coachMarkOrbit:before{content:"";position:absolute;left:1px;top:2px;width:3px;height:3px;border-radius:50%;background:#dff9ff;box-shadow:0 0 7px var(--orange3)}.coachMarkCore{position:absolute;z-index:2;width:11px;height:11px;border:1px solid rgba(255,255,255,.72);border-radius:3px;background:linear-gradient(145deg,#dff9ff,var(--orange) 48%,var(--orange2));box-shadow:0 0 11px rgba(127,219,242,.52);transform:rotate(45deg);animation:tokCorePulse 2.8s ease-in-out infinite}.coachMarkSpark{position:absolute;z-index:3;left:17px;top:3px;width:4px;height:4px;border-radius:50%;background:var(--good);box-shadow:0 0 7px rgba(102,217,144,.72);transform-origin:2px 16px;animation:tokOrbit 5.4s linear infinite reverse}@keyframes tokOrbit{to{transform:rotate(360deg)}}@keyframes tokCorePulse{0%,100%{opacity:.78;box-shadow:0 0 8px rgba(127,219,242,.36)}50%{opacity:1;box-shadow:0 0 15px rgba(127,219,242,.72)}}.coachTitleGroup{min-width:0}.coachTitleRow{display:flex;align-items:center;gap:7px}.coachTitleRow h2{margin:0;font-size:15px;font-weight:820;letter-spacing:.01em}.coachRole{display:inline-flex;border:1px solid rgba(127,219,242,.2);border-radius:999px;background:rgba(0,188,235,.065);padding:2px 6px;color:var(--orange3);font:720 8px/1.2 ui-monospace,"SF Mono",Menlo,monospace;text-transform:uppercase;letter-spacing:.045em}.coachHeader p{margin:2px 0 0;overflow:hidden;color:var(--faint);font-size:10.5px;text-overflow:ellipsis;white-space:nowrap}.coachClose{display:grid;width:30px;height:30px;place-items:center;border:1px solid var(--line);border-radius:8px;background:rgba(255,255,255,.025);color:var(--dim);font-size:19px;line-height:1;cursor:pointer}.coachClose:hover,.coachClose:focus-visible{border-color:var(--line3);color:var(--fg);outline:none} +.coachNotice{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:9px;align-items:center;margin:10px 12px 0;border:1px solid var(--line2);border-radius:9px;background:rgba(255,255,255,.03);padding:9px 10px;color:var(--dim);font-size:10.5px;line-height:1.45}.coachNotice[hidden]{display:none}.coachNotice:before{content:"";width:7px;height:7px;border-radius:50%;background:var(--faint)}.coachNotice.good:before{background:var(--good)}.coachNotice.warn{border-color:rgba(255,180,87,.3);background:rgba(255,180,87,.07);color:#ffdcb0}.coachNotice.warn:before{background:var(--warn)}.coachNotice.bad{border-color:rgba(255,111,111,.32);background:rgba(255,111,111,.07);color:#ffd2d2}.coachNotice.bad:before{background:var(--bad)}.coachNoticeAction{border:1px solid var(--line2);border-radius:7px;background:rgba(255,255,255,.05);padding:5px 8px;color:var(--fg);font:inherit;font-size:9.5px;font-weight:720;white-space:nowrap;cursor:pointer}.coachNoticeAction[hidden]{display:none}.coachNoticeAction:hover,.coachNoticeAction:focus-visible{border-color:var(--line3);outline:none} +.coachActivityOrbit{position:relative;width:15px;height:15px;border:1.5px solid rgba(127,219,242,.2);border-top-color:var(--orange3);border-radius:50%;animation:coachSpin .85s linear infinite}@keyframes coachSpin{to{transform:rotate(360deg)}}.coachActivityText{display:grid;gap:2px;min-width:0}.coachActivityStage{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.coachActivityNote{color:var(--faint);font-size:9.5px;line-height:1.4}.coachActivityNote[hidden]{display:none} +.coachMessage.error .coachMessageMeta{color:var(--bad)}.coachMessage.error .coachMessageText{border-color:rgba(255,111,111,.3);background:rgba(255,111,111,.06);color:#ffd2d2}.coachBody{position:relative;min-height:0}.coachView{display:none;height:100%;min-height:0}.coachView.on{display:flex;flex-direction:column}.coachLive{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.coachScroll{min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin}.coachChatScroll{display:flex;flex:1;flex-direction:column;padding:18px 14px 10px}.coachIntro{position:relative;overflow:hidden;border:1px solid rgba(127,219,242,.16);border-radius:11px;background:linear-gradient(145deg,rgba(0,188,235,.09),rgba(255,255,255,.018) 56%);padding:15px}.coachIntro:after{content:"";position:absolute;right:-26px;top:-34px;width:98px;height:98px;border:1px solid rgba(127,219,242,.14);border-radius:50%}.coachEyebrow{color:var(--orange3);font:760 9px/1.2 ui-monospace,"SF Mono",Menlo,monospace;text-transform:uppercase;letter-spacing:.06em}.coachIntro h3{max-width:280px;margin:7px 0 5px;font-size:19px;line-height:1.2;letter-spacing:-.02em}.coachIntro p{margin:0;color:var(--dim);font-size:11.5px;line-height:1.52}.coachPrompts{display:grid;gap:6px;margin-top:10px}.coachPrompt{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;align-items:center;border:1px solid var(--line);border-radius:8px;background:rgba(255,255,255,.025);padding:9px 10px;color:var(--dim);font:inherit;font-size:10.5px;text-align:left;cursor:pointer}.coachPrompt:after{content:"\2197";color:var(--orange3)}.coachPrompt:hover,.coachPrompt:focus-visible{border-color:rgba(127,219,242,.34);background:rgba(0,188,235,.045);color:var(--fg);outline:none} +.coachMessages{display:grid;gap:10px;margin-top:auto}.coachMessages:not(:empty)+.coachIntro{display:none}.coachMessage{display:grid;gap:6px}.coachMessageMeta{display:flex;align-items:center;gap:6px;color:var(--faint);font-size:9px;font-weight:750;text-transform:uppercase}.coachMessage.assistant .coachMessageMeta{color:var(--orange3)}.coachMessageText{width:fit-content;max-width:92%;border:1px solid var(--line);border-radius:10px 10px 10px 3px;background:rgba(255,255,255,.035);padding:9px 10px;color:var(--dim);font-size:11.5px;line-height:1.52;white-space:pre-wrap;overflow-wrap:anywhere}.coachMessage.user{justify-items:end}.coachMessage.user .coachMessageText{border-color:rgba(127,219,242,.17);border-radius:10px 10px 3px 10px;background:rgba(0,188,235,.075);color:var(--fg)}.coachEvidence{display:grid;gap:4px;max-width:94%;padding-left:8px;border-left:1px solid rgba(127,219,242,.25)}.coachEvidenceRow{display:flex;align-items:baseline;justify-content:space-between;gap:10px;color:var(--faint);font-size:9.5px}.coachEvidenceRow b{color:var(--fg);font:700 10px/1.3 ui-monospace,"SF Mono",Menlo,monospace;text-align:right}.coachReplyActions{display:flex;gap:6px;flex-wrap:wrap}.coachInlineButton{border:1px solid rgba(127,219,242,.22);border-radius:7px;background:rgba(0,188,235,.055);padding:6px 8px;color:var(--orange3);font:inherit;font-size:9.5px;font-weight:720;cursor:pointer}.coachInlineButton:hover,.coachInlineButton:focus-visible{border-color:rgba(127,219,242,.48);color:var(--fg);outline:none}.coachGoalDraft{display:grid;gap:8px;width:100%;margin-top:2px;border:1px solid rgba(102,217,144,.22);border-radius:9px;background:rgba(102,217,144,.045);padding:10px}.coachGoalDraft strong{font-size:11.5px}.coachGoalDraft span{color:var(--dim);font-size:10px}.coachGoalDraft .coachInlineButton{justify-self:start;border-color:rgba(102,217,144,.3);background:rgba(102,217,144,.07);color:var(--good)} +.coachComposer{padding:9px 12px 12px;border-top:1px solid var(--line);background:rgba(7,10,14,.74)}.coachComposer form{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:7px;align-items:end;border:1px solid var(--line2);border-radius:10px;background:rgba(255,255,255,.035);padding:5px}.coachComposer textarea{min-height:42px;max-height:118px;resize:none;border:0;background:transparent;padding:7px;color:var(--fg);font:inherit;font-size:11.5px;line-height:1.4;outline:none}.coachComposer textarea::placeholder{color:var(--faint)}.coachSend{display:grid;width:34px;height:34px;place-items:center;border:1px solid rgba(127,219,242,.28);border-radius:8px;background:linear-gradient(180deg,var(--orange),var(--orange2));color:#041019;cursor:pointer}.coachSend:hover,.coachSend:focus-visible{filter:brightness(1.08);outline:none}.coachSend:disabled{opacity:.4;cursor:wait}.coachSend svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round;stroke-width:2}.coachPrivacy{display:flex;align-items:center;gap:6px;margin:6px 3px 0;color:var(--faint);font-size:9px}.coachPrivacy svg{width:10px;height:10px;fill:none;stroke:currentColor;stroke-width:1.7} +.coachPaneScroll{flex:1;padding:22px 14px 16px}.coachPaneHead{margin-bottom:12px}.coachPaneHead .coachEyebrow{margin-bottom:5px}.coachPaneHead h3{margin:0;font-size:17px;letter-spacing:-.015em}.coachPaneHead p{margin:4px 0 0;color:var(--faint);font-size:10.5px;line-height:1.45}.coachEmpty{display:grid;place-items:start;border:1px dashed var(--line2);border-radius:10px;background:rgba(255,255,255,.015);padding:17px}.coachEmpty[hidden],.coachCard[hidden]{display:none}.coachEmpty strong{font-size:12px}.coachEmpty p{margin:5px 0 12px;color:var(--dim);font-size:10.5px}.coachCard{position:relative;overflow:hidden;border:1px solid var(--line);border-radius:10px;background:linear-gradient(180deg,rgba(255,255,255,.045),rgba(255,255,255,.02));padding:13px;box-shadow:var(--shadow-soft)}.coachCard:before{content:"";position:absolute;inset:0 0 auto;height:2px;background:linear-gradient(90deg,var(--orange),var(--orange3),transparent)}.coachGoalName{margin:3px 0 0;font-size:14px;line-height:1.3}.coachGoalMeta{margin-top:4px;color:var(--dim);font-size:10px}.coachProgressTop{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-top:17px}.coachProgressTop span{color:var(--faint);font-size:9.5px;text-transform:uppercase}.coachProgressTop strong{font:780 20px/1 ui-monospace,"SF Mono",Menlo,monospace}.coachProgressTrack{height:7px;margin-top:8px;overflow:hidden;border:1px solid var(--line);border-radius:999px;background:rgba(255,255,255,.045)}.coachProgressTrack i{display:block;width:0;height:100%;border-radius:inherit;background:linear-gradient(90deg,var(--orange2),var(--good));transition:width .28s}.coachMetricPair{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px;margin-top:12px}.coachMetricPair div{min-width:0;border:1px solid var(--line);border-radius:7px;background:rgba(255,255,255,.02);padding:8px}.coachMetricPair span{display:block;color:var(--faint);font-size:8.5px;text-transform:uppercase}.coachMetricPair b{display:block;margin-top:3px;overflow:hidden;font:710 11px/1.35 ui-monospace,"SF Mono",Menlo,monospace;text-overflow:ellipsis;white-space:nowrap}.coachCoverage{margin-top:9px;color:var(--faint);font-size:9.5px}.coachSettingsRow{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:12px;padding:10px 0;border-top:1px solid var(--line)}.coachSettingsRow b{display:block;font-size:10.5px}.coachSettingsRow small{display:block;margin-top:2px;color:var(--faint);font-size:9px}.coachSwitch{position:relative;width:34px;height:19px;flex:none;border:0;border-radius:999px;background:rgba(255,255,255,.13);cursor:pointer}.coachSwitch:after{content:"";position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%;background:#fff;transition:left .15s}.coachSwitch[aria-checked=true]{background:var(--good)}.coachSwitch[aria-checked=true]:after{left:17px}.coachDanger{border:0;background:none;padding:4px 0;color:#ff9d9d;font:inherit;font-size:9.5px;font-weight:700;cursor:pointer}.coachDanger:hover,.coachDanger:focus-visible{color:#ffd0d0;outline:none}.coachWeeklyRecommendation{margin-top:4px;font-size:14px;line-height:1.35}.coachWeeklyEvidence{display:grid;gap:5px;margin-top:12px}.coachWeeklyMeta{margin-top:10px;color:var(--faint);font-size:9.5px}.coachWeeklyActions{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px}.coachPrimary{border:1px solid rgba(127,219,242,.31);border-radius:8px;background:linear-gradient(180deg,rgba(0,188,235,.2),rgba(27,160,225,.09));padding:7px 10px;color:var(--fg);font:inherit;font-size:10px;font-weight:750;cursor:pointer}.coachPrimary:hover,.coachPrimary:focus-visible{border-color:rgba(127,219,242,.57);outline:none}.coachPrimary:disabled{opacity:.4;cursor:wait} +@media(min-width:1181px){body.coachOpen .wrap{margin-right:414px;margin-left:max(0px,calc((100vw - 414px - 1320px)/2));max-width:1320px}} +@media(min-width:1024px) and (max-width:1180px){body.coachOpen .coachBackdrop{display:block}#coach-panel{width:min(404px,calc(100vw - 24px))}} +@media(prefers-reduced-motion:reduce){#coach-panel,.coachTrigger,.coachProgressTrack i{transition:none}.coachMarkOrbit,.coachMarkSpark{animation:none}.coachMarkCore{animation:none}} +.coachBody{display:flex;flex-direction:column}.coachView.on{flex:1}.coachStop{display:grid;width:34px;height:34px;place-items:center;border:1px solid rgba(255,111,111,.42);border-radius:8px;background:linear-gradient(180deg,#ff8f8f,#e26060);color:#2a0808;cursor:pointer}.coachStop[hidden]{display:none}.coachStop:hover,.coachStop:focus-visible{filter:brightness(1.08);outline:none}.coachStop svg{width:14px;height:14px;fill:currentColor}.coachSend[hidden]{display:none}.coachGoalRail{display:block;width:100%;margin-top:7px;border:1px solid rgba(102,217,144,.24);border-radius:7px;background:rgba(102,217,144,.05);padding:6px 8px;color:var(--good);font:700 10px/1 inherit;text-align:left;cursor:pointer}.coachGoalSheet{display:flex;min-height:0;flex:1;flex-direction:column}.coachGoalSheet[hidden]{display:none}.coachPaneScroll>.coachCard+.coachCard{margin-top:10px}.coachBack{display:inline-flex;align-items:center;gap:6px;margin-bottom:11px;border:1px solid var(--line);border-radius:7px;background:rgba(255,255,255,.03);padding:5px 9px;color:var(--dim);font:inherit;font-size:9.5px;font-weight:720;cursor:pointer}.coachBack:before{content:"\2190"}.coachBack:hover,.coachBack:focus-visible{border-color:var(--line3);color:var(--fg);outline:none}.coachActivityMessage{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:7px;align-items:center;border:1px solid rgba(127,219,242,.18);border-radius:10px;background:rgba(0,188,235,.04);padding:9px 10px;color:var(--dim);font-size:11px}.coachActivityTimer{color:var(--faint);font:700 9px/1 ui-monospace,"SF Mono",monospace}.coachEvidence details{max-width:94%;padding-left:8px;border-left:1px solid rgba(127,219,242,.25)}.coachEvidence summary{color:var(--faint);font-size:9.5px;cursor:pointer}.coachContextFocus{outline:2px solid rgba(127,219,242,.65);outline-offset:4px}@media(prefers-reduced-motion:reduce){.coachActivityMessage,.coachMessage{transition:none}.coachContextFocus{outline:none;transition:none}} +.coachReducedMotion #coach-panel,.coachReducedMotion .coachTrigger,.coachReducedMotion .coachProgressTrack i,.coachReducedMotion .coachActivityMessage,.coachReducedMotion .coachMessage{transition:none}.coachReducedMotion .coachMarkOrbit,.coachReducedMotion .coachMarkSpark,.coachReducedMotion .coachMarkCore{animation:none}.coachReducedMotion .coachContextFocus{outline:none;transition:none}.coachReducedMotion .coachActivityOrbit{animation:none;border-color:rgba(127,219,242,.34);background:radial-gradient(circle,var(--orange3) 0 34%,transparent 36%)} native projections --> mcp app --> mcp_queries --> mcp + app --> coach --> browser + coach --> mcp ``` The server binds to `127.0.0.1:8722`. `meter.py` is intentionally only an @@ -52,6 +55,7 @@ executable and import-compatibility facade; current composition lives in | HTTP transport | `token_meter/web/`, `page.html` | Serve the loopback API, routes, actions, and the single-file dashboard. | | Native clients | `menubar/`, Windows scripts | Render the compact `/menubar` payload and delegate deep review to the browser. | | Local MCP | `token_meter_mcp.py` | Return bounded read-only current-run or aggregate evidence over stdio. | +| Tok (Goals Coach) | `token_meter/coach/` | Validate one structured goal, cache numeric progress, schedule weekly review, and run schema-constrained Codex analysis with only allowlisted Token Meter MCP tools. | | Packaging | `runtime-manifest.txt`, `token_meter/packaging.py`, `scripts/` | Stage one manifest-owned runtime and install platform-native lifecycle components. | | Telemetry mapping | `token_meter/telemetry/` | Produce a pure OpenTelemetry-shaped mapping from an immutable privacy projection; perform no export or I/O. | @@ -145,10 +149,19 @@ resolved parentage and uses the direct parent's stable filesystem identity. Parent appearance or replacement invalidates the child; append-only parent growth does not invalidate an unchanged child summary. -Settings use bounded validation and atomic JSON replacement. Browser-local -navigation and presentation preferences remain in local storage. Mutation -routes require a local origin plus the current action token and accept only -allowlisted fields or discovered canonical identifiers. +Settings use bounded validation, a process-wide serialized read-modify-write +transaction, and atomic JSON replacement. The transaction prevents background +Coach progress refreshes from erasing a simultaneous budget, update, pricing, +or language-signal change. Browser-local navigation and presentation +preferences remain in local storage. Mutation routes require a local origin +plus the current action token and accept only allowlisted fields or discovered +canonical identifiers. + +Coach state uses that same atomic settings path but stores only one validated +goal, numeric baseline/current snapshots, bounded timestamps/error codes, and a +recommendation enum. Evidence collection runs off the HTTP request path and is +cached for 15 minutes; activation and `GET /coach/state` never synchronously +scan traces. Chat messages and Codex prose remain in browser memory only. ## Client Interfaces @@ -195,16 +208,30 @@ qualifying days renders as unavailable rather than zero. Period ratios remain conditional on comparable projects and are not described as lower or upper bounds. +Tok is a shared right-side browser surface, not a top-level route. It receives +only the allowlisted route plus an optional opaque selected-session ID. Wide +desktop layouts reserve room for the panel; 1024-pixel layouts overlay it. The +same in-memory conversation survives hash-route changes, while a refresh clears +it. There are no Chat, Goal, or Weekly tabs: one active structured goal appears +as a rail below the composer and opens a compact detail sheet for progress and +weekly controls. Agent strings are rendered as text and navigation actions map +only to literal existing routes. A submitted turn inserts one transient answer +line immediately. It projects only `opening_codex`, `reading_token_meter`, and +`checking_evidence` from observed child/MCP boundaries; the browser supplies the +initial `Starting Tok` boundary. The visual elapsed timer is not announced. +After eight seconds, a stop action can cancel only the owned active child; a +stopped turn preserves its user message and offers retry. + Native companions never parse traces. macOS AppKit, Linux AppIndicator, and Windows NotifyIcon clients read the compact `/menubar` projection and use the runtime catalog for generic labels, colors, and capabilities. Provider quota views use cached normalized windows; unavailable is never rendered as 0%. The optional MCP server is local stdio, read-only, and independently bounded. -Its decision tools use caller-matched or aggregate projections. Its `sessions`, -`trace`, `stats`, and `schema` query tools select content-free session IDs, read -one standardized or sanitized-native trace, aggregate only standardized -evidence, and describe their schema. Opaque +Its decision tools use caller-matched or aggregate projections. `sessions`, +`trace`, and `stats` select content-free session IDs, read one standardized or +sanitized-native trace, and aggregate only standardized evidence; `goal` returns +the bounded structured Coach state, and `schema` describes query fields. Opaque cursors bind the normalized query to the source revision, and serialized pages are capped at 65,536 bytes. Native structure is not raw trace content: adapters attach only constant structural types/subtypes and the shared projection keeps @@ -212,6 +239,28 @@ an explicit allowlist of numeric, enum, model, and tool fields. Data returned to a connected coding agent may enter that agent provider's model context under the client's own terms. +The Tok runner creates an ephemeral Codex home and workspace containing only +its bundled skill, while reusing the user's existing saved Codex authentication. +It ignores user config and rules, disables general shell, file, browser, app, +plugin, memory, and sub-agent capabilities, and configures exactly one required +`tokenmeter` MCP server with a six-tool read-only Tok allowlist. Output schemas, +input/result/event bounds, a process lock, a timeout, and exact completed-MCP +event validation constrain each run. The `goal` MCP tool returns only the same +structured stored projection and never invokes Codex recursively. + +Coach-launched MCP processes may post a validated tool name and bounded +arguments to `/coach/evidence`, which dispatches through the same shared tool +and argument allowlists against the warm `AgentAPIService`. The sanitized isolated +Coach environment receives two separate per-process credentials: the normal action +token and a dedicated Coach-evidence token, sent only as `X-Token-Meter-Action` +and `X-Token-Meter-Coach-Evidence` loopback +headers, never command arguments or model input. The route requires both tokens +as well as local-origin, JSON, size, tool, and argument validation; neither token +is returned in a result. On an unavailable, +timed-out, malformed, or invalid warm response, the stdio server falls back once +to its existing local read path. This avoids a duplicate discovery scan when it +works; it does not bypass MCP, promise provider speed, or enlarge data access. + ## Privacy and Security Invariants No public HTTP, native, MCP, or telemetry projection may contain: @@ -227,6 +276,19 @@ the matching provider credential, fixed HTTPS endpoints, timeouts, response size limits, sanitized errors, and in-memory caching. No credential is copied to Token Meter storage or another provider. +Tok is an explicit user-initiated or opt-in weekly network boundary through +the signed-in Codex CLI. Only the sent Tok message, bounded in-memory turns, +allowlisted page context, structured goal, and content-free MCP results may +enter OpenAI's model context. Raw traces and the prohibited projection fields +above remain unavailable to the Tok process, and neither conversation text +nor Codex prose is persisted by Token Meter. The lifecycle projection contains +only a fixed stage, numeric start time, cancellability, the name of the +in-progress MCP tool bounded to the shared read-only tool allowlist, and a +clamped count of completed evidence readings. It never includes model reasoning, +JSONL text, tool arguments/results, or provider latency claims. The service +re-validates the tool name against that allowlist and clamps the count, so an +unexpected adapter value becomes `null` rather than browser-visible copy. + Git performs no network operation. Its SQLite ledger contains only salted repository/object keys, local observation timestamps and days, numeric text-line totals, and coverage state. Project paths, remote/ref names, Git identity, diff --git a/specs/SECURITY.md b/specs/SECURITY.md index 1623359..119e2a2 100644 --- a/specs/SECURITY.md +++ b/specs/SECURITY.md @@ -65,7 +65,7 @@ evidence, but never session titles, project names, source paths, or native provider payloads. Capability names are returned only when capability review is explicitly requested. -The `sessions`, `trace`, `stats`, and `schema` tools use strict input schemas, +The `sessions`, `trace`, `stats`, `goal`, and `schema` tools use strict input schemas, positive output allowlists, and fixed limits. Session IDs identify only an already discovered local source. Pagination cursors contain hashed query and revision bindings, not paths or trace content; a changed revision invalidates @@ -75,6 +75,49 @@ type/subtype enums plus bounded numeric, status, model, and tool fields. Raw prompts, responses, tool payloads, account data, and trace paths are not available through MCP. +The dashboard agent Tok is a separate, explicit provider-processing boundary. A +sent message starts the user's signed-in Codex CLI in an ephemeral temporary +home and non-repository workspace. User configuration, rules, skills, plugins, +memories, and prior Codex sessions are not loaded. General shell, file, browser, +app, image, and sub-agent capabilities are disabled; the run receives only the +bundled Tok skill and an allowlist of read-only `tokenmeter` MCP tools. The +CLI is read-only, never asks for approval, has fixed time and output limits, and +must return a validated schema. Token Meter accepts an evidence-bearing answer +only after a completed `tokenmeter` MCP tool call is present in the CLI event +stream. Free text in that stream, stderr, paths, and raw errors are never sent +to the browser. + +Tok messages are held only in browser memory. Token Meter settings contain at +most one allowlisted goal contract, numeric baseline/current snapshots, bounded +review timestamps and status codes, and one recommendation enum; they contain +neither the natural-language request nor Codex prose. The message and bounded +MCP results used for a response may be processed by OpenAI under the user's +Codex account. This is opt-in per chat request and per weekly-review setting; it +does not make raw traces available to Codex. + +Coach-launched MCP may use the local `/coach/evidence` bridge only to avoid a +second local discovery scan. Its sanitized isolated environment receives two +separate per-process credentials: the normal action token and a dedicated +Coach-evidence token, sent only in the `X-Token-Meter-Action` and +`X-Token-Meter-Coach-Evidence` internal loopback +headers, never command arguments or model input. The route requires both tokens +alongside local-origin, JSON, request-size, tool-name, and argument validation, +dispatches through the same read-only allowlists as stdio MCP, and returns only +the usual bounded sanitized projection; neither token is returned. A failed, +malformed, oversized, or unavailable bridge response +falls back once to the existing local read. It never returns the dashboard +state, raw trace data, event text, or MCP payloads. + +Tok's visible lifecycle is likewise content-free: it can name only fixed +execution boundaries, the name of the read-only MCP tool currently running, a +clamped count of completed evidence readings, and a visual elapsed timer. The +tool name is re-validated against the shared read-only allowlist before it +reaches the browser, so it can only ever be one of those fixed labels. The +lifecycle never reveals model reasoning, raw JSONL events, tool +arguments/results, or a provider-latency guarantee. Stop terminates only the +active ephemeral Coach child and does not target unrelated Codex or MCP +processes. + When an MCP tool is called, the bounded derived result is handed to the connected Codex or Claude client. That client may send the result to its model provider under the client's own terms and configuration. “Local analysis” means diff --git a/specs/USER_GUIDE.md b/specs/USER_GUIDE.md index af54409..4aa56ea 100644 --- a/specs/USER_GUIDE.md +++ b/specs/USER_GUIDE.md @@ -16,6 +16,9 @@ troubleshooting. For a product overview, start with the Installer. The bootstrap installs missing Git and native Windows Python 3.8 or newer. - `curl` for macOS and Linux lifecycle helpers. +- A signed-in Codex CLI for Tok chat, natural-language goal drafting, and + weekly agent analysis. The dashboard and deterministic goal progress still + work when Codex is unavailable. The browser dashboard uses only the Python standard library. A machine with no supported evidence still starts normally and shows an empty state. @@ -171,6 +174,73 @@ and provider sources for bundled rates. Select the models to change, edit their prices, choose **From now**, **From date**, or **All history**, and save them together. Unselected models are not changed. +### Tok and goals + +Select **Tok** in the sticky header from any dashboard page. It is one shared +conversation, not separate Chat, Goal, and Weekly tabs, and remains available +as you move between routes. Tok receives only the current route and, when one +is selected, an opaque session ID; conversation messages are kept only in +browser memory and clear on refresh. + +Submitting a message immediately adds one working line with a spinning +indeterminate indicator, the current stage, and an elapsed timer. The stage +begins at **Starting Tok**, then changes to **Opening Codex**, the specific +Token Meter reading in progress such as **Reading your usage history**, or +**Checking the evidence**—only after that client or run boundary is observed. +Once a reading finishes, the line also reports how many readings have completed, +and past roughly twenty-five seconds it says that deeper analysis takes longer +rather than appearing stuck. Both additions come from observed boundaries; the +count never runs ahead of real work. + +The elapsed timer is visual only, so assistive technology hears stage changes +but not timer ticks. Reduced-motion users get a static marker instead of the +spinner. The panel does not expose model reasoning, raw events, tool +arguments/results, fake percentages, or a provider-response estimate. Provider +response time is variable. After eight seconds the send control becomes +**Stop**, which ends only the active Tok turn; it keeps your sent message and +replaces the working line with **Stopped** and **Retry**. + +Errors are visible rather than announced only to assistive technology. A request +that fails becomes a **Not delivered** entry with **Retry**, so a failure is +never phrased as something Tok said. Goal, weekly, and state failures appear as a +notice under the panel header with the action that can recover them. If the local +Codex CLI is missing, the notice names that prerequisite and the closed **Tok** +control carries one quiet attention dot; the same dot appears when the last +weekly review did not complete. + +Describe an ordinary-language intention, such as reducing retry rate for Codex +over the next 14 days. Codex returns a structured draft; verify its metric, +relative target, evidence window, runtime, review day, and weekly setting before +selecting **Activate goal**. Token Meter begins collecting the numeric baseline +in the background, so activation stays immediate. One active goal appears in a +thin rail below the composer; open its detail sheet to see progress, pause +automatic reviews, **Run now**, or clear the goal. The sheet is one scroll with +the goal above its weekly review and a **Back to chat** control at the top; +clearing asks for confirmation in a Token Meter dialog. A completed review also +appears as a concise Tok message. Goal progress compares the latest +cached snapshot with the activation baseline and retains complete, partial, and +unavailable coverage. A ratio with no covered denominator is unavailable, not +zero. Clearing removes the structured goal, baseline, current snapshot, and +saved weekly result. + +Tok answers with a concise paragraph, one collapsed source line, and at most +one next action. Expand the source line only when you need its at-most-three +content-free measurements; it is not a raw trace or a separate evidence table. + +Tok runs the local Codex CLI ephemerally with only the bundled Tok skill and +allowlisted read-only `tokenmeter` MCP tools. General shell, file, browser, app, +plugin, memory, and sub-agent capabilities are disabled for these runs. The +message you send and the content-free MCP evidence Codex selects may be +processed by OpenAI under your existing Codex account. Natural-language +messages and Codex prose are never written to Token Meter settings. + +For a Tok turn, the local MCP process can ask the running Token Meter server for +the same bounded read-only tool result it would otherwise build locally. This +warm evidence path reuses the server's discovery snapshot when available; if it +cannot return a valid result, the MCP process falls back once to its existing +local read. This optimization does not change the MCP tool allowlist, expose raw +trace content, or guarantee a provider response time. + ## Native Companions The macOS menu bar and Linux AppIndicator tray are supported. The Windows @@ -199,6 +269,8 @@ The bounded tools are: - `mcp__tokenmeter__trace` for standardized evidence or sanitized native structure from one selected session; - `mcp__tokenmeter__stats` for selected metrics grouped by stable dimensions; +- `mcp__tokenmeter__goal` for the active structured goal, progress, or latest + weekly numeric review; - `mcp__tokenmeter__schema` for query fields, units, limits, and evidence semantics. diff --git a/specs/plans/2026-09-11-goals-coach.md b/specs/plans/2026-09-11-goals-coach.md new file mode 100644 index 0000000..4726e1a --- /dev/null +++ b/specs/plans/2026-09-11-goals-coach.md @@ -0,0 +1,328 @@ +# Global Goals Coach Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a global right-side Token Meter Coach that converts natural-language optimization goals into measurable contracts and produces evidence-bounded Codex/MCP coaching, including optional weekly reviews. + +**Architecture:** A focused `token_meter.coach` package owns goal validation, evidence formulas, persistence projections, safe Codex execution, and scheduling. `token_meter.app` wires those services into four localhost HTTP routes and one read-only MCP tool; `page.html` owns the shared in-memory conversation and responsive panel. The runtime bundles a repo-local Coach skill into a temporary non-repository workspace for every ephemeral invocation. + +**Tech Stack:** Python 3.8+ standard library, `unittest`, stdio MCP, Codex CLI non-interactive mode, single-file HTML/CSS/JavaScript dashboard. + +**Spec:** `specs/2026-09-11-goals-coach-design.md` + +## Global Constraints + +- Keep the Python server dependency-free and local-only. +- Keep the top-level dashboard order `Sessions -> Spend -> Models -> Efficiency -> Git -> Learn -> Tools -> Settings`; Goals lives inside Coach. +- Never send trace prompts, responses, reasoning, tool arguments/results, credentials, settings values, paths, project names, or raw traces to Codex. +- Never persist Coach messages or Codex prose; persist only validated goal fields, numeric snapshots, bounded status codes, and allowlisted recommendation codes. +- Treat unavailable evidence as unavailable, never measured zero. +- Use only the user's existing Codex CLI authentication and model selection. +- Require the `tokenmeter` MCP server and allow only read-only Token Meter tools for a Coach run. +- Do not silently change model, skill, MCP, budget, or agent configuration. +- Do not commit, push, or open a pull request without new explicit user approval. + +--- + +### Task 1: Goal contracts, formulas, and atomic settings + +**Files:** +- Create: `token_meter/coach/__init__.py` +- Create: `token_meter/coach/contracts.py` +- Create: `token_meter/coach/service.py` +- Create: `tests/test_coach.py` +- Modify: `token_meter/app.py` + +**Interfaces:** +- Consumes: `stats(metrics, **filters) -> dict`, existing `load_json` and `atomic_write_text`, wall-clock `now()`. +- Produces: `normalize_goal(value) -> dict`, `metric_snapshot(goal, stats) -> dict`, `goal_progress(goal, snapshot) -> dict`, and `CoachService.state()/save_goal()/clear_goal()`. + +- [x] **Step 1: Write failing contract and formula tests** + +Add table-driven tests with literal results for every supported metric, target +bounds, runtime/window/weekday validation, zero denominators, partial coverage, +and relative progress direction. Assert that serialized stored state excludes a +sentinel natural-language goal and all unknown input keys. + +- [x] **Step 2: Run the focused tests and verify RED** + +Run: `python3 -m unittest tests.test_coach.GoalContractTests tests.test_coach.GoalEvidenceTests -v` + +Expected: import failure because `token_meter.coach.contracts` does not exist. + +- [x] **Step 3: Implement contracts and evidence formulas** + +Use immutable allowlists for metrics/runtimes/windows/recommendations, finite +numeric validation, and a `coverage` value of `complete`, `partial`, or +`unavailable`. Calculate ratios only from covered numerator/denominator values; +clamp progress to `[0, 100]`. + +- [x] **Step 4: Write failing persistence behavior tests** + +Exercise a real temporary settings file. Verify migration from missing or +malformed `coach` state, idempotent writes, atomic replacement, baseline capture +at activation, weekly pause updates, clear behavior, and omission of free text. + +- [x] **Step 5: Run persistence tests and verify RED** + +Run: `python3 -m unittest tests.test_coach.CoachPersistenceTests -v` + +Expected: failure because `CoachService` is not implemented. + +- [x] **Step 6: Implement minimal persistence and state projection** + +Implement `CoachService` with injected settings path, stats callback, and clock. +Reuse Token Meter's atomic JSON writer, preserve unrelated settings keys, and +return only public structured state. + +- [x] **Step 7: Run all Task 1 tests and verify GREEN** + +Run: `python3 -m unittest tests.test_coach -v` + +Expected: all contract, formula, and persistence tests pass. + +### Task 2: Safe Codex executor and bundled Coach skill + +**Files:** +- Create: `token_meter/coach/codex.py` +- Create: `token_meter/coach/workspace/.agents/skills/token-meter-coach/SKILL.md` +- Create: `token_meter/coach/workspace/.agents/skills/token-meter-coach/agents/openai.yaml` +- Modify: `tests/test_coach.py` + +**Interfaces:** +- Consumes: `codex_path`, `mcp_command`, `mcp_args`, request mode, bounded prompt context, JSON schema, and injected process runner. +- Produces: `CodexCoach.status() -> dict` and `CodexCoach.run(request) -> dict` with sanitized `CoachRunError.code` failures. + +- [x] **Step 1: Write failing command-boundary and output tests** + +Assert with a recording fake process that the command contains `exec`, +`--ephemeral`, `--sandbox read-only`, `--ignore-user-config`, `--ignore-rules`, +`--skip-git-repo-check`, `--json`, `--output-schema`, a required `tokenmeter` +stdio MCP definition, an MCP tool allowlist, and a temporary `-C` directory. +Assert the prompt explicitly invokes `$token-meter-coach`, contains only the +allowlisted page/goal fields, and never embeds the settings path or MCP command. + +- [x] **Step 2: Run executor tests and verify RED** + +Run: `python3 -m unittest tests.test_coach.CodexCoachTests -v` + +Expected: import failure because `token_meter.coach.codex` does not exist. + +- [x] **Step 3: Implement bounded ephemeral execution** + +Use `TemporaryDirectory`, `shutil.copytree`, `subprocess.Popen.communicate`, a +90-second timeout, a 64 KiB event cap, a 32 KiB result cap, strict mode-specific +schemas, and recursive field validation. Kill only the owned child on timeout. +Map missing CLI, timeout, busy, auth, MCP startup, nonzero exit, oversized output, +malformed JSON, and invalid schema to stable error codes without stderr text. + +- [x] **Step 4: Add failing MCP-use and privacy tests** + +Feed literal JSONL fixtures for successful required `tokenmeter` tool use, +missing MCP use on weekly runs, adversarial extra result fields, HTML, a private +sentinel, oversized output, timeout, and nonzero exit. Verify temporary files are +removed and no result is written to settings. + +- [x] **Step 5: Write the focused Coach skill** + +The skill must tell Codex to use only Token Meter MCP evidence, keep unavailable +distinct from zero, avoid causal/quality claims, never change configuration, +return the supplied schema exactly, and prefer one measurable experiment. + +- [x] **Step 6: Run Task 2 tests and verify GREEN** + +Run: `python3 -m unittest tests.test_coach.CodexCoachTests -v` + +Expected: all safe-execution and privacy cases pass. + +### Task 3: Chat, goal, weekly HTTP routes and scheduler + +**Files:** +- Modify: `token_meter/coach/service.py` +- Modify: `token_meter/app.py` +- Create: `tests/test_coach_http.py` +- Modify: `tests/test_coach.py` + +**Interfaces:** +- Consumes: `CoachService`, `CodexCoach`, existing `_ACTION_TOKEN`, existing + `TokenMeterHTTPServer`, and existing MCP statistic service. +- Produces: `GET /coach/state`, `POST /coach/ask`, `POST /coach/goal`, + `POST /coach/weekly`, `coach_scheduler()`, and sanitized HTTP JSON responses. + +- [x] **Step 1: Write failing real-server HTTP tests** + +Start `TokenMeterHTTPServer(("127.0.0.1", 0), H)` for each case. Verify state +shape; valid ask; draft activation; pause; clear; manual review; 404 for unknown +routes; and rejection of foreign Origin, missing/wrong action token, non-JSON, +empty/oversized body, malformed JSON, unsupported keys, oversized conversation, +invalid route/session id, and invalid goal fields. + +- [x] **Step 2: Run HTTP tests and verify RED** + +Run: `python3 -m unittest tests.test_coach_http -v` + +Expected: the new routes return 404. + +- [x] **Step 3: Wire the four endpoints** + +Factor the existing POST preflight so Coach uses the same local-origin, +content-type, action-token, and request-size rules. Use status 200 for reads and +completed actions, 202 only for an accepted nonblocking run, 400 for validation, +409 for busy, and 503 for CLI/auth/MCP unavailability. + +- [x] **Step 4: Write failing scheduler tests** + +With an injected local clock and fake executor, prove disabled/no-goal/not-due +are no-ops, a due review runs once, repeated wakes in the same ISO week do not +run again, failures keep the prior report, and a nonblocking lock prevents +overlap between scheduled and interactive runs. + +- [x] **Step 5: Implement scheduler and structured weekly storage** + +Check once per minute in the existing background-service lifecycle. Store only +the numeric snapshot, recommendation enum, completed/attempt timestamps, and +bounded error code. Render prose later from local templates. + +- [x] **Step 6: Run Task 3 tests and verify GREEN** + +Run: `python3 -m unittest tests.test_coach tests.test_coach_http -v` + +Expected: all service, route, scheduler, and concurrency tests pass. + +### Task 4: Read-only goal MCP surface + +**Files:** +- Modify: `token_meter/services/agent_api.py` +- Modify: `token_meter_mcp.py` +- Modify: `tests/test_mcp_server.py` +- Modify: `tests/test_coach.py` + +**Interfaces:** +- Consumes: `CoachService.agent_projection(focus) -> dict`. +- Produces: MCP tool `goal` with `focus=active|progress|weekly` and read-only annotations. + +- [x] **Step 1: Write failing MCP list/call/privacy tests** + +Verify `tools/list` includes `goal`, all three focus values dispatch, unknown +arguments fail, read-only annotations are present, and a serialized response +contains no natural-language sentinel, path, settings key, or Codex prose. + +- [x] **Step 2: Run MCP tests and verify RED** + +Run: `python3 -m unittest tests.test_mcp_server tests.test_coach -v` + +Expected: `goal` is absent from the tool list. + +- [x] **Step 3: Add the bounded service and tool adapter** + +Expose `AgentAPIService.goal(**arguments)`, add the schema/description/tool +dispatch, increment the MCP version, and update server instructions. The tool +must never invoke Codex and must remain idempotent/read-only. + +- [x] **Step 4: Run MCP and privacy tests and verify GREEN** + +Run: `python3 -m unittest tests.test_mcp_server tests.test_mcp_queries tests.test_coach -v` + +Expected: all MCP contracts pass. + +### Task 5: Shared responsive dashboard Coach + +**Files:** +- Modify: `page.html` +- Modify: `tests/test_meter.py` +- Create: `tests/test_coach_dashboard.py` + +**Interfaces:** +- Consumes: the four HTTP routes, existing hash router, `LATEST` state, and the + existing action token. +- Produces: one global accessible Coach panel with Chat, Goal, Weekly tabs and + explicit allowlisted navigation actions. + +- [x] **Step 1: Write failing DOM and JavaScript behavior tests** + +Use a lightweight Node DOM harness to assert open/close, tab switching, +conversation survival across `applyHashRoute`, no `localStorage` or +`sessionStorage` message writes, route-context allowlisting, safe `textContent` +rendering, explicit route-button navigation, goal activation, busy/error live +regions, Escape close, focus return, and 1024 overlay versus wide dock classes. + +- [x] **Step 2: Run dashboard tests and verify RED** + +Run: `python3 -m unittest tests.test_coach_dashboard -v` + +Expected: Coach DOM and functions are absent. + +- [x] **Step 3: Implement the panel shell and state renderer** + +Add semantic `aside`, labelled tablist, live region, empty/loading/error states, +message list, composer, goal contract card, progress/coverage display, weekly +card, run/pause/clear controls, and local disclosure. Use existing design tokens, +8-pixel radii, restrained cyan signal, and no hosted assets. + +- [x] **Step 4: Implement bounded fetch and navigation behavior** + +Keep messages in a module-level array only; send at most six bounded turns. +Render every agent string through `textContent`. Map returned route ids through a +literal object and require a button click before changing the hash. + +- [x] **Step 5: Run dashboard tests and embedded-JS parse** + +Run: `python3 -m unittest tests.test_coach_dashboard -v` + +Run: `node -e "const fs=require('fs');const h=fs.readFileSync('page.html','utf8');const m=h.match(/