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

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
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.
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.
Comparisons are about design, not quality: the projects below are good, and this one borrows from them.
| Common in prompt-only workflow skills | SSI |
|---|---|
| The model remembers the phase | A script derives the phase from git, the PR and recorded proof |
| You call each step or skill yourself | One entry point; it switches phases itself |
| Stopping is a judgement call | Stops and thresholds are computed (diff size, protected paths from the real diff, retry budgets) |
| An interrupted run starts over | It resumes, restores notes from the PR and redoes only stale proof |
| Review is the same model re-reading its work | Codex if it works, else a sub-agent on another model; the report says whether it was independent |
| Output tuned for experts | One short template: where we are, what happened, what is next, at most one question; optional adhd style |
| Remote side effects are ad hoc | Keyed and idempotent: no duplicate Issue or comment; evidence GIF on a ssi-assets branch |
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.
Exact versions and licences: skills/ssi/NOTICE.md.
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.
hooks/register.tsx 101 lines1import { 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}
101types/index.d.ts 8 lines1export 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