SLOPSHOPPER

cortex-cockpit

The harness cockpit, a viewer: Cortex memory health and grooming, the context window against the Stop guard's thresholds (from zetetic-autopilot), the…

newpanebandrowsguardcommand
v0.2.0no licenseupdated 2026-10-07cdeust/claude-mods/mods/cortex-cockpit
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cortex-cockpit
│ ┃ Cortex ✕ › fix the failing auth test and add an audit log call │ ┃ Cortex cockpit │ ┃ ──────────────────────────────────────────── ⏺ Read(src/auth.ts) │ ┃ ──────────── ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ CONTEXT ⎿ Added 2 lines, removed 1 line │ ┃ context: pending (no response measured yet) ⏺ Bash(bun test) │ ┃ ⎿ 3 pass, 1 fail │ ┃ MEMORY │ ┃ stats: blocked, ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ plugin_hypermnesia-mcp_cortex unreachable: │ ┃ TypeError: undefined is not an object ✻ Worked for 42s · done 4:20 PM │ ┃ (evaluating 'result.content.find') │ ┃ › /cortex │ ┃ PIPELINE SPEC → CODE ⎿ cortex-cockpit: Cortex cockpit opened. │ ┃ ○ recall not reached │ ┃ ○ spec not reached │ ┃ ○ spec-verify not reached │ ┃ ○ code-intel not reached │ ┃ ○ grounding not reached │ ┃ ○ gates not reached │ ┃ ○ subagents not reached │ ┃ ○ remember not reached │ ┃ ○ wiki not reached │ ┃ ● git 1 calls · last Bash 08:53:20 │ ┃ cortex · ctx: no reading · stats blocked · 0 calls · nothing refused [ Cockpit ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
cortex · ctx: no reading · stats blocked · 0 calls · nothing refused [ Cockpit ]
Pane · Cortex
Cortex cockpit ──────────────────────────────────────────────────────── CONTEXT context: pending (no response measured yet) MEMORY stats: blocked, plugin_hypermnesia-mcp_cortex unreachable: TypeError: undefined is not an object (evaluating 'result.content.find') PIPELINE SPEC → CODE ○ recall not reached ○ spec not reached ○ spec-verify not reached ○ code-intel not reached ○ grounding not reached ○ gates not reached ○ subagents not reached ○ remember not reached ○ wiki not reached ● git 1 calls · last Bash 08:53:20 HYGIENE no worktree beside the main checkout no test runner running read 0s ago POLICY autopilot: not loaded genius: not loaded ENFORCED no call refused this session CORTEX CALLS no Cortex call this session TURNS 1 turns · in 2 100 · out 1 480 · cache read 91 000 · cache write 4 300 · 93.4 % from cache tools: Bash 4 · Write 2 · Read 1 · Grep 1 · Edit 1 [ Refresh ]
README

claude-mods

Claude Code mods for the ai-architect.tools harness, one concern per mod, state shared through dependencies:

ModOwnsDepends on
cortex-guardrefusals: wiki pages only through wiki_write, worktrees inside <repo>/.claude/worktrees/nothing
cortex-wiki/wiki <wiki/**.md> to PDFnothing
zetetic-geniusstate: the request grade (task class → effort), the genius patterns and skills it matchesnothing
zetetic-autopilotcontext, policy: effort ladder, model routing, pressure, lean resultszetetic-genius
cortex-cockpitstats, ledger, tally, stages, hygiene; the /cortex pane draws the restthe three above
harness-fleetfleet: the owner's plugins, installed vs offered version, open PRs with CI, open issues (a defect is taken and fixed, a feature request goes to the owner); /fleet. Reads only (gh pr list, gh issue list, git remote): every outward action is a button the owner presses, which puts a prompt in front of the modelguard, genius, autopilot

Developing

Each mod is validated, tested and type-checked on its own:

cd mods/<mod> && claude plugin validate . && claude plugin test && npx -y -p typescript tsc -p .

tsc needs the engine to have loaded the mod once (it lays .claude-plugin/types/). For hot reload in a session, link the mod into that session's ~/.claude/dev-mods/<session>/ folder; the engine watches the folder a link names.

Installing

.claude-plugin/marketplace.json lists each mod as "source": "./mods/<mod>", so the repository is the marketplace. From another machine:

/plugin install <mod> --marketplace cdeust/claude-mods

y adds the marketplace, then the user scope. On this machine a folder marketplace reads the mods from the checkout, with no copy: claude plugin marketplace add <this folder>, then /plugin install <mod> and /reload-plugins after an edit.

Source 12 files
hooks/register.tsx 270 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { CortexEntry, CortexStats, HygieneSnapshot, StageTally, TurnTally } from '../types'
5import type { ContextHealth, GeniusState, PolicyState, Refusal } from './deps'
6import { OWNERSHIP_PATH, parseOwnership, testProcesses, worktrees } from './hygiene'
7import { begin, emptyTally, finish, group, isCortexTool, parseStats, shortTool } from './model'
8import { bump, emptyStages, stageOf } from './pipeline'
9import { inputGist, outputText, parseRecall } from './recall'
10import { RecallResultView, UseRowView } from './rows'
11import { type Surface, tone } from './tokens'
12import { Cockpit } from './view'
13
14const PANE = 'cortex-cockpit'
15const LEDGER_CAP = 200 // source: bounds $.state size; the pane shows the last rows only
16const SLOW_MS = 2000 // source: memory_stats states ~75 ms; a call ten times slower is worth a word
17const RETRY_MS = [2000, 5000, 15000] // source: own choice, MCP servers connect after the mod loads
18
19const ledger = atom({ plugin: 'cortex-cockpit', key: 'ledger' } as const, [] as CortexEntry[])
20const stats = atom({ plugin: 'cortex-cockpit', key: 'stats' } as const, null as CortexStats | null)
21const statsError = atom({ plugin: 'cortex-cockpit', key: 'statsError' } as const, null as string | null)
22const tally = atom({ plugin: 'cortex-cockpit', key: 'tally' } as const, emptyTally())
23const stages = atom({ plugin: 'cortex-cockpit', key: 'stages' } as const, emptyStages() as Record<string, StageTally>)
24const hygiene = atom({ plugin: 'cortex-cockpit', key: 'hygiene' } as const, null as HygieneSnapshot | null)
25
26// Tools after which the memory population changes, so the headline numbers are re-read.
27// source: Cortex handlers that write (mcp_server/handlers/*: remember, forget, consolidate, ...).
28const MUTATING = new Set(['remember', 'forget', 'consolidate', 'checkpoint', 'import_sessions'])
29
30const surfaceOf = (value: unknown): Surface => (value === 'paper' ? 'paper' : 'ink')
31
32// Module state the readers share: where the repo is and which Cortex server to call.
33let repo = ''
34let server = 'plugin_hypermnesia-mcp_cortex'
35
36const expandHome = async ($: EngineInterface, path: string): Promise<string> =>
37  path.startsWith('~/') ? `${(await $.env.get('HOME')) ?? ''}/${path.slice(2)}` : path
38
39// The other mods' state, as their contracts declare it (plugin.json `dependencies`); a mod not
40// loaded reads as undefined. The references are literals here, as the validator lists them.
41const GUARD_REFUSALS = { plugin: 'cortex-guard', key: 'refusals' } as const
42const AUTOPILOT_CONTEXT = { plugin: 'zetetic-autopilot', key: 'context' } as const
43const AUTOPILOT_POLICY = { plugin: 'zetetic-autopilot', key: 'policy' } as const
44const GENIUS_STATE = { plugin: 'zetetic-genius', key: 'state' } as const
45
46async function peekRefusals($: EngineInterface): Promise<readonly Refusal[]> {
47  try {
48    return (await $.state.get(GUARD_REFUSALS)).value ?? []
49  } catch {
50    return []
51  }
52}
53
54async function peekContext($: EngineInterface): Promise<ContextHealth | null> {
55  try {
56    return (await $.state.get(AUTOPILOT_CONTEXT)).value ?? null
57  } catch {
58    return null
59  }
60}
61
62async function peekPolicy($: EngineInterface): Promise<PolicyState | null> {
63  try {
64    return (await $.state.get(AUTOPILOT_POLICY)).value ?? null
65  } catch {
66    return null
67  }
68}
69
70async function peekGenius($: EngineInterface): Promise<GeniusState | null> {
71  try {
72    return (await $.state.get(GENIUS_STATE)).value ?? null
73  } catch {
74    return null
75  }
76}
77
78// memory_stats is read-only and takes no arguments (handlers/memory_stats.py, schema). A server
79// not yet connected answers with an error; the read re-arms itself a few times.
80async function refreshStats($: EngineInterface, attempt = 0): Promise<void> {
81  let parsed: ReturnType<typeof parseStats>
82  try {
83    parsed = parseStats(await $.mcp.call(server, 'memory_stats'), await $.clock.now())
84  } catch (error) {
85    parsed = { error: `${server} unreachable: ${String(error).slice(0, 140)}` }
86  }
87  if ('stats' in parsed) {
88    await update($, stats, () => parsed.stats)
89    await update($, statsError, () => null)
90    return
91  }
92  await update($, statsError, () => parsed.error)
93  const wait = RETRY_MS[attempt]
94  if (wait !== undefined) $.clock.after(wait, () => refreshStats($, attempt + 1))
95}
96
97async function refreshHygiene($: EngineInterface): Promise<void> {
98  const readAt = await $.clock.now()
99  let snapshot: HygieneSnapshot
100  try {
101    const list = await $.process.run(['git', 'worktree', 'list', '--porcelain'], { cwd: repo })
102    let ownership: string | undefined
103    try {
104      ownership = await $.fs.read(await expandHome($, OWNERSHIP_PATH))
105    } catch {
106      ownership = undefined
107    }
108    const ps = await $.process.run(['ps', '-eo', 'pid,etime,command'])
109    snapshot =
110      list.exitCode === 0
111        ? {
112            worktrees: worktrees(list.stdout, ownership === undefined ? undefined : parseOwnership(ownership)),
113            testProcesses: testProcesses(ps.stdout),
114            error: null,
115            readAt,
116          }
117        : { worktrees: [], testProcesses: [], error: list.stderr.slice(0, 120), readAt }
118  } catch (error) {
119    snapshot = { worktrees: [], testProcesses: [], error: String(error).slice(0, 120), readAt }
120  }
121  await update($, hygiene, () => snapshot)
122}
123
124async function consolidateNow($: EngineInterface): Promise<void> {
125  $.ui.toast('consolidate: running')
126  try {
127    await $.mcp.call(server, 'consolidate')
128  } finally {
129    void refreshStats($)
130  }
131}
132
133export const register: Register = (on, options) => {
134  server = String(options.cortex_server ?? server)
135  const surface = surfaceOf(options.surface)
136
137  on('session.start', async ($, e, next) => {
138    repo = (await $.session.repo())?.root ?? e.cwd
139    void refreshStats($)
140    void refreshHygiene($)
141    try {
142      await $.command.register({ name: 'cortex', description: 'Open the Cortex cockpit pane', immediate: true })
143    } catch (error) {
144      $.ui.toast(`/cortex is taken by another plugin: ${String(error).slice(0, 80)}`)
145    }
146
147    return next(e)
148  })
149
150  on('command.run', { command: 'cortex' }, async ($) => {
151    await $.ui.open({ id: PANE, title: 'Cortex' })
152    void refreshStats($)
153    void refreshHygiene($)
154
155    return { text: 'Cortex cockpit opened.' }
156  })
157
158  // Observer: tallies every tool, places it on the pipeline, and keeps the Cortex ledger.
159  on('tool.call', async ($, e, next) => {
160    const name = String(e.tool)
161    const short = shortTool(name)
162    const stage = stageOf(name, e as { command?: string })
163    await update($, tally, (t: TurnTally) => ({ ...t, tools: { ...t.tools, [short]: (t.tools[short] ?? 0) + 1 } }))
164    const entry =
165      isCortexTool(name) && e.tool_use_id !== undefined ? begin(e.tool_use_id, name, await $.clock.now()) : undefined
166    if (entry !== undefined) await update($, ledger, (list) => [...list, entry].slice(-LEDGER_CAP))
167
168    const ran = await next(e)
169    const now = await $.clock.now()
170    const isBlocked = ran.deny !== undefined || ran.isError === true
171    if (stage !== undefined) await update($, stages, (s) => bump(s, stage, name, now, isBlocked))
172    if (entry !== undefined) {
173      const done = finish(entry, now, ran.text ?? '', isBlocked)
174      await update($, ledger, (list) => list.map((one) => (one.id === entry.id ? done : one)))
175      if ((done.ms ?? 0) >= SLOW_MS) $.ui.status(`cortex ${short} slow: ${group(done.ms ?? 0)} ms`)
176      if (MUTATING.has(short)) void refreshStats($)
177    }
178
179    return ran
180  })
181
182  on('turn.complete', async ($, e, next) => {
183    const u = e.usage
184    if (u !== undefined && e.agentId === undefined) {
185      await update($, tally, (t: TurnTally) => ({
186        ...t,
187        turns: t.turns + 1,
188        input: t.input + u.input_tokens,
189        output: t.output + u.output_tokens,
190        cacheRead: t.cacheRead + u.cache_read_input_tokens,
191        cacheWrite: t.cacheWrite + u.cache_creation_input_tokens,
192      }))
193    }
194
195    return next(e)
196  })
197
198  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) =>
199    Cockpit($.ui.resolve(e) as never, {
200      surface,
201      now: await $.clock.now(),
202      columns: e.props.bodyColumns,
203      repo,
204      stats: await read($, stats),
205      statsError: await read($, statsError),
206      ledger: await read($, ledger),
207      tally: await read($, tally),
208      stages: await read($, stages),
209      hygiene: await read($, hygiene),
210      context: await peekContext($),
211      policy: await peekPolicy($),
212      genius: await peekGenius($),
213      refusals: await peekRefusals($),
214      onRefresh: () => {
215        void refreshStats($)
216        void refreshHygiene($)
217      },
218      onConsolidate: () => void consolidateNow($),
219      onCurateWiki: () =>
220        void $.prompt.submit({ text: 'Call curate_wiki, then write each authoring job it returns with wiki_write.' }),
221    }),
222  )
223
224  // Transcript rows, Cortex tools only; everything else is the engine's own row.
225  on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
226    if (!isCortexTool(e.props.tool)) return next(e)
227    const entry = (await read($, ledger)).find((one) => one.id === e.props.tool_use_id)
228
229    return UseRowView($.ui.resolve(e) as never, {
230      surface,
231      tool: shortTool(e.props.tool),
232      gist: inputGist(e.props.input),
233      isRunning: e.props.isRunning,
234      isErrored: e.props.isErrored,
235      entry,
236    })
237  })
238
239  on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
240    if (!isCortexTool(e.props.tool) || e.props.isErrored) return next(e)
241    const view = parseRecall(outputText(e.props.output))
242    if (view === undefined) return next(e)
243
244    return RecallResultView($.ui.resolve(e) as never, shortTool(e.props.tool), view)
245  })
246
247  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
248    const s = await read($, stats)
249    const failure = await read($, statsError)
250    const c = await peekContext($)
251    if (e.props.hasSurvey || (s === null && failure === null && c === null)) return next(e)
252    const { Box, Button, Text } = $.ui.resolve(e)
253    const refused = (await peekRefusals($)).length
254    const pressure = (await peekPolicy($))?.pressure ?? 'none'
255    const calls = (await read($, ledger)).length
256    const ctx = c === null || c.percent === null ? 'ctx: no reading' : `ctx ${c.percent} %`
257    const mem = s === null ? 'stats blocked' : `${group(s.total_memories)} memories`
258
259    return (
260      <Box>
261        <Text color={s === null ? tone(surface, 'danger') : undefined} dimColor={s !== null}>
262          cortex · {ctx} · {mem} · {group(calls)} calls · {refused === 0 ? 'nothing refused' : `${group(refused)} refused`}
263          {pressure === 'none' ? '' : ` · pressure ${pressure}`}{' '}
264        </Text>
265        <Button key="open" label="Cockpit" onPress={() => void $.ui.open({ id: PANE, title: 'Cortex' })} />
266      </Box>
267    )
268  })
269}
270
hooks/deps.ts 11 lines
1// The state this viewer reads from the mods it depends on (plugin.json `dependencies`), typed
2// by their contracts as the engine lays them: nothing is copied from another mod's folder.
3
4import type { PluginState } from 'claude-code'
5
6export type ContextHealth = NonNullable<PluginState['zetetic-autopilot']['context']>
7export type PolicyState = PluginState['zetetic-autopilot']['policy']
8export type PolicyDecision = PolicyState['decisions'][number]
9export type GeniusState = PluginState['zetetic-genius']['state']
10export type Refusal = PluginState['cortex-guard']['refusals'][number]
11
hooks/hygiene.ts 68 lines
1import type { Worktree } from '../types'
2// source: ~/.claude/CLAUDE.md rule 2 and Cortex CLAUDE.md: <repo>/.claude/worktrees/<name>/,
3// .Codex/worktrees/<name>/ for Codex. The same rule cortex-guard enforces; a viewer only reads it.
4const WORKTREE_ROOTS = ['/.claude/worktrees/', '/.Codex/worktrees/'] as const
5const isInsideWorktreeRoot = (path: string): boolean => WORKTREE_ROOTS.some((root) => path.includes(root))
6
7// source: ~/.local/state/disk-hygiene/ownership.json, the registry disk_hygiene.py keeps
8// (its --state default); one entry per registered path, `owner` is "<host>:<session>".
9export const OWNERSHIP_PATH = '~/.local/state/disk-hygiene/ownership.json'
10
11export type Ownership = Record<string, { owner?: string; pr?: string; kind?: string }>
12
13export const parseOwnership = (text: string): Ownership | undefined => {
14  try {
15    const raw = JSON.parse(text) as unknown
16    return raw !== null && typeof raw === 'object' ? (raw as Ownership) : undefined
17  } catch {
18    return undefined
19  }
20}
21
22// `git worktree list --porcelain`: stanzas separated by a blank line, the first is main.
23export const parsePorcelain = (text: string): { path: string; branch: string | null }[] =>
24  text
25    .split(/\n\s*\n/)
26    .map((stanza) => stanza.trim())
27    .filter((stanza) => stanza.startsWith('worktree '))
28    .map((stanza) => {
29      const lines = stanza.split('\n')
30      const path = (lines[0] ?? '').slice('worktree '.length)
31      const branch = lines.find((l) => l.startsWith('branch '))?.slice('branch '.length) ?? null
32      return { path, branch }
33    })
34
35export const worktrees = (porcelain: string, ownership: Ownership | undefined): Worktree[] =>
36  parsePorcelain(porcelain).map((w, i) => {
37    const entry = ownership?.[w.path]
38    return {
39      path: w.path,
40      branch: w.branch,
41      isMain: i === 0,
42      isInsideRepoRule: i === 0 || isInsideWorktreeRoot(w.path),
43      isRegistered: entry !== undefined,
44      owner: entry?.owner ?? null,
45      pr: entry?.pr ?? null,
46    }
47  })
48
49// source: the test runners the Cortex and ai-architect projects use (pytest, vitest, cargo).
50const TEST_RUNNER = /\b(pytest|vitest|jest|cargo test|go test|mutmut|hypothesis)\b/
51
52// `ps -eo pid,etime,command` rows that are a test runner; the header row never matches.
53export const testProcesses = (
54  ps: string,
55): { pid: number; elapsed: string; command: string }[] =>
56  ps
57    .split('\n')
58    .map((line) => line.trim())
59    .filter((line) => TEST_RUNNER.test(line) && !/\bgrep\b/.test(line))
60    .map((line) => {
61      const [pid, elapsed, ...rest] = line.split(/\s+/)
62      return { pid: Number(pid), elapsed: elapsed ?? '', command: rest.join(' ').slice(0, 80) }
63    })
64    .filter((p) => Number.isFinite(p.pid))
65
66export const shortPath = (path: string, repo: string): string =>
67  path.startsWith(repo) ? path.slice(repo.length).replace(/^\//, '') || '.' : path
68
hooks/model.ts 105 lines
1import type { CortexEntry, CortexStats, TurnTally } from '../types'
2
3// Pure functions: no engine access, so the test file exercises them directly.
4
5// source: the Cortex plugin exposes its tools to the model as
6// mcp__plugin_hypermnesia-mcp_cortex__<tool> (seen in this build's tool list).
7const CORTEX_PREFIX = 'mcp__plugin_hypermnesia-mcp_cortex__'
8const STATS_KEYS = ['total_memories', 'avg_heat', 'grooming_staleness'] as const
9
10export const isCortexTool = (tool: string): boolean => tool.startsWith(CORTEX_PREFIX)
11
12export const shortTool = (tool: string): string =>
13  isCortexTool(tool) ? tool.slice(CORTEX_PREFIX.length) : tool
14
15// Exact counts, thousands grouped by a space: "119 304" (gate G10).
16export const group = (n: number): string =>
17  String(Math.trunc(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ' ')
18
19export const emptyTally = (): TurnTally => ({
20  turns: 0,
21  input: 0,
22  output: 0,
23  cacheRead: 0,
24  cacheWrite: 0,
25  tools: {},
26})
27
28export const begin = (id: string, tool: string, now: number): CortexEntry => ({
29  id,
30  tool: shortTool(tool),
31  startedAt: now,
32  status: 'pending',
33})
34
35// recall answers {"count": n, "receipt_id": m, ...}; absent means we say so, not 0.
36const numberAfter = (text: string, key: string): number | undefined => {
37  const hit = new RegExp(`"${key}"\\s*:\\s*(\\d+)`).exec(text)
38  return hit === null ? undefined : Number(hit[1])
39}
40
41export const finish = (
42  entry: CortexEntry,
43  now: number,
44  text: string,
45  isError: boolean,
46): CortexEntry => ({
47  ...entry,
48  status: isError ? 'blocked' : 'measured',
49  ms: Math.max(0, now - entry.startedAt),
50  count: numberAfter(text, 'count'),
51  receipt: numberAfter(text, 'receipt_id'),
52})
53
54export const countBy = (ledger: readonly CortexEntry[]): Record<string, number> => {
55  const out: Record<string, number> = {}
56  for (const one of ledger) out[one.tool] = (out[one.tool] ?? 0) + 1
57  return out
58}
59
60type Block = { type: string; text?: string }
61export type StatsParse = { stats: CortexStats } | { error: string }
62
63export const parseStats = (
64  result: { content: readonly Block[]; isError: boolean; structuredContent?: unknown },
65  now: number,
66): StatsParse => {
67  const first = result.content.find((b) => b.type === 'text')?.text ?? ''
68  if (result.isError) return { error: first.slice(0, 160) || 'server reported an error' }
69  let raw: unknown = result.structuredContent
70  if (raw === undefined) {
71    try {
72      raw = JSON.parse(first)
73    } catch {
74      return { error: 'memory_stats did not return JSON' }
75    }
76  }
77  const obj = raw as Record<string, unknown> | null
78  const missing = STATS_KEYS.filter((k) => obj === null || typeof obj !== 'object' || !(k in obj))
79  if (missing.length > 0) return { error: `memory_stats lacks ${missing.join(', ')}` }
80  return { stats: { ...(obj as Omit<CortexStats, 'readAt'>), readAt: now } }
81}
82
83// Heat track of `width` cells: how many are filled and each cell's rung 0..3.
84export const heatCells = (value: number, width: number) => {
85  const v = Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 0
86  const filled = Math.round(v * width)
87  return Array.from({ length: width }, (_, i) => ({
88    isFilled: i < filled,
89    rung: Math.min(3, Math.floor((i / width) * 4)),
90  }))
91}
92
93export const ageLabel = (ms: number): string => {
94  const s = Math.round(ms / 1000)
95  if (s < 60) return `${s}s`
96  const m = Math.floor(s / 60)
97  return m < 60 ? `${m}m ${s % 60}s` : `${Math.floor(m / 60)}h ${m % 60}m`
98}
99
100// Share of input served from the prompt cache; undefined when nothing was read.
101export const cacheShare = (t: TurnTally): number | undefined => {
102  const all = t.input + t.cacheRead + t.cacheWrite
103  return all === 0 ? undefined : t.cacheRead / all
104}
105
hooks/pipeline.ts 94 lines
1import type { StageTally } from '../types'
2
3// The spec-to-code pipeline as the harness runs it, read off the tools the model calls.
4// Stage order and membership are the servers' own published orderings:
5// source: prd-gen server instructions (coordinate_context_budget → start_pipeline →
6//   get_pipeline_state → submit_action_result; plan_* → submit_action_result →
7//   conclude_verification; validate_prd_*).
8// source: ai-architect server instructions (stages 1 findings, 2 verification, 3 code
9//   intelligence, 4/6 PRD grounding, 8/9 gates).
10// source: Cortex server instructions (recall/remember, wiki_*).
11
12export const STAGES = [
13  'recall',
14  'spec',
15  'spec-verify',
16  'code-intel',
17  'grounding',
18  'gates',
19  'subagents',
20  'remember',
21  'wiki',
22  'git',
23] as const
24
25export type Stage = (typeof STAGES)[number]
26
27const PRD = 'mcp__plugin_ai-architect-mcp-spec_prd-gen__'
28const ARCH = 'mcp__plugin_ai-architect-mcp-codebase_ai-architect__'
29const CORTEX = 'mcp__plugin_hypermnesia-mcp_cortex__'
30
31const BY_TOOL: Record<string, Stage> = {
32  [`${CORTEX}recall`]: 'recall',
33  [`${CORTEX}recall_hierarchical`]: 'recall',
34  [`${CORTEX}unified_search`]: 'recall',
35  [`${CORTEX}recall_skills`]: 'recall',
36  [`${CORTEX}remember`]: 'remember',
37  [`${CORTEX}checkpoint`]: 'remember',
38  [`${PRD}coordinate_context_budget`]: 'spec',
39  [`${PRD}start_pipeline`]: 'spec',
40  [`${PRD}get_pipeline_state`]: 'spec',
41  [`${PRD}submit_action_result`]: 'spec',
42  [`${PRD}validate_prd_section`]: 'spec-verify',
43  [`${PRD}validate_prd_document`]: 'spec-verify',
44  [`${PRD}plan_section_verification`]: 'spec-verify',
45  [`${PRD}plan_document_verification`]: 'spec-verify',
46  [`${PRD}conclude_verification`]: 'spec-verify',
47  [`${ARCH}prepare_prd_input`]: 'grounding',
48  [`${ARCH}validate_prd_against_graph`]: 'grounding',
49  [`${ARCH}check_doc_claims`]: 'grounding',
50  [`${ARCH}check_security_gates`]: 'gates',
51  [`${ARCH}verify_semantic_diff`]: 'gates',
52  [`${ARCH}start_verification`]: 'gates',
53  [`${ARCH}finalize_verification`]: 'gates',
54}
55
56const GIT_WRITE = /(^|[\s;&|])git\s+(commit|push|merge|rebase|worktree\s+add|tag)\b/
57
58export const stageOf = (tool: string, input?: { command?: string }): Stage | undefined => {
59  const direct = BY_TOOL[tool]
60  if (direct !== undefined) return direct
61  if (tool.startsWith(ARCH)) return 'code-intel'
62  if (tool.startsWith(`${CORTEX}wiki_`)) return 'wiki'
63  if (tool === 'Agent') return 'subagents'
64  if (tool === 'Bash' && input?.command !== undefined && GIT_WRITE.test(input.command))
65    return 'git'
66  return undefined
67}
68
69export const emptyStages = (): Record<string, StageTally> =>
70  Object.fromEntries(
71    STAGES.map((s) => [s, { calls: 0, blocked: 0, lastAt: null, lastTool: null }]),
72  )
73
74export const bump = (
75  stages: Record<string, StageTally>,
76  stage: Stage,
77  tool: string,
78  now: number,
79  isBlocked: boolean,
80): Record<string, StageTally> => {
81  const s = stages[stage] ?? { calls: 0, blocked: 0, lastAt: null, lastTool: null }
82  return {
83    ...stages,
84    [stage]: {
85      calls: s.calls + 1,
86      blocked: s.blocked + (isBlocked ? 1 : 0),
87      lastAt: now,
88      lastTool: tool,
89    },
90  }
91}
92
93export const shortName = (tool: string): string => tool.replace(/^mcp__[^_]+(_[^_]+)*__/, '')
94
hooks/recall.ts 91 lines
1// Pure reading of a Cortex tool call for the transcript rows (ToolUse, ToolResult).
2
3export type RecallHit = {
4  id: string
5  score?: number
6  heat?: number
7  domain?: string
8  source?: string
9  content: string
10}
11
12export type RecallView = {
13  count: number
14  receipt?: number
15  intent?: string
16  dropped?: number
17  shown: RecallHit[]
18  hidden: number
19}
20
21// source: own choice, the rows a result shows before "n more"; the full list stays in ctrl+o
22export const SHOWN_HITS = 5
23// source: own choice, one transcript line per hit
24const GIST_CHARS = 100
25
26const oneLine = (text: string, max: number): string => {
27  const flat = text.replace(/\s+/g, ' ').trim()
28  return flat.length <= max ? flat : `${flat.slice(0, max - 1)}…`
29}
30
31const num = (v: unknown): number | undefined => (typeof v === 'number' ? v : undefined)
32const str = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined)
33
34// An MCP result reaches the row as text, as MCP content blocks, or as an object.
35export const outputText = (output: unknown): string => {
36  if (typeof output === 'string') return output
37  const content = (output as { content?: unknown } | null)?.content
38  if (Array.isArray(content)) {
39    return content
40      .map((b) => (b as { type?: string; text?: string }).text)
41      .filter((t): t is string => typeof t === 'string')
42      .join('\n')
43  }
44  return output === undefined ? '' : JSON.stringify(output)
45}
46
47// Shape-driven: anything without a `memories` array is left to the engine's own row.
48export const parseRecall = (text: string): RecallView | undefined => {
49  let raw: unknown
50  try {
51    raw = JSON.parse(text)
52  } catch {
53    return undefined
54  }
55  const obj = raw as Record<string, unknown> | null
56  const list = obj?.memories
57  if (obj === null || typeof obj !== 'object' || !Array.isArray(list)) return undefined
58  const hits: RecallHit[] = list.map((m) => {
59    const r = m as Record<string, unknown>
60    return {
61      id: String(r.memory_id ?? '?'),
62      score: num(r.score),
63      heat: num(r.heat),
64      domain: str(r.domain),
65      source: str(r.source),
66      content: oneLine(str(r.content) ?? '', GIST_CHARS),
67    }
68  })
69  return {
70    count: num(obj.count) ?? hits.length,
71    receipt: num(obj.receipt_id),
72    intent: str(obj.intent),
73    dropped: num(obj.low_signal_dropped),
74    shown: hits.slice(0, SHOWN_HITS),
75    hidden: Math.max(0, hits.length - SHOWN_HITS),
76  }
77}
78
79// The input field that says what the call is about, first present wins.
80const GIST_KEYS = ['query', 'content', 'title', 'domain', 'memory_id'] as const
81
82export const inputGist = (input: unknown): string => {
83  const obj = (input ?? {}) as Record<string, unknown>
84  for (const key of GIST_KEYS) {
85    const v = obj[key]
86    if (typeof v === 'string' && v !== '') return oneLine(v, 80)
87    if (typeof v === 'number') return String(v)
88  }
89  return ''
90}
91
hooks/rows.tsx 82 lines
1import type { CortexEntry } from '../types'
2import { group } from './model'
3import type { RecallView } from './recall'
4import { HEAT_TRACK, type Surface, tone } from './tokens'
5import type { Ui } from './view'
6
7export type UseRow = {
8  surface: Surface
9  tool: string
10  gist: string
11  isRunning: boolean
12  isErrored: boolean
13  entry?: CortexEntry
14}
15
16const heatRung = (heat: number): number => Math.min(3, Math.max(0, Math.floor(heat * 4)))
17
18// One ruled line per call: what was asked, and the lexicon word for where it stands.
19export const UseRowView = (ui: Ui, r: UseRow) => {
20  const { Box, Text } = ui
21  const status = r.isErrored ? 'blocked' : r.isRunning ? 'pending' : 'measured'
22  const color = r.isErrored
23    ? tone(r.surface, 'danger')
24    : r.isRunning
25      ? tone(r.surface, 'warn')
26      : undefined
27  const e = r.entry
28  const facts = [
29    status,
30    e?.ms === undefined ? undefined : `${group(e.ms)} ms`,
31    e?.receipt === undefined ? undefined : `rcpt ${e.receipt}`,
32  ].filter((f): f is string => f !== undefined)
33
34  return (
35    <Box>
36      <Text dimColor>cortex ▸ </Text>
37      <Text bold>{r.tool}</Text>
38      <Text dimColor>{r.gist === '' ? '' : `  ${r.gist}`}</Text>
39      <Text color={color} dimColor={color === undefined}>
40        {'  '}
41        {facts.join(' · ')}
42      </Text>
43    </Box>
44  )
45}
46
47export const RecallResultView = (ui: Ui, tool: string, v: RecallView) => {
48  const { Box, Text } = ui
49  const head = [
50    `${group(v.count)} rows`,
51    v.receipt === undefined ? undefined : `rcpt ${v.receipt}`,
52    v.intent === undefined ? undefined : `intent ${v.intent}`,
53    v.dropped === undefined ? undefined : `${group(v.dropped)} low-signal dropped`,
54  ].filter((f): f is string => f !== undefined)
55
56  return (
57    <Box flexDirection="column">
58      <Text>
59        <Text bold>{tool}</Text>
60        <Text dimColor> · {head.join(' · ')}</Text>
61      </Text>
62      {v.count === 0 && <Text dimColor>no memory matched</Text>}
63      {v.shown.map((hit) => (
64        <Text>
65          {hit.heat === undefined ? (
66            <Text dimColor>· </Text>
67          ) : (
68            <Text color={HEAT_TRACK[heatRung(hit.heat)]}>■ </Text>
69          )}
70          <Text dimColor>
71            {hit.score === undefined ? '    ' : hit.score.toFixed(2)}{' '}
72            {hit.heat === undefined ? '' : `heat ${hit.heat.toFixed(2)} `}
73            {hit.domain ?? 'no domain'}{' '}
74          </Text>
75          {hit.content}
76        </Text>
77      ))}
78      {v.hidden > 0 && <Text dimColor>{group(v.hidden)} more not shown here</Text>}
79    </Box>
80  )
81}
82
hooks/tokens.ts 41 lines
1// Colour tokens of the AI Architect design system, resolved for a terminal.
2// A terminal has no CSS variables, so each token is converted from its oklch
3// definition to sRGB (OKLab matrices, Ottosson 2020) and written here once.
4// Chrome stays greyscale: it uses the terminal's own theme keys and dimColor.
5// Only the single accent and data/status tones are coloured (gate G3, G4).
6// A value marked "clipped" was outside the sRGB gamut and is clamped.
7const DS = 'AI Architect Design System/tokens/colors.css'
8
9export type Tone = 'accent' | 'ok' | 'warn' | 'danger' | 'info'
10export type Surface = 'ink' | 'paper'
11
12// source: DS :L27 --accent (terracotta; selection only, never a data category)
13export const ACCENT = '#cf6e39'
14
15// source: DS :L85-88 --ok --warn --danger --info (lifted tones, dark terminal)
16const LIFTED: Record<Exclude<Tone, 'accent'>, string> = {
17  ok: '#65c98c',
18  warn: '#e8aa4e',
19  danger: '#e86154',
20  info: '#57b8e3',
21}
22
23// source: DS :L57-61 --accent-deep --ok-deep --warn-deep --danger-deep --info-deep
24// (warn, info and accent-deep clipped to sRGB), deep inks for a light terminal
25const DEEP: Record<Tone, string> = {
26  accent: '#a53e00',
27  ok: '#0a693c',
28  warn: '#845b00',
29  danger: '#a52a24',
30  info: '#006185',
31}
32
33export const tone = (surface: Surface, t: Tone): string =>
34  surface === 'paper' ? DEEP[t] : t === 'accent' ? ACCENT : LIFTED[t]
35
36// source: DS :L65-68 --heat-hot --heat-warm --heat-cool --heat-cold
37// Heat track, the one gradient the gate allows (G6): cold to hot.
38export const HEAT_TRACK = ['#645d51', '#9c896a', '#d99c68', '#fd9976'] as const
39
40export const SOURCE = DS
41
hooks/view.tsx 184 lines
1import type { Elements, RenderChildren } from 'claude-code'
2
3import type { CortexEntry, CortexStats, HygieneSnapshot, StageTally, TurnTally } from '../types'
4import type { ContextHealth, GeniusState, PolicyState, Refusal } from './deps'
5import { ageLabel, cacheShare, countBy, group, heatCells } from './model'
6import { ContextPanel, HygienePanel, PipelinePanel, PolicyPanel, RefusalsPanel, clock } from './panels'
7import { HEAT_TRACK, type Surface, tone } from './tokens'
8
9export type Ui = Elements['terminal']
10
11export type CockpitData = {
12  surface: Surface
13  now: number
14  columns: number
15  repo: string
16  stats: CortexStats | null
17  statsError: string | null
18  ledger: readonly CortexEntry[]
19  tally: TurnTally
20  context: ContextHealth | null
21  stages: Record<string, StageTally>
22  refusals: readonly Refusal[]
23  hygiene: HygieneSnapshot | null
24  policy: PolicyState | null
25  genius: GeniusState | null
26  onRefresh: () => void
27  onConsolidate: () => void
28  onCurateWiki: () => void
29}
30
31const SHOWN = 8 // source: rows of the ledger kept on screen; older rows stay in state
32
33// Micro-label: mono UPPERCASE is the one place the voice allows caps (gate G13).
34const Label = (ui: Ui, text: string) => {
35  const { Text } = ui
36  return (
37    <Text bold dimColor>
38      {text}
39    </Text>
40  )
41}
42
43const HeatTrack = (ui: Ui, value: number) => {
44  const { Text } = ui
45  return (
46    <Text>
47      {heatCells(value, 20).map((c) =>
48        c.isFilled ? <Text color={HEAT_TRACK[c.rung]}>█</Text> : <Text dimColor>·</Text>,
49      )}
50    </Text>
51  )
52}
53
54const Memory = (ui: Ui, d: CockpitData) => {
55  const { Box, Button, Text } = ui
56  const s = d.stats
57  if (s === null) {
58    return d.statsError === null ? (
59      <Text color={tone(d.surface, 'warn')}>stats: pending</Text>
60    ) : (
61      <Text color={tone(d.surface, 'danger')}>stats: blocked, {d.statsError}</Text>
62    )
63  }
64  const stale = Object.entries(s.grooming_staleness)
65  const anyStale = stale.some(([, g]) => g.stale)
66  return (
67    <Box flexDirection="column">
68      <Text>
69        {group(s.total_memories)} memories · episodic {group(s.episodic_count)} · semantic{' '}
70        {group(s.semantic_count)} · active {group(s.active_count)} · stale {group(s.stale_count)}
71      </Text>
72      <Box>
73        <Text>heat {s.avg_heat.toFixed(4)} </Text>
74        {HeatTrack(ui, s.avg_heat)}
75        <Text dimColor>
76          {' '}
77          · {group(s.total_entities)} entities · {group(s.total_relationships)} relationships
78        </Text>
79      </Box>
80      <Text dimColor>
81        last consolidation {s.last_consolidation ?? 'none recorded'} · vector search{' '}
82        {s.has_vector_search ? 'available' : 'absent'}
83      </Text>
84      {stale.map(([kind, g]) => (
85        <Text color={g.stale ? tone(d.surface, 'warn') : undefined} dimColor={!g.stale}>
86          grooming {kind}:{' '}
87          {g.days_since_last_run === null ? 'never run' : `${g.days_since_last_run.toFixed(1)} d`}
88          {g.stale ? ` (stale, threshold ${s.grooming_staleness_threshold_days} d)` : ''}
89        </Text>
90      ))}
91      {anyStale && (
92        <Box gap={1}>
93          <Button key="consolidate" label="Consolidate now" hotkey="c" onPress={d.onConsolidate} />
94          <Button key="curate" label="Queue wiki curation" hotkey="w" onPress={d.onCurateWiki} />
95        </Box>
96      )}
97      <Text dimColor>read {ageLabel(d.now - s.readAt)} ago</Text>
98    </Box>
99  )
100}
101
102const statusColor = (surface: Surface, e: CortexEntry): string | undefined =>
103  e.status === 'blocked'
104    ? tone(surface, 'danger')
105    : e.status === 'pending'
106      ? tone(surface, 'warn')
107      : undefined
108
109const Ledger = (ui: Ui, d: CockpitData) => {
110  const { Box, Text } = ui
111  if (d.ledger.length === 0) return <Text dimColor>no Cortex call this session</Text>
112  const summary = Object.entries(countBy(d.ledger))
113    .sort((a, b) => b[1] - a[1])
114    .map(([tool, n]) => `${group(n)} ${tool}`)
115    .join(' · ')
116  return (
117    <Box flexDirection="column">
118      <Text>{summary}</Text>
119      {d.ledger.slice(-SHOWN).map((e) => (
120        <Text color={statusColor(d.surface, e)} dimColor={e.status === 'measured'}>
121          {clock(e.startedAt)} {e.tool.padEnd(18)}{' '}
122          {(e.ms === undefined ? '…' : `${group(e.ms)} ms`).padStart(9)}{' '}
123          {(e.count === undefined ? 'no count' : `${group(e.count)} rows`).padStart(10)}{' '}
124          {e.receipt === undefined ? '' : `rcpt ${e.receipt} `}
125          {e.status}
126        </Text>
127      ))}
128    </Box>
129  )
130}
131
132const Turns = (ui: Ui, d: CockpitData) => {
133  const { Box, Text } = ui
134  const t = d.tally
135  if (t.turns === 0) return <Text dimColor>no turn completed yet</Text>
136  const share = cacheShare(t)
137  const tools = Object.entries(t.tools)
138    .sort((a, b) => b[1] - a[1])
139    .slice(0, 6)
140    .map(([name, n]) => `${name} ${group(n)}`)
141    .join(' · ')
142  return (
143    <Box flexDirection="column">
144      <Text>
145        {group(t.turns)} turns · in {group(t.input)} · out {group(t.output)} · cache read{' '}
146        {group(t.cacheRead)} · cache write {group(t.cacheWrite)}
147        {share === undefined ? '' : ` · ${(share * 100).toFixed(1)} % from cache`}
148      </Text>
149      <Text dimColor>tools: {tools || 'none'}</Text>
150    </Box>
151  )
152}
153
154const Section = (ui: Ui, label: string, body: RenderChildren) => {
155  const { Box } = ui
156  return (
157    <Box flexDirection="column">
158      {Label(ui, label)}
159      {body}
160    </Box>
161  )
162}
163
164export const Cockpit = (ui: Ui, d: CockpitData) => {
165  const { Box, Button, Text } = ui
166  return (
167    <Box flexDirection="column" gap={1}>
168      <Box flexDirection="column">
169        <Text bold>Cortex cockpit</Text>
170        <Text dimColor>{'─'.repeat(Math.max(8, d.columns))}</Text>
171      </Box>
172      {Section(ui, 'CONTEXT', ContextPanel(ui, d.surface, d.context, d.now))}
173      {Section(ui, 'MEMORY', Memory(ui, d))}
174      {Section(ui, 'PIPELINE SPEC → CODE', PipelinePanel(ui, d.surface, d.stages))}
175      {Section(ui, 'HYGIENE', HygienePanel(ui, d.surface, d.hygiene, d.repo, d.now))}
176      {Section(ui, 'POLICY', PolicyPanel(ui, d.surface, d.policy, d.genius))}
177      {Section(ui, 'ENFORCED', RefusalsPanel(ui, d.surface, d.refusals))}
178      {Section(ui, 'CORTEX CALLS', Ledger(ui, d))}
179      {Section(ui, 'TURNS', Turns(ui, d))}
180      <Button key="refresh" label="Refresh" hotkey="r" onPress={d.onRefresh} />
181    </Box>
182  )
183}
184
hooks/panels.tsx 219 lines
1import type { HygieneSnapshot, StageTally } from '../types'
2import { bar, tokensLeft } from './contextview'
3import type { ContextHealth, GeniusState, PolicyDecision, PolicyState, Refusal } from './deps'
4import { shortPath } from './hygiene'
5import { ageLabel, group } from './model'
6import { STAGES, shortName } from './pipeline'
7import { type Surface, tone } from './tokens'
8import type { Ui } from './view'
9
10const pad2 = (n: number): string => String(n).padStart(2, '0')
11export const clock = (ms: number): string => {
12  const d = new Date(ms)
13  return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`
14}
15
16const BAR_WIDTH = 30 // source: own choice, fits the narrowest docked pane the engine seats
17const TOP = 5 // source: own choice, the rows a one-line summary can carry
18
19export const ContextPanel = (ui: Ui, surface: Surface, c: ContextHealth | null, now: number) => {
20  const { Box, Text } = ui
21  if (c === null) return <Text dimColor>context: pending (no response measured yet)</Text>
22  const b = c.band
23  const color =
24    b === 'hard' ? tone(surface, 'danger') : b === 'warn' ? tone(surface, 'warn') : undefined
25  const left = tokensLeft(c)
26  const limits = c.rateLimits.map((r) => `${r.kind} ${r.percentUsed} %`).join(' · ')
27  return (
28    <Box flexDirection="column">
29      <Box>
30        <Text color={color}>{bar(c, BAR_WIDTH)} </Text>
31        <Text color={color}>
32          {c.tokens === null ? 'no reading' : `${group(c.tokens)} / ${group(c.window)}`}
33          {c.percent === null ? '' : ` · ${c.percent} %`} · {b}
34        </Text>
35      </Box>
36      <Text dimColor>
37        {c.warn === null
38          ? `no thresholds for ${c.model} (${c.thresholdsSource})`
39          : `warn ${group(c.warn)} · hard ${group(c.hard ?? 0)} · ${
40              left.toWarn === null ? '' : left.toWarn > 0 ? `${group(left.toWarn)} to warn` : 'past warn'
41            }${left.toHard === null ? '' : left.toHard > 0 ? ` · ${group(left.toHard)} to hard` : ' · past hard'} · ${c.model}`}
42      </Text>
43      <Text dimColor>
44        {limits === '' ? 'rate limits: no reading' : limits}
45        {c.usd === null ? '' : ` · cost ${c.usd.toFixed(2)} USD`} · read {ageLabel(now - c.readAt)}{' '}
46        ago
47      </Text>
48      {c.categories.length > 0 && (
49        <Text dimColor>
50          by category:{' '}
51          {c.categories
52            .filter((k) => k.tokens > 0)
53            .slice(0, TOP)
54            .map((k) => `${k.name} ${group(k.tokens)}`)
55            .join(' · ')}
56        </Text>
57      )}
58      {c.mcpServers.length > 0 && (
59        <Text dimColor>
60          MCP schemas:{' '}
61          {c.mcpServers
62            .slice(0, TOP)
63            .map((s) => `${s.server} ${group(s.tokens)} (${s.tools} tools)`)
64            .join(' · ')}
65        </Text>
66      )}
67    </Box>
68  )
69}
70
71export const PipelinePanel = (ui: Ui, surface: Surface, stages: Record<string, StageTally>) => {
72  const { Box, Text } = ui
73  const any = STAGES.some((s) => (stages[s]?.calls ?? 0) > 0)
74  if (!any) return <Text dimColor>no pipeline tool called this session</Text>
75  return (
76    <Box flexDirection="column">
77      {STAGES.map((name) => {
78        const s = stages[name] ?? { calls: 0, blocked: 0, lastAt: null, lastTool: null }
79        const idle = s.calls === 0
80        const color = s.blocked > 0 ? tone(surface, 'danger') : undefined
81        return (
82          <Text color={color} dimColor={idle}>
83            {idle ? '○' : s.blocked > 0 ? '✗' : '●'} {name.padEnd(12)}
84            {idle
85              ? 'not reached'
86              : `${group(s.calls)} calls${s.blocked > 0 ? ` · ${group(s.blocked)} blocked` : ''} · last ${
87                  s.lastTool === null ? '' : shortName(s.lastTool)
88                } ${s.lastAt === null ? '' : clock(s.lastAt)}`}
89          </Text>
90        )
91      })}
92    </Box>
93  )
94}
95
96export const HygienePanel = (
97  ui: Ui,
98  surface: Surface,
99  snap: HygieneSnapshot | null,
100  repo: string,
101  now: number,
102) => {
103  const { Box, Text } = ui
104  if (snap === null) return <Text dimColor>hygiene: pending</Text>
105  if (snap.error !== null)
106    return <Text color={tone(surface, 'danger')}>hygiene: blocked, {snap.error}</Text>
107  const extra = snap.worktrees.filter((w) => !w.isMain)
108  return (
109    <Box flexDirection="column">
110      {extra.length === 0 && <Text dimColor>no worktree beside the main checkout</Text>}
111      {extra.map((w) => {
112        const bad = !w.isInsideRepoRule || !w.isRegistered
113        return (
114          <Text color={bad ? tone(surface, 'danger') : undefined}>
115            {bad ? '✗' : '●'} {shortPath(w.path, repo)}
116            {w.branch === null ? '' : ` · ${w.branch.replace('refs/heads/', '')}`}
117            {w.isInsideRepoRule ? '' : ' · outside <repo>/.claude/worktrees/'}
118            {w.isRegistered ? ` · ${w.owner ?? 'owner unknown'}` : ' · not registered with disk_hygiene'}
119            {w.pr === null ? '' : ` · ${w.pr}`}
120          </Text>
121        )
122      })}
123      {snap.testProcesses.length === 0 ? (
124        <Text dimColor>no test runner running</Text>
125      ) : (
126        snap.testProcesses.map((p) => (
127          <Text color={tone(surface, 'warn')}>
128            ▶ pid {p.pid} · {p.elapsed} · {p.command}
129          </Text>
130        ))
131      )}
132      <Text dimColor>read {ageLabel(now - snap.readAt)} ago</Text>
133    </Box>
134  )
135}
136
137const SHOWN_REFUSALS = 6 // source: own choice, the newest refusals on screen
138
139export const RefusalsPanel = (ui: Ui, surface: Surface, refusals: readonly Refusal[]) => {
140  const { Box, Text } = ui
141  if (refusals.length === 0) return <Text dimColor>no call refused this session</Text>
142  return (
143    <Box flexDirection="column">
144      <Text>
145        {group(refusals.length)} refused · wiki pages only through wiki_write · worktrees inside the
146        repo
147      </Text>
148      {refusals.slice(-SHOWN_REFUSALS).map((r) => (
149        <Text color={tone(surface, 'danger')}>
150          {clock(r.at)} {r.tool.padEnd(12)} {r.rule.padEnd(22)} {r.target}
151        </Text>
152      ))}
153    </Box>
154  )
155}
156
157const SHOWN_DECISIONS = 6 // source: own choice, the newest decisions on screen
158
159type AnyDecision = PolicyDecision | GeniusState['decisions'][number]
160const decisionRow = (d: AnyDecision): string =>
161  `${clock(d.at)} ${d.kind.padEnd(6)} ${d.subject.padEnd(28)} ${'from' in d ? `${d.from} → ` : '→ '}${d.to}${
162    d.applied ? '' : ' · observed'
163  }`
164
165const GeniusLine = (ui: Ui, g: GeniusState | null) => {
166  const { Text } = ui
167  if (g === null) return <Text dimColor>genius: not loaded</Text>
168  const c = g.classified
169  const picks = c === null ? [] : [...c.geniuses.map((p) => `${p.agent} · ${p.shape}`), ...c.shapes]
170  const last =
171    c === null
172      ? 'no request graded yet'
173      : `last: ${c.taskClass} → effort ${c.effort} · ${picks.length === 0 ? 'no pattern' : picks.join(' · ')} · ${c.ms} ms`
174  return (
175    <Text dimColor>
176      genius {g.mode} · classifier {g.classifierModel} · {g.skillShapesLoaded} skills · {g.geniusShapesLoaded} genius
177      shapes · {last}
178      {g.classifierError === null ? '' : ` · ${g.classifierError}`}
179    </Text>
180  )
181}
182
183export const PolicyPanel = (ui: Ui, surface: Surface, p: PolicyState | null, g: GeniusState | null) => {
184  const { Box, Text } = ui
185  if (p === null)
186    return (
187      <Box flexDirection="column">
188        <Text dimColor>autopilot: not loaded</Text>
189        {GeniusLine(ui, g)}
190      </Box>
191    )
192  const pressureColor = p.pressure === 'none' ? undefined : tone(surface, 'warn')
193  const decisions: AnyDecision[] = [...p.decisions, ...(g?.decisions ?? [])].sort((a, b) => a.at - b.at)
194  return (
195    <Box flexDirection="column">
196      <Text>
197        <Text bold>{p.mode}</Text>
198        <Text dimColor>
199          {' '}
200          · ladder from the graded rung, loop at medium, pressure at low · quota pressure from {p.quotaPercent} % of
201          5 h · results capped at {group(p.resultCapChars)} chars
202        </Text>
203      </Text>
204      <Text color={pressureColor}>
205        pressure: {p.pressure}
206        {p.charsCut > 0
207          ? ` · ${group(p.charsCut)} chars kept out, about ${group(Math.round(p.charsCut / 4))} tokens (estimated)`
208          : ''}
209      </Text>
210      {GeniusLine(ui, g)}
211      {decisions.length === 0 ? (
212        <Text dimColor>no decision yet</Text>
213      ) : (
214        decisions.slice(-SHOWN_DECISIONS).map((d) => <Text dimColor={!d.applied}>{decisionRow(d)}</Text>)
215      )}
216    </Box>
217  )
218}
219
hooks/contextview.ts 25 lines
1// Display-only readings of the context window the autopilot measures.
2
3import type { ContextHealth } from './deps'
4
5// A bar of `width` cells: the fill, and where the warn and hard marks fall.
6export const bar = (c: ContextHealth, width: number): string => {
7  const cell = (n: number | null) =>
8    n === null ? -1 : Math.min(width - 1, Math.round((n / c.window) * width))
9  const fill = c.tokens === null ? 0 : Math.round((c.tokens / c.window) * width)
10  const warnAt = cell(c.warn)
11  const hardAt = cell(c.hard)
12  let out = ''
13  for (let i = 0; i < width; i++) {
14    if (i === hardAt) out += '┃'
15    else if (i === warnAt) out += '│'
16    else out += i < fill ? '█' : '·'
17  }
18  return out
19}
20
21export const tokensLeft = (c: ContextHealth): { toWarn: number | null; toHard: number | null } => ({
22  toWarn: c.tokens === null || c.warn === null ? null : c.warn - c.tokens,
23  toHard: c.tokens === null || c.hard === null ? null : c.hard - c.tokens,
24})
25
types/index.d.ts 83 lines
1export type CortexStatus = 'pending' | 'measured' | 'blocked'
2
3export type CortexEntry = {
4  id: string
5  tool: string
6  startedAt: number
7  status: CortexStatus
8  ms?: number
9  count?: number
10  receipt?: number
11}
12
13export type GroomingKind = {
14  days_since_last_run: number | null
15  stale: boolean
16}
17
18export type CortexStats = {
19  total_memories: number
20  episodic_count: number
21  semantic_count: number
22  active_count: number
23  archived_count: number
24  stale_count: number
25  protected_count: number
26  avg_heat: number
27  total_entities: number
28  total_relationships: number
29  active_triggers: number
30  last_consolidation: string | null
31  has_vector_search: boolean
32  grooming_staleness: Record<string, GroomingKind>
33  grooming_staleness_threshold_days: number
34  readAt: number
35}
36
37export type TurnTally = {
38  turns: number
39  input: number
40  output: number
41  cacheRead: number
42  cacheWrite: number
43  tools: Record<string, number>
44}
45
46// One stage of the spec-to-code pipeline, as observed from the tools the model called.
47export type StageTally = {
48  calls: number
49  blocked: number
50  lastAt: number | null
51  lastTool: string | null
52}
53
54export type Worktree = {
55  path: string
56  branch: string | null
57  isMain: boolean
58  isInsideRepoRule: boolean
59  isRegistered: boolean
60  owner: string | null
61  pr: string | null
62}
63
64export type HygieneSnapshot = {
65  worktrees: Worktree[]
66  testProcesses: { pid: number; elapsed: string; command: string }[]
67  error: string | null
68  readAt: number
69}
70
71declare module 'claude-code' {
72  interface PluginState {
73    'cortex-cockpit': {
74      ledger: CortexEntry[]
75      stats: CortexStats | null
76      statsError: string | null
77      tally: TurnTally
78      stages: Record<string, StageTally>
79      hygiene: HygieneSnapshot | null
80    }
81  }
82}
83