SLOPSHOPPER

ssi-cockpit

Progress band, toasts and a phase map for the ssi loop

newpanebandcommandtoaststatus
v0.1.0MITupdated 2026-10-04ssime-git/ssi-ai-skill/mods/ssi-cockpit
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · ssi-cockpit
│ ┃ SSI map ✕ › fix the failing auth test and add an audit log call │ ┃ No ssi run here yet. Ask for a fix or a │ ┃ feature with /ssi. ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /ssi-map │ ⎿ ssi-cockpit: SSI map opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · SSI map
No ssi run here yet. Ask for a fix or a feature with /ssi.
README

SSI

One command takes a request to a reviewed draft PR. A small script, not the model, decides what comes next, so it remembers where it was and repairs itself after an interruption. It never merges, closes an Issue or deploys.

/ssi Fix: total() ignores percentage discounts

The flow

flowchart LR
  A[1 Analyze] --> B[2 Confirm] --> C[3 Reproduce<br/>bugs only] --> D[4 Plan<br/>reuse first] --> E[5 Implement<br/>draft PR] --> F[6 Review<br/>Codex or sub-agent] --> G[7 Visual GIF<br/>UI only] --> H[8 Land<br/>CI green] --> Z([Draft PR<br/>ready for you])
  F -- blocker --> E
  G -- wrong --> E
  H -- CI red --> E
  A -.-> Q{{One question<br/>to you}}
  C -.-> Q
  D -.-> Q
  E -.-> Q
  F -.-> Q

It runs on its own and asks one question only when it must: unclear request, no reproduction, protected code touched, a big diff, a review or CI that keeps failing, or an action outside its mandate.

What happens inside

sequenceDiagram
  participant You
  participant Skill as Model (skill)
  participant Engine as ssi engine (script)
  participant World as git + PR
  You->>Skill: /ssi <request>
  loop until done or a question
    Skill->>Engine: ssi next
    Engine->>World: read branch, PR, recorded proof
    Engine-->>Skill: the one next action, or a stop
    Skill->>Skill: do the work (tests, code, review, GIF)
    Skill->>Engine: ssi record (the proof)
  end
  Engine-->>You: draft PR ready, or one question

If the notes and the repo disagree, the repo wins. Lost local notes are restored from a block in the PR's progress comment. A new commit that touches proven files makes that proof stale and it is redone.

What is different

Comparisons are about design, not quality: the projects below are good, and this one borrows from them.

Common in prompt-only workflow skillsSSI
The model remembers the phaseA script derives the phase from git, the PR and recorded proof
You call each step or skill yourselfOne entry point; it switches phases itself
Stopping is a judgement callStops and thresholds are computed (diff size, protected paths from the real diff, retry budgets)
An interrupted run starts overIt resumes, restores notes from the PR and redoes only stale proof
Review is the same model re-reading its workCodex if it works, else a sub-agent on another model; the report says whether it was independent
Output tuned for expertsOne short template: where we are, what happened, what is next, at most one question; optional adhd style
Remote side effects are ad hocKeyed and idempotent: no duplicate Issue or comment; evidence GIF on a ssi-assets branch

Install and use

Needs Node 20+, git, and gh logged in. Optional: Codex CLI (review), Chrome or Playwright and ffmpeg (GIF).

cp -R skills/ssi ~/.claude/skills/ssi          # the folder is self-contained
claude --plugin-dir .                          # or load the repo as a plugin

Settings live in ssi.config.json (repo) and .ssi/config.local.json (yours); ssi config prints them. Optional status band and phase map: mods/ssi-cockpit.

Inspired by

Exact versions and licences: skills/ssi/NOTICE.md.

Read more

Status

Early. 134 engine tests, three real runs, six rounds of Codex review (the last round's fixes are not re-reviewed yet). MIT, see LICENSE.

Source 2 files
hooks/register.tsx 101 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Snapshot } from '../types'
5
6const PANE = 'ssi-map'
7const NAMES = ['Analyze', 'Confirm', 'Reproduce', 'Plan', 'Implement', 'Review', 'Visual', 'Land']
8const ETA = [3, 4, 8, 5, 15, 8, 6, 10]
9
10const tick = atom({ plugin: 'ssi-cockpit', key: 'tick' } as const, 0)
11const lastPhase = atom({ plugin: 'ssi-cockpit', key: 'lastPhase' } as const, 0)
12const lastStop = atom({ plugin: 'ssi-cockpit', key: 'lastStop' } as const, '')
13
14async function load($: any): Promise<Snapshot | null> {
15  try {
16    const state = JSON.parse((await $.fs.read('.ssi/state.json')) as string)
17    const open = state.stop && !state.stop.answer
18    // `done` and `action` are what the engine actually answered last: phase 9 alone is not completion.
19    return { phase: Number(state.phase ?? 1), goal: String(state.goal ?? ''), stop: open ? String(state.stop.because) : null, done: state.last?.done === true, action: state.last?.action ? String(state.last.action) : null }
20  } catch {
21    return null
22  }
23}
24
25const bar = (phase: number) => '▓'.repeat(Math.min(phase, 8)) + '░'.repeat(8 - Math.min(phase, 8))
26
27async function refresh($: any) {
28  await update($, tick, n => n + 1)
29  const snap = await load($)
30  if (!snap) {
31    $.ui.status(undefined)
32    return
33  }
34  const p = Math.min(snap.phase, 8)
35  $.ui.status(snap.stop ? 'ssi: stopped' : snap.done ? 'ssi: done' : snap.phase > 8 ? `ssi: finishing (${snap.action ?? '…'})` : `ssi ${p}/8 ${NAMES[p - 1]}`)
36  const before = await read($, lastPhase)
37  const stopBefore = await read($, lastStop)
38  const stopNow = snap.stop ?? ''
39  if (stopNow !== stopBefore) {
40    await update($, lastStop, () => stopNow)
41    if (stopNow) $.ui.toast('I stopped and need you.')
42  }
43  if (before !== snap.phase) {
44    await update($, lastPhase, () => snap.phase)
45    if (before !== 0 && !stopNow) {
46      $.ui.toast(snap.done ? '✓ Done. The PR is ready for you.' : snap.phase > 8 ? `One last step: ${snap.action ?? 'finish the delivery'}` : snap.phase < before ? `↩ Back to ${NAMES[p - 1]}: something needs a fix.` : `✓ Phase done. Now: ${NAMES[p - 1]}`)
47    }
48  }
49}
50
51export const register: Register = on => {
52  on('session.start', async ($, e, next) => {
53    await $.command.register({ name: 'ssi-map', description: 'Show the ssi phase map' })
54    await refresh($)
55    return next(e)
56  })
57
58  on('command.run', { command: 'ssi-map' }, async $ => {
59    await $.ui.open({ id: PANE, title: 'SSI map' })
60    return { text: 'SSI map opened.' }
61  })
62
63  on('turn.complete', async ($, e, next) => {
64    await refresh($)
65    return next(e)
66  })
67
68  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
69    await read($, tick)
70    const snap = await load($)
71    if (!snap || e.props.hasSurvey) return next(e)
72    const { Box, Text } = $.ui.resolve(e)
73    const p = Math.min(snap.phase, 8)
74    const where = snap.done ? `${bar(8)} 8/8 Done` : snap.phase > 8 ? `${bar(8)} 8/8 Finishing: ${snap.action ?? '…'}` : `${bar(p)} ${p}/8 ${NAMES[p - 1]}`
75    return (
76      <Box>
77        <Text>{where}</Text>
78        {snap.stop ? <Text bold> · I stopped: {snap.stop}</Text> : snap.phase <= 8 ? <Text dimColor> · ~{ETA[p - 1]} min</Text> : null}
79      </Box>
80    )
81  })
82
83  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
84    await read($, tick)
85    const snap = await load($)
86    const { Box, Text } = $.ui.resolve(e)
87    if (!snap) return <Text dimColor>No ssi run here yet. Ask for a fix or a feature with /ssi.</Text>
88    return (
89      <Box flexDirection="column">
90        <Text bold>{snap.goal}</Text>
91        {NAMES.map((name, i) => {
92          const mark = snap.phase > i + 1 ? '✓' : snap.phase === i + 1 ? '▶' : '·'
93          return <Text dimColor={snap.phase < i + 1}>{mark} {i + 1}. {name}{snap.phase === i + 1 ? `  ~${ETA[i]} min` : ''}</Text>
94        })}
95        {snap.stop ? <Text bold>I stopped: {snap.stop}</Text> : null}
96        <Text dimColor>Back edges: review, visual or CI failing returns to 5. Implement.</Text>
97      </Box>
98    )
99  })
100}
101
types/index.d.ts 8 lines
1export type Snapshot = { phase: number; goal: string; stop: string | null; done: boolean; action: string | null }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'ssi-cockpit': { tick: number; lastPhase: number; lastStop: string }
6  }
7}
8