@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.
- package/HARNESS.md +56 -0
- package/flavor-plugin.json +25 -0
- package/index.js +238 -0
- package/package.json +23 -0
- package/scripts/ralph-lib.ps1 +297 -0
- package/scripts/ralph-lib.sh +366 -0
- package/skills/brainstorm/SKILL.md +179 -0
- package/skills/brainstorm/scripts/layout.js +76 -0
- package/skills/brainstorm/scripts/mindmap.html +249 -0
- package/skills/brainstorm/scripts/server.cjs +208 -0
- package/skills/brainstorm/scripts/start-server.ps1 +57 -0
- package/skills/brainstorm/scripts/stop-server.ps1 +17 -0
- package/skills/finishing-a-development-branch/SKILL.md +112 -0
- package/skills/go/SKILL.md +169 -0
- package/skills/light/SKILL.md +85 -0
- package/skills/requesting-code-review/SKILL.md +103 -0
- package/skills/requesting-code-review/code-reviewer.md +168 -0
- package/skills/subagent-driven-development/SKILL.md +125 -0
- package/skills/systematic-debugging/SKILL.md +296 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/test-driven-development/SKILL.md +371 -0
- package/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/skills/using-git-worktrees/SKILL.md +91 -0
- package/skills/verification-before-completion/SKILL.md +139 -0
- 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
|
+
});
|