@flavor-code/superharness 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/HARNESS.md +56 -0
  2. package/flavor-plugin.json +25 -0
  3. package/index.js +238 -0
  4. package/package.json +23 -0
  5. package/scripts/ralph-lib.ps1 +297 -0
  6. package/scripts/ralph-lib.sh +366 -0
  7. package/skills/brainstorm/SKILL.md +179 -0
  8. package/skills/brainstorm/scripts/layout.js +76 -0
  9. package/skills/brainstorm/scripts/mindmap.html +249 -0
  10. package/skills/brainstorm/scripts/server.cjs +208 -0
  11. package/skills/brainstorm/scripts/start-server.ps1 +57 -0
  12. package/skills/brainstorm/scripts/stop-server.ps1 +17 -0
  13. package/skills/finishing-a-development-branch/SKILL.md +112 -0
  14. package/skills/go/SKILL.md +169 -0
  15. package/skills/light/SKILL.md +85 -0
  16. package/skills/requesting-code-review/SKILL.md +103 -0
  17. package/skills/requesting-code-review/code-reviewer.md +168 -0
  18. package/skills/subagent-driven-development/SKILL.md +125 -0
  19. package/skills/systematic-debugging/SKILL.md +296 -0
  20. package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  21. package/skills/systematic-debugging/condition-based-waiting.md +115 -0
  22. package/skills/systematic-debugging/defense-in-depth.md +122 -0
  23. package/skills/systematic-debugging/find-polluter.sh +63 -0
  24. package/skills/systematic-debugging/root-cause-tracing.md +169 -0
  25. package/skills/test-driven-development/SKILL.md +371 -0
  26. package/skills/test-driven-development/testing-anti-patterns.md +299 -0
  27. package/skills/using-git-worktrees/SKILL.md +91 -0
  28. package/skills/verification-before-completion/SKILL.md +139 -0
  29. package/skills/writing-plans/SKILL.md +138 -0
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env bash
2
+ # Ralph state mechanism — zero-dependency bash state library (macOS / Linux).
3
+ # Counterpart of scripts/ralph-lib.ps1.
4
+ #
5
+ # Manages the four runtime files of a resumable autonomous-task loop, all under
6
+ # <project>/<state-root>/superharness/ralph/ where <state-root> follows the host:
7
+ # .claude (Claude Code install: .claude/superharness marketplace)
8
+ # .flavor (flavor-code install: .flavor/plugins/superharness)
9
+ # .current-task one-line pointer to the active task (switch = rewrite the line)
10
+ # task.json task-list snapshot {status,phase,sprint,tasks[],updated_at}
11
+ # trace.jsonl append-only ledger, one {ts,phase,event,detail} JSON per line
12
+ # .ralph-state.json retry counter {retries,max,updated_at}, capped at 5
13
+ #
14
+ # Source this file to use the functions. The trace hooks (hooks/stop.sh,
15
+ # hooks/user-prompt-submit.sh) source it for go task tracking. Conventions:
16
+ # UTF-8 without BOM, atomic temp-then-move for JSON snapshots, ISO-8601 timestamps.
17
+ #
18
+ # JSON handling needs node (preferred) or python3 as a fallback.
19
+
20
+ # ---------------------------------------------------------------- json helper
21
+
22
+ ralph_json_escape() {
23
+ # $1 = raw string -> prints JSON-escaped content WITHOUT surrounding quotes.
24
+ local __raw="$1"
25
+ if command -v node >/dev/null 2>&1; then
26
+ RALPH_ESC="$__raw" node -e 'process.stdout.write(JSON.stringify(process.env.RALPH_ESC).slice(1,-1))'
27
+ elif command -v python3 >/dev/null 2>&1; then
28
+ RALPH_ESC="$__raw" python3 -c 'import json,os;print(json.dumps(os.environ["RALPH_ESC"])[1:-1],end="")'
29
+ else
30
+ # minimal fallback: backslash then double quote
31
+ printf '%s' "${__raw//\\/\\\\}" | sed 's/"/\\"/g' | awk '{if(NR>1)printf "\\n";printf "%s",$0}'
32
+ fi
33
+ }
34
+
35
+ ralph_json_get() {
36
+ # $1 = file $2... = property path (top-level keys only, one per arg)
37
+ # Prints the value as text (objects/arrays as JSON), nothing when absent.
38
+ local __f="$1"; shift
39
+ [ -f "$__f" ] || return 0
40
+ if command -v node >/dev/null 2>&1; then
41
+ node -e '
42
+ const fs=require("fs");
43
+ let o; try{o=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));}catch{process.exit(0);}
44
+ let v=o;
45
+ for(let i=2;i<process.argv.length;i++){ if(v==null||typeof v!=="object"){process.exit(0);} v=v[process.argv[i]]; }
46
+ if(v===undefined||v===null) process.exit(0);
47
+ process.stdout.write(typeof v==="object"?JSON.stringify(v):String(v));
48
+ ' "$__f" "$@"
49
+ elif command -v python3 >/dev/null 2>&1; then
50
+ python3 - "$__f" "$@" <<'PY'
51
+ import json,sys
52
+ try:
53
+ o=json.load(open(sys.argv[1],encoding="utf-8"))
54
+ except Exception:
55
+ sys.exit(0)
56
+ v=o
57
+ for k in sys.argv[2:]:
58
+ if not isinstance(v,dict) or k not in v: sys.exit(0)
59
+ v=v[k]
60
+ if v is None: sys.exit(0)
61
+ print(json.dumps(v) if isinstance(v,(dict,list)) else v,end="")
62
+ PY
63
+ fi
64
+ }
65
+
66
+ # ---------------------------------------------------------------- paths & helpers
67
+
68
+ ralph_parse_hook_stdin() {
69
+ # Parse hook input JSON from stdin. Writes the `cwd` and `prompt` fields to two
70
+ # temp files and sets RALPH_HOOK_CWD_FILE / RALPH_HOOK_PROMPT_FILE. Returns 1
71
+ # when stdin is empty or not valid JSON. Needs node or python3.
72
+ local raw
73
+ raw="$(cat || true)"
74
+ printf '%s' "$raw" | grep -q '[^[:space:]]' || return 1
75
+ local tmpdir="${TMPDIR:-/tmp}"
76
+ RALPH_HOOK_CWD_FILE="$tmpdir/ralph-hook-cwd.$$"
77
+ RALPH_HOOK_PROMPT_FILE="$tmpdir/ralph-hook-prompt.$$"
78
+ if command -v node >/dev/null 2>&1; then
79
+ printf '%s' "$raw" | RALPH_CWD_F="$RALPH_HOOK_CWD_FILE" RALPH_PROMPT_F="$RALPH_HOOK_PROMPT_FILE" node -e '
80
+ const fs = require("fs");
81
+ let s = "";
82
+ process.stdin.setEncoding("utf8");
83
+ process.stdin.on("data", d => { s += d; });
84
+ process.stdin.on("end", () => {
85
+ let o;
86
+ try { o = JSON.parse(s); } catch { process.exit(1); }
87
+ fs.writeFileSync(process.env.RALPH_CWD_F, typeof o.cwd === "string" ? o.cwd : "");
88
+ fs.writeFileSync(process.env.RALPH_PROMPT_F, typeof o.prompt === "string" ? o.prompt : "");
89
+ });
90
+ ' || return 1
91
+ elif command -v python3 >/dev/null 2>&1; then
92
+ printf '%s' "$raw" | RALPH_CWD_F="$RALPH_HOOK_CWD_FILE" RALPH_PROMPT_F="$RALPH_HOOK_PROMPT_FILE" python3 -c '
93
+ import json, os, sys
94
+ try:
95
+ o = json.load(sys.stdin)
96
+ except Exception:
97
+ sys.exit(1)
98
+ cwd = o.get("cwd") if isinstance(o.get("cwd"), str) else ""
99
+ prompt = o.get("prompt") if isinstance(o.get("prompt"), str) else ""
100
+ open(os.environ["RALPH_CWD_F"], "w", encoding="utf-8").write(cwd)
101
+ open(os.environ["RALPH_PROMPT_F"], "w", encoding="utf-8").write(prompt)
102
+ ' || return 1
103
+ else
104
+ return 1
105
+ fi
106
+ }
107
+
108
+ ralph_cleanup_hook_stdin() {
109
+ rm -f "${RALPH_HOOK_CWD_FILE:-}" "${RALPH_HOOK_PROMPT_FILE:-}" 2>/dev/null || true
110
+ }
111
+
112
+ ralph_state_root() {
113
+ # Host detection: this library ships inside the host install, so its own path
114
+ # names the state root. SUPERHARNESS_STATE_ROOT ('.claude' | '.flavor') forces
115
+ # a choice for unusual layouts; Claude Code is the historical default.
116
+ case "${SUPERHARNESS_STATE_ROOT:-}" in
117
+ .flavor|flavor) printf '%s' '.flavor'; return ;;
118
+ .claude|claude) printf '%s' '.claude'; return ;;
119
+ esac
120
+ local self="${BASH_SOURCE[0]:-$0}" dir
121
+ dir="$(cd "$(dirname "$self")" 2>/dev/null && pwd)" || dir=""
122
+ case "$dir" in
123
+ */.flavor/plugins/superharness/scripts) printf '%s' '.flavor' ;;
124
+ *) printf '%s' '.claude' ;;
125
+ esac
126
+ }
127
+
128
+ ralph_dir() { printf '%s/%s/superharness/ralph' "$1" "$(ralph_state_root)"; }
129
+
130
+ ralph_iso() { date -u '+%Y-%m-%dT%H:%M:%S+00:00'; }
131
+
132
+ ralph_go_invocation() {
133
+ # Parse a UserPromptSubmit prompt. If it is a `/superharness:go <goal>` invocation
134
+ # (leading slash optional, must be at the start of the prompt), print two lines:
135
+ # line 1: Goal
136
+ # line 2: Slug='YYYY-MM-DD-<kebab|task-HHmmss>'
137
+ # Returns 1 when the prompt is not a go invocation. Pure.
138
+ # Portable across GNU sed and BSD sed (macOS).
139
+ local prompt="$1"
140
+ printf '%s' "$prompt" | grep -qE '^[[:space:]]*/?superharness:go([^A-Za-z0-9_]|$)' || return 1
141
+ # strip the invocation prefix (word boundary already verified above) and trim
142
+ local goal
143
+ goal="$(printf '%s' "$prompt" | sed -E 's/^[[:space:]]*\/?superharness:go[[:space:]]*//')"
144
+ goal="$(printf '%s' "$goal" | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//')"
145
+
146
+ local date_part kebab tokens
147
+ date_part="$(date '+%Y-%m-%d')"
148
+ tokens="$(printf '%s' "$goal" | tr '[:upper:]' '[:lower:]' | LC_ALL=C grep -oE '[a-z0-9]+' | head -n 6 | paste -sd '-' - 2>/dev/null || true)"
149
+ if [ -n "$tokens" ]; then
150
+ kebab="$tokens"
151
+ else
152
+ kebab="task-$(date '+%H%M%S')"
153
+ fi
154
+ printf '%s\n%s-%s\n' "$goal" "$date_part" "$kebab"
155
+ }
156
+
157
+ ralph_mkdir() {
158
+ local dir
159
+ dir="$(ralph_dir "$1")"
160
+ mkdir -p "$dir"
161
+ printf '%s' "$dir"
162
+ }
163
+
164
+ ralph_write_text() {
165
+ # Atomic write: temp file then move-replace. UTF-8 without BOM.
166
+ local path="$1" text="$2"
167
+ mkdir -p "$(dirname "$path")"
168
+ local tmp="$path.tmp.$$"
169
+ printf '%s' "$text" > "$tmp"
170
+ mv -f "$tmp" "$path"
171
+ }
172
+
173
+ ralph_write_json() {
174
+ # $1 = path, $2 = already-serialized JSON text
175
+ ralph_write_text "$1" "$2"
176
+ }
177
+
178
+ ralph_read_json() {
179
+ # Prints raw JSON content when the file exists and is non-empty.
180
+ local path="$1"
181
+ [ -f "$path" ] || return 1
182
+ [ -s "$path" ] || return 1
183
+ cat "$path"
184
+ }
185
+
186
+ # ---------------------------------------------------------------- .current-task
187
+
188
+ ralph_current_task_path() { printf '%s/.current-task' "$(ralph_dir "$1")"; }
189
+
190
+ ralph_set_current_task() {
191
+ # The pointer is a single line; switching a task rewrites only this line.
192
+ local root="$1" task_id="$2"
193
+ task_id="$(printf '%s' "$task_id" | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//')"
194
+ ralph_write_text "$(ralph_current_task_path "$root")" "$task_id"
195
+ }
196
+
197
+ ralph_get_current_task() {
198
+ local p line
199
+ p="$(ralph_current_task_path "$1")"
200
+ [ -f "$p" ] || return 0
201
+ line="$(sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//' "$p" | head -n 1)"
202
+ [ -n "$line" ] && printf '%s' "$line"
203
+ return 0
204
+ }
205
+
206
+ # ---------------------------------------------------------------- task.json
207
+
208
+ ralph_task_path() { printf '%s/task.json' "$(ralph_dir "$1")"; }
209
+
210
+ ralph_init_tasks() {
211
+ # Write a fresh task-list snapshot with an empty task list (the agent enriches
212
+ # it later). $1=root $2=status(planning) $3=phase(plan)
213
+ local root="$1" status="${2:-planning}" phase="${3:-implement}"
214
+ local now
215
+ now="$(ralph_iso)"
216
+ local snapshot="{\"status\":\"$(ralph_json_escape "$status")\",\"phase\":\"$(ralph_json_escape "$phase")\",\"sprint\":{\"current\":0,\"total\":0},\"tasks\":[],\"updated_at\":\"$now\"}"
217
+ ralph_write_json "$(ralph_task_path "$root")" "$snapshot"
218
+ }
219
+
220
+ ralph_get_tasks() {
221
+ ralph_read_json "$(ralph_task_path "$1")"
222
+ }
223
+
224
+ ralph_get_phase() {
225
+ # Prints task.json's phase, or 'go' when absent.
226
+ local phase
227
+ phase="$(ralph_json_get "$(ralph_task_path "$1")" phase)"
228
+ if [ -n "$phase" ]; then printf '%s' "$phase"; else printf 'go'; fi
229
+ }
230
+
231
+ # ---------------------------------------------------------------- trace.jsonl
232
+
233
+ ralph_trace_path() { printf '%s/trace.jsonl' "$(ralph_dir "$1")"; }
234
+
235
+ ralph_add_trace() {
236
+ # Append a single minified {ts,phase,event,detail} line. Never rewrites earlier
237
+ # lines — the worst a crash can corrupt is the final line.
238
+ # $1=root $2=phase $3=event $4=detail
239
+ local root="$1" phase="$2" event="$3" detail="${4:-}"
240
+ ralph_mkdir "$root" > /dev/null
241
+ local now line
242
+ now="$(ralph_iso)"
243
+ line="{\"ts\":\"$now\",\"phase\":\"$(ralph_json_escape "$phase")\",\"event\":\"$(ralph_json_escape "$event")\",\"detail\":\"$(ralph_json_escape "$detail")\"}"
244
+ printf '%s\n' "$line" >> "$(ralph_trace_path "$root")"
245
+ }
246
+
247
+ ralph_get_trace_tail() {
248
+ # Print the last N non-empty lines of the ledger (raw JSON lines).
249
+ local root="$1" count="${2:-1}"
250
+ local p
251
+ p="$(ralph_trace_path "$root")"
252
+ [ -f "$p" ] || return 0
253
+ tail -n "$count" "$p" | grep -v '^[[:space:]]*$' || true
254
+ }
255
+
256
+ # ---------------------------------------------------------------- .ralph-state.json (retry counter)
257
+
258
+ ralph_retry_path() { printf '%s/.ralph-state.json' "$(ralph_dir "$1")"; }
259
+
260
+ ralph_get_retry_state() {
261
+ # Prints two lines: retries, max. Defaults to 0, 5 when absent or malformed.
262
+ local root="$1" retries max
263
+ retries="$(ralph_json_get "$(ralph_retry_path "$root")" retries)"
264
+ max="$(ralph_json_get "$(ralph_retry_path "$root")" max)"
265
+ case "$retries" in ''|*[!0-9]*) retries=0 ;; esac
266
+ case "$max" in ''|*[!0-9]*) max=5 ;; esac
267
+ printf '%s\n%s\n' "$retries" "$max"
268
+ }
269
+
270
+ ralph_set_retry_state() {
271
+ # $1=root $2=retries $3=max
272
+ local root="$1" retries="$2" max="$3" now
273
+ now="$(ralph_iso)"
274
+ ralph_write_json "$(ralph_retry_path "$root")" "{\"retries\":$retries,\"max\":$max,\"updated_at\":\"$now\"}"
275
+ }
276
+
277
+ ralph_add_retry() {
278
+ # Increment the retry counter, clamped at max. Prints the new retry count.
279
+ local root="$1" state retries max
280
+ state="$(ralph_get_retry_state "$root")"
281
+ retries="$(printf '%s\n' "$state" | head -n 1)"
282
+ max="$(printf '%s\n' "$state" | tail -n 1)"
283
+ local n=$((retries + 1))
284
+ if [ "$n" -gt "$max" ]; then n=$max; fi
285
+ ralph_set_retry_state "$root" "$n" "$max"
286
+ printf '%s' "$n"
287
+ }
288
+
289
+ ralph_test_retry_exhausted() {
290
+ # Exit status 0 when retries >= max.
291
+ local state retries max
292
+ state="$(ralph_get_retry_state "$1")"
293
+ retries="$(printf '%s\n' "$state" | head -n 1)"
294
+ max="$(printf '%s\n' "$state" | tail -n 1)"
295
+ [ "$retries" -ge "$max" ]
296
+ }
297
+
298
+ ralph_reset_retry() {
299
+ local state max
300
+ state="$(ralph_get_retry_state "$1")"
301
+ max="$(printf '%s\n' "$state" | tail -n 1)"
302
+ ralph_set_retry_state "$1" 0 "$max"
303
+ }
304
+
305
+ # ---------------------------------------------------------------- task bootstrap
306
+
307
+ ralph_start_task() {
308
+ # Auto-bootstrap a fresh go task: point .current-task, seed an empty task.json
309
+ # (planning/plan — the agent enriches the task list later), open the trace ledger
310
+ # with a task:started event, and reset the retry counter. Idempotent-ish: calling
311
+ # again repoints to a new TaskId and appends another task:started line.
312
+ # $1=root $2=task_id $3=goal
313
+ local root="$1" task_id="$2" goal="${3:-}"
314
+ ralph_set_current_task "$root" "$task_id"
315
+ ralph_init_tasks "$root" 'planning' 'plan'
316
+ ralph_add_trace "$root" 'plan' 'task:started' "$goal"
317
+ ralph_reset_retry "$root"
318
+ }
319
+
320
+ # ---------------------------------------------------------------- cold-start recovery
321
+
322
+ ralph_get_resume_context() {
323
+ # Assemble the deterministic file-based facts a freshly-started agent needs to
324
+ # resume: the active pointer, the task snapshot, the last ledger event, and the
325
+ # retry state — as a single JSON object. The agent then reconciles these
326
+ # against `git diff` (code wins) and fixes task.json.
327
+ local root="$1"
328
+ local current tasks last_trace retries max all_done
329
+ current="$(ralph_get_current_task "$root")"
330
+ tasks="$(ralph_get_tasks "$root" || true)"
331
+ last_trace="$(ralph_get_trace_tail "$root" 1 | tail -n 1)"
332
+ local state
333
+ state="$(ralph_get_retry_state "$root")"
334
+ retries="$(printf '%s\n' "$state" | head -n 1)"
335
+ max="$(printf '%s\n' "$state" | tail -n 1)"
336
+
337
+ all_done=false
338
+ if [ -n "$tasks" ]; then
339
+ local remaining=""
340
+ if command -v node >/dev/null 2>&1; then
341
+ remaining="$(RALPH_TASKS="$tasks" node -e '
342
+ let o; try{o=JSON.parse(process.env.RALPH_TASKS||"");}catch{process.exit(0);}
343
+ const ts=Array.isArray(o.tasks)?o.tasks:[];
344
+ process.stdout.write(ts.some(t=>t.status!=="done")?"1":"");
345
+ ')"
346
+ elif command -v python3 >/dev/null 2>&1; then
347
+ remaining="$(RALPH_TASKS="$tasks" python3 -c '
348
+ import json,os
349
+ try:
350
+ o=json.loads(os.environ.get("RALPH_TASKS",""))
351
+ except Exception:
352
+ raise SystemExit(0)
353
+ ts=o.get("tasks") or []
354
+ print("1" if any(t.get("status")!="done" for t in ts) else "",end="")')"
355
+ fi
356
+ [ -z "$remaining" ] && all_done=true
357
+ fi
358
+
359
+ local current_json="null" tasks_json="null" last_json="null"
360
+ [ -n "$current" ] && current_json="\"$(ralph_json_escape "$current")\""
361
+ [ -n "$tasks" ] && tasks_json="$tasks"
362
+ [ -n "$last_trace" ] && last_json="$last_trace"
363
+
364
+ printf '{"current_task":%s,"tasks":%s,"last_trace":%s,"all_done":%s,"retry":{"retries":%s,"max":%s}}' \
365
+ "$current_json" "$tasks_json" "$last_json" "$all_done" "$retries" "$max"
366
+ }
@@ -0,0 +1,179 @@
1
+ ---
2
+ name: brainstorm
3
+ description: Manual-only brainstorming with a live browser mind map - explores requirements and design one question at a time while pushing the discussion structure to a draggable, zoomable mind map. ONLY invoke when the user explicitly runs /superharness:brainstorm; never self-invoke.
4
+ disable-model-invocation: true
5
+ argument-hint: [topic]
6
+ ---
7
+
8
+ # Superharness Brainstorm — live mind-map requirement design
9
+
10
+ **Topic:** $ARGUMENTS
11
+
12
+ If the topic above is empty, ask your human partner what they want to brainstorm and stop.
13
+
14
+ **Announce at start:** "Superharness brainstorm engaged. Topic: <topic>."
15
+
16
+ Turn the idea into a validated design through collaborative dialogue, while mirroring
17
+ the discussion structure to a live mind map in the user's browser.
18
+
19
+ **State root:** the superharness state root follows the host — `.claude/superharness/`
20
+ under Claude Code, `.flavor/superharness/` under flavor-code. Everywhere below,
21
+ `.claude/superharness/` stands for whichever state root applies to your host
22
+ (the scripts detect it automatically; use the actual paths they print).
23
+
24
+ <HARD-GATE>
25
+ Do NOT write implementation code or invoke implementation skills during this flow.
26
+ The output of this skill is a design document, not code.
27
+ </HARD-GATE>
28
+
29
+ ## Phase 1 — Start the mind map session
30
+
31
+ 1. Run (the script backgrounds node itself and prints server-info JSON before exiting):
32
+
33
+ ```
34
+ powershell -NoProfile -ExecutionPolicy Bypass -File "<this skill's base directory>/scripts/start-server.ps1" -ProjectDir "<project root>"
35
+ ```
36
+
37
+ 2. Parse the printed JSON: save `url`, `content_dir`, `state_dir`, and the session
38
+ directory (parent of `state_dir`). Tell the user to open `url` in a browser.
39
+ 3. Remind the user to add the brainstorm root — the parent of the session directory
40
+ the script printed (`<state-root>/superharness/brainstorm/`) — to `.gitignore` if missing.
41
+ 4. **Degrade gracefully:** if node is missing or the script fails, say so and continue
42
+ the whole flow in the terminal only. Never block brainstorming on the mind map.
43
+
44
+ ## Phase 2 — Explore context
45
+
46
+ Read relevant project files, docs, and recent commits. Push the first snapshot:
47
+ the root node is the topic. Then proceed.
48
+
49
+ ## Phase 3 — Clarify, one question at a time
50
+
51
+ For each clarifying question:
52
+
53
+ 1. **Before asking in the terminal**, push a snapshot adding the question node
54
+ (`kind: "question"`, `state: "open"`) with its candidate options
55
+ (`kind: "option"`, `state: "open"`) as children.
56
+ 2. Ask in the terminal (multiple choice preferred). Mention that the user can also
57
+ click an option node in the browser.
58
+ 3. **After the user answers** (terminal text is primary): read `<state_dir>/events`
59
+ if it exists and merge with the terminal answer. Push a snapshot marking the
60
+ chosen option `state: "chosen"`, the others `state: "rejected"`, and the question
61
+ `state: "resolved"`.
62
+
63
+ ## Phase 4 — Propose approaches
64
+
65
+ Push 2-3 approaches as branches (a `kind: "decision"` parent with `kind: "option"`
66
+ children, trade-offs in `note`). Present them in the terminal with your
67
+ recommendation. Mark the chosen approach as in Phase 3. Set top-level
68
+ `status: "designing"`.
69
+
70
+ ## Phase 5 — Present the design
71
+
72
+ Present the design in sections in the terminal, validating each. Fix agreed points
73
+ into the map as `kind: "requirement"` / `kind: "decision"` nodes; record known risks
74
+ as `kind: "risk"`.
75
+
76
+ ## Phase 6 — Wrap up
77
+
78
+ After the user approves the design:
79
+
80
+ 1. Push a final snapshot with `status: "approved"`.
81
+ 2. Write the design to `<state-root>/superharness/specs/YYYY-MM-DD-<topic-slug>.md`
82
+ (`.claude/superharness/` under Claude Code, `.flavor/superharness/` under
83
+ flavor-code; create the folder if missing) and commit it.
84
+ 3. Stop the server:
85
+
86
+ ```
87
+ powershell -NoProfile -ExecutionPolicy Bypass -File "<this skill's base directory>/scripts/stop-server.ps1" -SessionDir "<session directory>"
88
+ ```
89
+
90
+ 4. Tell the user: the design is saved, and they can run
91
+ `/superharness:go <goal>` to implement it. Do NOT start implementation yourself.
92
+
93
+ ## Message protocol
94
+
95
+ ### Claude → browser: write the full snapshot to `<content_dir>/mindmap.json`
96
+
97
+ Always rewrite the whole file with the Write tool. The server watches it and pushes
98
+ it to the browser over WebSocket. Before each write, check that
99
+ `<state_dir>/server-info` exists and `<state_dir>/server-stopped` does not;
100
+ otherwise restart the server (Phase 1) or continue terminal-only.
101
+
102
+ ```json
103
+ {
104
+ "type": "mindmap:snapshot",
105
+ "rev": 7,
106
+ "topic": "User Login",
107
+ "status": "exploring",
108
+ "root": {
109
+ "id": "root", "label": "User Login", "kind": "topic",
110
+ "children": [
111
+ { "id": "q1", "label": "Auth method?", "kind": "question", "state": "resolved",
112
+ "children": [
113
+ { "id": "q1-a", "label": "JWT", "kind": "option", "state": "chosen", "note": "Stateless, easy to scale" },
114
+ { "id": "q1-b", "label": "Session", "kind": "option", "state": "rejected" }
115
+ ] }
116
+ ]
117
+ }
118
+ }
119
+ ```
120
+
121
+ Rules:
122
+ - `rev`: increment by 1 on every write (the browser discards stale revisions).
123
+ - `status`: `exploring` → `designing` → `approved`.
124
+ - Node `id`s are stable across snapshots; never reuse an id for a different node.
125
+ - `kind`: `topic | question | option | decision | requirement | risk | note`.
126
+ - `state`: `open | chosen | rejected | resolved` (default `open`).
127
+ - `note`: optional hover tooltip text. Keep labels short; details go in `note`.
128
+
129
+ ### Browser → Claude: read `<state_dir>/events` (JSONL)
130
+
131
+ The server clears this file each time you push a new snapshot, so pending lines
132
+ always refer to the current screen. Missing file = no browser interaction.
133
+
134
+ ```json
135
+ {"type":"node:click","id":"q1-a","label":"JWT","kind":"option","timestamp":1760000000}
136
+ ```
137
+
138
+ The last click is usually the user's choice, but the terminal answer always wins
139
+ on conflict.
140
+
141
+ ### Browser → Claude: read `<state_dir>/edits` (JSONL)
142
+
143
+ Node `label`/`note` edits and the submit marker land here. Unlike `events`, this file
144
+ is **NOT cleared on snapshot push** — it persists until you merge and clear it.
145
+
146
+ ```json
147
+ {"type":"node:edit","id":"q1-a","label":"New label","note":"New note","timestamp":1760000000}
148
+ {"type":"submit","timestamp":1760000005}
149
+ ```
150
+
151
+ Only `label` and `note` are editable. Same `id` later in the file wins.
152
+
153
+ ### Edit round — pull browser edits into the design
154
+
155
+ When you invite the user to edit node text:
156
+
157
+ 1. **Establish the baseline first.** Clear `<state_dir>/edits` (truncate it) so a stale
158
+ submit from an earlier round can't immediately satisfy the wait. Do this BEFORE
159
+ inviting the user, so the window where an eager submit lands un-watched is closed.
160
+ 2. Tell them: double-click a node in the browser to edit its label/note, save each one, then click "Submit" in the top bar when done.
161
+ 3. Do NOT end the turn. Block-wait for a `{"type":"submit"}` line in
162
+ `<state_dir>/edits` using `Monitor` (fall back to `ScheduleWakeup`, ≤60s, if
163
+ `Monitor` is unavailable). This only works while you are parked in this wait.
164
+ 4. On submit: read `<state_dir>/edits`, take all `node:edit` lines (same `id` later
165
+ wins), apply each `label`/`note` onto the current snapshot tree by `id`; ignore
166
+ ids no longer present.
167
+ 5. If a browser edit conflicts with what the terminal dialogue concluded for that
168
+ node, ask in the terminal which wins.
169
+ 6. Rewrite `<content_dir>/mindmap.json` (`rev` + 1), then clear `<state_dir>/edits`.
170
+
171
+ ## Red Flags
172
+
173
+ | Thought | Reality |
174
+ |---------|---------|
175
+ | "The server won't start, fix it first" | Degrade to terminal-only and keep going; brainstorming must not be blocked by the mind map. |
176
+ | "Asking three questions at once is faster" | One question at a time. |
177
+ | "The design is approved, start coding right away" | The endpoint is a design document + a prompt to run /superharness:go. |
178
+ | "Snapshots push too often, batch them" | Push every question/decision; real-time-ness is this skill's value. |
179
+ | "Take browser edits as final" | label/note edits wait for "Submit" and are confirmed in the terminal when they conflict with the dialogue. |
@@ -0,0 +1,76 @@
1
+ // Mind map tree layout. Pure and deterministic.
2
+ // Loadable from Node (module.exports) and the browser (window.MindmapLayout).
3
+ (function (root, factory) {
4
+ if (typeof module === 'object' && module.exports) module.exports = factory();
5
+ else root.MindmapLayout = factory();
6
+ })(typeof self !== 'undefined' ? self : this, function () {
7
+ const LEVEL_X = 220; // horizontal distance per depth level
8
+ const NODE_H = 44; // vertical slot per leaf
9
+
10
+ function leafCount(node) {
11
+ if (!node.children || node.children.length === 0) return 1;
12
+ return node.children.reduce((sum, c) => sum + leafCount(c), 0);
13
+ }
14
+
15
+ // Distribute root children left/right, balancing total leaf count.
16
+ function splitSides(children) {
17
+ const right = [];
18
+ const left = [];
19
+ let rightLeaves = 0;
20
+ let leftLeaves = 0;
21
+ for (const c of children) {
22
+ const n = leafCount(c);
23
+ if (rightLeaves <= leftLeaves) { right.push(c); rightLeaves += n; }
24
+ else { left.push(c); leftLeaves += n; }
25
+ }
26
+ return { right, left };
27
+ }
28
+
29
+ function visit(node, depth, side, top, parentId, nodes, links) {
30
+ const leaves = leafCount(node);
31
+ nodes.push({
32
+ id: node.id,
33
+ label: node.label,
34
+ kind: node.kind || 'note',
35
+ state: node.state || 'open',
36
+ note: node.note || '',
37
+ x: side * depth * LEVEL_X,
38
+ y: top + (leaves * NODE_H) / 2,
39
+ side,
40
+ });
41
+ links.push({ from: parentId, to: node.id });
42
+ let childTop = top;
43
+ for (const c of node.children || []) {
44
+ visit(c, depth + 1, side, childTop, node.id, nodes, links);
45
+ childTop += leafCount(c) * NODE_H;
46
+ }
47
+ }
48
+
49
+ function layout(rootNode) {
50
+ const nodes = [];
51
+ const links = [];
52
+ if (!rootNode) return { nodes, links };
53
+ nodes.push({
54
+ id: rootNode.id,
55
+ label: rootNode.label,
56
+ kind: rootNode.kind || 'topic',
57
+ state: rootNode.state || 'open',
58
+ note: rootNode.note || '',
59
+ x: 0,
60
+ y: 0,
61
+ side: 0,
62
+ });
63
+ const { right, left } = splitSides(rootNode.children || []);
64
+ for (const [side, group] of [[1, right], [-1, left]]) {
65
+ const total = group.reduce((sum, c) => sum + leafCount(c), 0);
66
+ let top = -(total * NODE_H) / 2;
67
+ for (const c of group) {
68
+ visit(c, 1, side, top, rootNode.id, nodes, links);
69
+ top += leafCount(c) * NODE_H;
70
+ }
71
+ }
72
+ return { nodes, links };
73
+ }
74
+
75
+ return { layout, leafCount, splitSides };
76
+ });