@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
package/HARNESS.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Superharness Bootstrap
|
|
2
|
+
|
|
3
|
+
You have superharness: a project-level engineering discipline harness. It is loaded
|
|
4
|
+
as a plugin of your host — from the local marketplace at `.claude/superharness`
|
|
5
|
+
under Claude Code, or as a native plugin under `.flavor/plugins/superharness`
|
|
6
|
+
under flavor-code — and this document is injected at session start by its
|
|
7
|
+
SessionStart hook. The state root follows the host: `.claude/superharness/`
|
|
8
|
+
under Claude Code, `.flavor/superharness/` under flavor-code.
|
|
9
|
+
|
|
10
|
+
## The Rule
|
|
11
|
+
|
|
12
|
+
**Before responding to ANY engineering task — including "simple" ones — check whether a
|
|
13
|
+
superharness skill applies. If there is even a 1% chance a skill applies, invoke it with
|
|
14
|
+
the Skill tool before doing anything else.**
|
|
15
|
+
|
|
16
|
+
## Available Skills
|
|
17
|
+
|
|
18
|
+
| Skill | Invoke when |
|
|
19
|
+
|-------|-------------|
|
|
20
|
+
| `superharness:go` | The user gives a task goal to complete end-to-end (also triggered by `/superharness:go <goal>`) |
|
|
21
|
+
| `superharness:brainstorm` | ONLY when the user explicitly runs `/superharness:brainstorm <topic>` — never self-invoke. Requirements/design dialogue with a live browser mind map |
|
|
22
|
+
| `superharness:light` | Small, focused tasks that need discipline without the full go machinery — quick fixes, small features, config/docs tweaks, prototypes. Lighter go: TDD with explicit exemptions, real-output verification, root-cause debugging; no worktree, no plan file, no ralph tracking |
|
|
23
|
+
| `superharness:writing-plans` | A multi-step task needs an implementation plan, before touching code |
|
|
24
|
+
| `superharness:using-git-worktrees` | Starting feature work that needs an isolated workspace, before implementation (go Phase 0.5) |
|
|
25
|
+
| `superharness:finishing-a-development-branch` | Worktree/branch work is done, verified, and reviewed — merge it back, remove the worktree, delete the branch (go Phase 5) |
|
|
26
|
+
| `superharness:subagent-driven-development` | Executing a multi-task plan with independent tasks in the current session (go Phase 2) |
|
|
27
|
+
| `superharness:test-driven-development` | Implementing ANY feature or bugfix, before writing implementation code |
|
|
28
|
+
| `superharness:systematic-debugging` | Any bug, test failure, or unexpected behavior, before proposing fixes |
|
|
29
|
+
| `superharness:requesting-code-review` | A task or major feature is complete, before moving on or merging |
|
|
30
|
+
| `superharness:verification-before-completion` | About to claim work is complete, fixed, or passing |
|
|
31
|
+
|
|
32
|
+
## Non-Negotiable Constraints
|
|
33
|
+
|
|
34
|
+
1. **TDD always.** Write the failing test first, watch it fail (RED), write minimal code,
|
|
35
|
+
watch it pass (GREEN), refactor, commit. Implementation code written before its test
|
|
36
|
+
gets deleted and redone. See `superharness:test-driven-development`.
|
|
37
|
+
2. **Evidence before claims.** Never say "done", "fixed", or "passing" without running the
|
|
38
|
+
verification command in this session and seeing the output. See
|
|
39
|
+
`superharness:verification-before-completion`.
|
|
40
|
+
3. **Root cause over quick fix.** When something breaks, follow the 4-phase process in
|
|
41
|
+
`superharness:systematic-debugging`. No "let me just try this" patches.
|
|
42
|
+
4. **Plan before multi-step work.** Tasks with 3+ steps get a written plan
|
|
43
|
+
(`superharness:writing-plans`) saved to `<state-root>/superharness/plans/` in the
|
|
44
|
+
project, with bite-sized 2–5 minute TDD steps.
|
|
45
|
+
5. **Review before declaring victory.** Significant changes get a code review pass
|
|
46
|
+
(`superharness:requesting-code-review`); Critical issues block progress.
|
|
47
|
+
|
|
48
|
+
## Red Flags — STOP if you catch yourself thinking:
|
|
49
|
+
|
|
50
|
+
| Thought | Reality |
|
|
51
|
+
|---------|---------|
|
|
52
|
+
| "This is too simple for TDD" | Simple things break too. Test first. |
|
|
53
|
+
| "I'll write tests after" | That's not TDD. RED comes first. |
|
|
54
|
+
| "It should work now" | Run it. Show the output. Then say it works. |
|
|
55
|
+
| "Let me just try changing this" | That's guessing, not debugging. Find the root cause. |
|
|
56
|
+
| "The plan is in my head" | Write it down. Plans in heads evaporate. |
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "superharness",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"apiVersion": "1",
|
|
5
|
+
"main": "./index.js",
|
|
6
|
+
"permissions": [],
|
|
7
|
+
"contributes": {
|
|
8
|
+
"commands": [],
|
|
9
|
+
"tools": [],
|
|
10
|
+
"hooks": [
|
|
11
|
+
{ "name": "SessionStart" },
|
|
12
|
+
{ "name": "UserPromptSubmit" },
|
|
13
|
+
{ "name": "Stop" },
|
|
14
|
+
{ "name": "SessionEnd" },
|
|
15
|
+
{ "name": "BeforePlan" },
|
|
16
|
+
{ "name": "AfterPlan" },
|
|
17
|
+
{ "name": "SubagentStart" },
|
|
18
|
+
{ "name": "SubagentStop" }
|
|
19
|
+
],
|
|
20
|
+
"skillRoots": [
|
|
21
|
+
{ "name": "superharness", "path": "./skills" }
|
|
22
|
+
],
|
|
23
|
+
"modelAdapters": []
|
|
24
|
+
}
|
|
25
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// superharness flavor-code plugin
|
|
2
|
+
// Registers the superharness skill root so that go, brainstorm, tdd, etc.
|
|
3
|
+
// are discovered under the /superharness namespace, and registers three
|
|
4
|
+
// lifecycle hooks mirroring the Claude Code hooks:
|
|
5
|
+
// SessionStart inject HARNESS.md (+ STACK.md when present) as additionalContext
|
|
6
|
+
// UserPromptSubmit auto-bootstrap ralph tracking on `/superharness:go <goal>`
|
|
7
|
+
// Stop append a 'round' heartbeat to trace.jsonl while a go task runs
|
|
8
|
+
// All hooks are best-effort and always return { decision: "allow" }.
|
|
9
|
+
|
|
10
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const PLUGIN_ROOT = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const ALLOW = Object.freeze({ decision: "allow" });
|
|
16
|
+
|
|
17
|
+
// The workspace path is cached from SessionStart (payload.workspace is a string);
|
|
18
|
+
// hooks fall back to the process working directory when it is unavailable.
|
|
19
|
+
let workspaceRoot;
|
|
20
|
+
|
|
21
|
+
function projectRoot(event) {
|
|
22
|
+
const workspace = event?.payload?.workspace;
|
|
23
|
+
if (typeof workspace === "string" && workspace.length > 0) return workspace;
|
|
24
|
+
if (typeof workspaceRoot === "string" && workspaceRoot.length > 0) return workspaceRoot;
|
|
25
|
+
return process.cwd();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ralphDir(root) {
|
|
29
|
+
// flavor-code host: runtime state lives under the .flavor state root
|
|
30
|
+
// (the Claude Code counterpart uses .claude/superharness/ralph).
|
|
31
|
+
return join(root, ".flavor", "superharness", "ralph");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isoNow() {
|
|
35
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "+00:00");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function atomicWrite(path, text) {
|
|
39
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
40
|
+
const tmp = `${path}.tmp`;
|
|
41
|
+
writeFileSync(tmp, text, "utf8");
|
|
42
|
+
renameSync(tmp, path);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readJson(path) {
|
|
46
|
+
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return undefined; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readText(path) {
|
|
50
|
+
try {
|
|
51
|
+
const text = readFileSync(path, "utf8");
|
|
52
|
+
return text.length > 0 ? text : undefined;
|
|
53
|
+
} catch { return undefined; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function currentTaskPath(root) {
|
|
57
|
+
return join(ralphDir(root), ".current-task");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getCurrentTask(root) {
|
|
61
|
+
const raw = readText(currentTaskPath(root));
|
|
62
|
+
const line = raw === undefined ? "" : raw.trim();
|
|
63
|
+
return line.length > 0 ? line : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function appendTrace(root, phase, traceEvent, detail) {
|
|
67
|
+
const line = JSON.stringify({ ts: isoNow(), phase, event: traceEvent, detail: detail ?? "" });
|
|
68
|
+
const path = join(ralphDir(root), "trace.jsonl");
|
|
69
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
70
|
+
const existing = readText(path) ?? "";
|
|
71
|
+
writeFileSync(path, `${existing}${line}\n`, "utf8");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Parse a go invocation at the start of the prompt. flavor-code invokes skills
|
|
75
|
+
// as `/go <goal>`; the Claude-style `superharness:go <goal>` is also accepted.
|
|
76
|
+
// Returns { goal, slug } or undefined.
|
|
77
|
+
function goInvocation(prompt) {
|
|
78
|
+
if (typeof prompt !== "string") return undefined;
|
|
79
|
+
const match = /^\s*(?:\/go|superharness:go)(?:[^A-Za-z0-9_]|$)/.exec(prompt);
|
|
80
|
+
if (match === null) return undefined;
|
|
81
|
+
const goal = prompt.replace(/^\s*(?:\/go|superharness:go)[ \t]*/, "").trim();
|
|
82
|
+
const now = new Date();
|
|
83
|
+
const date = now.toISOString().slice(0, 10);
|
|
84
|
+
const tokens = (goal.toLowerCase().match(/[a-z0-9]+/g) ?? []).slice(0, 6);
|
|
85
|
+
const kebab = tokens.length > 0
|
|
86
|
+
? tokens.join("-")
|
|
87
|
+
: `task-${String(now.getUTCHours()).padStart(2, "0")}${String(now.getUTCMinutes()).padStart(2, "0")}${String(now.getUTCSeconds()).padStart(2, "0")}`;
|
|
88
|
+
return { goal, slug: `${date}-${kebab}` };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Bootstrap a fresh go task: point .current-task, seed an empty task.json
|
|
92
|
+
// (planning/plan), open the trace ledger with task:started, reset retry state.
|
|
93
|
+
function startTask(root, taskId, goal) {
|
|
94
|
+
atomicWrite(currentTaskPath(root), taskId.trim());
|
|
95
|
+
atomicWrite(join(ralphDir(root), "task.json"), JSON.stringify({
|
|
96
|
+
status: "planning",
|
|
97
|
+
phase: "plan",
|
|
98
|
+
sprint: { current: 0, total: 0 },
|
|
99
|
+
tasks: [],
|
|
100
|
+
updated_at: isoNow(),
|
|
101
|
+
}));
|
|
102
|
+
appendTrace(root, "plan", "task:started", goal);
|
|
103
|
+
atomicWrite(join(ralphDir(root), ".ralph-state.json"), JSON.stringify({
|
|
104
|
+
retries: 0, max: 5, updated_at: isoNow(),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function onSessionStart(event) {
|
|
109
|
+
const workspace = event?.payload?.workspace;
|
|
110
|
+
if (typeof workspace === "string" && workspace.length > 0) workspaceRoot = workspace;
|
|
111
|
+
const harness = readText(join(PLUGIN_ROOT, "HARNESS.md"));
|
|
112
|
+
if (harness === undefined) return ALLOW;
|
|
113
|
+
let context = `<EXTREMELY_IMPORTANT>\nYou have superharness. Follow it for all engineering work in this project.\n\n${harness}\n</EXTREMELY_IMPORTANT>`;
|
|
114
|
+
const stack = readText(join(PLUGIN_ROOT, "STACK.md"));
|
|
115
|
+
if (stack !== undefined) {
|
|
116
|
+
context += `\n\n<EXTREMELY_IMPORTANT>\nThis project targets a specific tech stack. Follow this guidance.\n\n${stack}\n</EXTREMELY_IMPORTANT>`;
|
|
117
|
+
}
|
|
118
|
+
return { decision: "allow", additionalContext: context };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function onUserPromptSubmit(event) {
|
|
122
|
+
const root = projectRoot(event);
|
|
123
|
+
const prompt = typeof event?.payload?.prompt === "string" ? event.payload.prompt : "";
|
|
124
|
+
|
|
125
|
+
// 1. Auto-trigger on a go invocation (start/repoint a task automatically).
|
|
126
|
+
const invocation = goInvocation(prompt);
|
|
127
|
+
if (invocation !== undefined && invocation.slug !== getCurrentTask(root)) {
|
|
128
|
+
startTask(root, invocation.slug, invocation.goal);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 2. Stash the pending round so the Stop hook can record a heartbeat.
|
|
132
|
+
atomicWrite(join(ralphDir(root), ".pending-prompt.json"), JSON.stringify({ ts: isoNow(), query: prompt }));
|
|
133
|
+
return ALLOW;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function onStop(event) {
|
|
137
|
+
const root = projectRoot(event);
|
|
138
|
+
const pendingPath = join(ralphDir(root), ".pending-prompt.json");
|
|
139
|
+
const current = getCurrentTask(root);
|
|
140
|
+
if (current === undefined) {
|
|
141
|
+
// Not tracking a go task — drop any stray pending prompt and bail.
|
|
142
|
+
try { rmSync(pendingPath, { force: true }); } catch { /* ignore */ }
|
|
143
|
+
return ALLOW;
|
|
144
|
+
}
|
|
145
|
+
const pending = readJson(pendingPath);
|
|
146
|
+
const query = typeof pending?.query === "string" ? pending.query : "";
|
|
147
|
+
const tasks = readJson(join(ralphDir(root), "task.json"));
|
|
148
|
+
const phase = typeof tasks?.phase === "string" && tasks.phase.length > 0 ? tasks.phase : "go";
|
|
149
|
+
appendTrace(root, phase, "round", query);
|
|
150
|
+
try { rmSync(pendingPath, { force: true }); } catch { /* ignore */ }
|
|
151
|
+
return ALLOW;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Called when the session ends (host fires SessionEnd). Records a final
|
|
155
|
+
// trace event and clears any remaining ralph state so the next session
|
|
156
|
+
// starts with a clean slate.
|
|
157
|
+
function onSessionEnd(event) {
|
|
158
|
+
const root = projectRoot(event);
|
|
159
|
+
const current = getCurrentTask(root);
|
|
160
|
+
if (current === undefined) return ALLOW;
|
|
161
|
+
const tasks = readJson(join(ralphDir(root), "task.json"));
|
|
162
|
+
const phase = typeof tasks?.phase === "string" && tasks.phase.length > 0 ? tasks.phase : "go";
|
|
163
|
+
appendTrace(root, phase, "session:end", "session ended");
|
|
164
|
+
// Clear the active-task pointer so the Stop hook no longer records heartbeats.
|
|
165
|
+
try { rmSync(currentTaskPath(root), { force: true }); } catch { /* ignore */ }
|
|
166
|
+
return ALLOW;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Called before the planning phase begins. Injects a lightweight context
|
|
170
|
+
// flagging that plan mode is about to start so the model can prepare.
|
|
171
|
+
function onBeforePlan(event) {
|
|
172
|
+
const root = projectRoot(event);
|
|
173
|
+
const current = getCurrentTask(root);
|
|
174
|
+
if (current === undefined) return ALLOW;
|
|
175
|
+
appendTrace(root, "plan", "plan:before", "planning phase starting");
|
|
176
|
+
return ALLOW;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Called after the planning phase ends. Records the plan result and
|
|
180
|
+
// advances the ralph task status so the next phase is clear.
|
|
181
|
+
function onAfterPlan(event) {
|
|
182
|
+
const root = projectRoot(event);
|
|
183
|
+
const current = getCurrentTask(root);
|
|
184
|
+
if (current === undefined) return ALLOW;
|
|
185
|
+
const tasks = readJson(join(ralphDir(root), "task.json"));
|
|
186
|
+
const phase = typeof tasks?.phase === "string" && tasks.phase.length > 0 ? tasks.phase : "go";
|
|
187
|
+
appendTrace(root, phase, "plan:after", "planning phase complete");
|
|
188
|
+
return ALLOW;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Called when a subagent is about to start. Records the subagent
|
|
192
|
+
// launch so the trace has a clear subagent lifecycle boundary.
|
|
193
|
+
function onSubagentStart(event) {
|
|
194
|
+
const root = projectRoot(event);
|
|
195
|
+
const current = getCurrentTask(root);
|
|
196
|
+
if (current === undefined) return ALLOW;
|
|
197
|
+
const tasks = readJson(join(ralphDir(root), "task.json"));
|
|
198
|
+
const phase = typeof tasks?.phase === "string" && tasks.phase.length > 0 ? tasks.phase : "go";
|
|
199
|
+
appendTrace(root, phase, "subagent:start", "subagent starting");
|
|
200
|
+
return ALLOW;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Called when a subagent finishes. Records the subagent completion
|
|
204
|
+
// and any errors so trace.jsonl has a complete subagent picture.
|
|
205
|
+
function onSubagentStop(event) {
|
|
206
|
+
const root = projectRoot(event);
|
|
207
|
+
const current = getCurrentTask(root);
|
|
208
|
+
if (current === undefined) return ALLOW;
|
|
209
|
+
const tasks = readJson(join(ralphDir(root), "task.json"));
|
|
210
|
+
const phase = typeof tasks?.phase === "string" && tasks.phase.length > 0 ? tasks.phase : "go";
|
|
211
|
+
const outcome = typeof event?.payload?.outcome === "string" ? event.payload.outcome : "completed";
|
|
212
|
+
appendTrace(root, phase, "subagent:stop", `subagent ${outcome}`);
|
|
213
|
+
return ALLOW;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function activate(context) {
|
|
217
|
+
context.registerSkillRoot("superharness", "./skills");
|
|
218
|
+
|
|
219
|
+
const guard = (fn) => (event, signal) => {
|
|
220
|
+
try { return fn(event, signal); } catch { return ALLOW; }
|
|
221
|
+
};
|
|
222
|
+
const options = { failurePolicy: "allow" };
|
|
223
|
+
|
|
224
|
+
const disposers = [
|
|
225
|
+
context.registerHook("SessionStart", guard(onSessionStart), options),
|
|
226
|
+
context.registerHook("UserPromptSubmit", guard(onUserPromptSubmit), options),
|
|
227
|
+
context.registerHook("Stop", guard(onStop), options),
|
|
228
|
+
context.registerHook("SessionEnd", guard(onSessionEnd), options),
|
|
229
|
+
context.registerHook("BeforePlan", guard(onBeforePlan), options),
|
|
230
|
+
context.registerHook("AfterPlan", guard(onAfterPlan), options),
|
|
231
|
+
context.registerHook("SubagentStart", guard(onSubagentStart), options),
|
|
232
|
+
context.registerHook("SubagentStop", guard(onSubagentStop), options),
|
|
233
|
+
];
|
|
234
|
+
return () => { for (const dispose of disposers.reverse()) dispose(); };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Exported for tests.
|
|
238
|
+
export const __test = { goInvocation, onSessionStart, onUserPromptSubmit, onStop, onSessionEnd, onBeforePlan, onAfterPlan, onSubagentStart, onSubagentStop, ralphDir };
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flavor-code/superharness",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "为 flavor-code 注入规划、调试、测试与交付工作流的工程增强插件。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"**/*"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"flavor-code",
|
|
13
|
+
"flavor-plugin",
|
|
14
|
+
"workflow",
|
|
15
|
+
"skills",
|
|
16
|
+
"hooks"
|
|
17
|
+
],
|
|
18
|
+
"author": "Flavor Code",
|
|
19
|
+
"repository": "C:\\Users\\wangzh\\Desktop\\idea\\superharness",
|
|
20
|
+
"flavorPlugin": {
|
|
21
|
+
"apiVersion": "1"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# Ralph state mechanism — zero-dependency PowerShell state library.
|
|
2
|
+
#
|
|
3
|
+
# Manages the four runtime files of a resumable autonomous-task loop, all under
|
|
4
|
+
# <project>/<state-root>/superharness/ralph/ where <state-root> follows the host:
|
|
5
|
+
# .claude (Claude Code install: .claude/superharness marketplace)
|
|
6
|
+
# .flavor (flavor-code install: .flavor/plugins/superharness)
|
|
7
|
+
# .current-task one-line pointer to the active task (switch = rewrite the line)
|
|
8
|
+
# task.json task-list snapshot {status,phase,sprint,tasks[],updated_at}
|
|
9
|
+
# trace.jsonl append-only ledger, one {ts,phase,event,detail} JSON per line
|
|
10
|
+
# .ralph-state.json retry counter {retries,max,updated_at}, capped at 5
|
|
11
|
+
#
|
|
12
|
+
# Dot-source this file to use the functions. The trace hooks (hooks/stop.ps1,
|
|
13
|
+
# hooks/user-prompt-submit.ps1) dot-source it for go task tracking. Conventions:
|
|
14
|
+
# UTF-8 without BOM, atomic temp-then-move for JSON snapshots, ISO-8601 timestamps.
|
|
15
|
+
|
|
16
|
+
# ---------------------------------------------------------------- paths & helpers
|
|
17
|
+
|
|
18
|
+
function Get-RalphStateRoot {
|
|
19
|
+
# Host detection: this library ships inside the host install, so its own path
|
|
20
|
+
# names the state root. $env:SUPERHARNESS_STATE_ROOT ('.claude' | '.flavor')
|
|
21
|
+
# forces a choice for unusual layouts; Claude Code is the historical default.
|
|
22
|
+
$forced = $env:SUPERHARNESS_STATE_ROOT
|
|
23
|
+
if ($forced -eq '.flavor' -or $forced -eq 'flavor') { return '.flavor' }
|
|
24
|
+
if ($forced -eq '.claude' -or $forced -eq 'claude') { return '.claude' }
|
|
25
|
+
if ($PSScriptRoot -match '[/\\]\.flavor[/\\]plugins[/\\]superharness[/\\]scripts') { return '.flavor' }
|
|
26
|
+
return '.claude'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function Get-RalphDir {
|
|
30
|
+
param([Parameter(Mandatory)][string]$Root)
|
|
31
|
+
$stateRoot = Get-RalphStateRoot # '.claude' or '.flavor'
|
|
32
|
+
Join-Path $Root (Join-Path $stateRoot 'superharness\ralph')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Get-RalphIso { (Get-Date).ToString('yyyy-MM-ddTHH:mm:sszzz') }
|
|
36
|
+
|
|
37
|
+
function Get-RalphGoInvocation {
|
|
38
|
+
# Parse a UserPromptSubmit prompt. If it is a `/superharness:go <goal>` invocation
|
|
39
|
+
# (leading slash optional, must be at the start of the prompt), return
|
|
40
|
+
# { Goal; Slug='YYYY-MM-DD-<kebab|task-HHmmss>' }; otherwise return $null. Pure.
|
|
41
|
+
param(
|
|
42
|
+
[Parameter(Mandatory)][AllowEmptyString()][string]$Prompt,
|
|
43
|
+
[datetime]$Now = (Get-Date)
|
|
44
|
+
)
|
|
45
|
+
if ($null -eq $Prompt) { return $null }
|
|
46
|
+
$m = [regex]::Match($Prompt, '^\s*/?superharness:go\b[ \t]*(?<goal>[\s\S]*)$')
|
|
47
|
+
if (-not $m.Success) { return $null }
|
|
48
|
+
$goal = $m.Groups['goal'].Value.Trim()
|
|
49
|
+
$date = $Now.ToString('yyyy-MM-dd')
|
|
50
|
+
$tokens = [regex]::Matches($goal.ToLower(), '[a-z0-9]+') | ForEach-Object { $_.Value }
|
|
51
|
+
if ($tokens.Count -gt 0) {
|
|
52
|
+
$kebab = (@($tokens) | Select-Object -First 6) -join '-'
|
|
53
|
+
} else {
|
|
54
|
+
$kebab = 'task-' + $Now.ToString('HHmmss')
|
|
55
|
+
}
|
|
56
|
+
[PSCustomObject]@{ Goal = $goal; Slug = "$date-$kebab" }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function New-RalphDir {
|
|
60
|
+
param([string]$Root)
|
|
61
|
+
$dir = Get-RalphDir $Root
|
|
62
|
+
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force $dir | Out-Null }
|
|
63
|
+
return $dir
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function Read-RalphJson {
|
|
67
|
+
param([string]$Path)
|
|
68
|
+
if (-not (Test-Path $Path)) { return $null }
|
|
69
|
+
try { return Get-Content $Path -Raw | ConvertFrom-Json } catch { return $null }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function Write-RalphText {
|
|
73
|
+
# Atomic write: temp file then move-replace. UTF-8 without BOM.
|
|
74
|
+
param([string]$Path, [string]$Text)
|
|
75
|
+
$dir = Split-Path -Parent $Path
|
|
76
|
+
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force $dir | Out-Null }
|
|
77
|
+
$enc = New-Object System.Text.UTF8Encoding($false)
|
|
78
|
+
$tmp = "$Path.tmp"
|
|
79
|
+
[IO.File]::WriteAllText($tmp, $Text, $enc)
|
|
80
|
+
Move-Item -Path $tmp -Destination $Path -Force
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function Write-RalphJson {
|
|
84
|
+
param([string]$Path, $Object)
|
|
85
|
+
Write-RalphText $Path (ConvertTo-Json -InputObject $Object -Depth 12 -Compress)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------- .current-task
|
|
89
|
+
|
|
90
|
+
function Get-RalphCurrentTaskPath { param([string]$Root) Join-Path (Get-RalphDir $Root) '.current-task' }
|
|
91
|
+
|
|
92
|
+
function Set-RalphCurrentTask {
|
|
93
|
+
# The pointer is a single line; switching a task rewrites only this line.
|
|
94
|
+
param([Parameter(Mandatory)][string]$Root, [Parameter(Mandatory)][string]$TaskId)
|
|
95
|
+
Write-RalphText (Get-RalphCurrentTaskPath $Root) ($TaskId.Trim())
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function Get-RalphCurrentTask {
|
|
99
|
+
param([Parameter(Mandatory)][string]$Root)
|
|
100
|
+
$p = Get-RalphCurrentTaskPath $Root
|
|
101
|
+
if (-not (Test-Path $p)) { return $null }
|
|
102
|
+
$raw = (Get-Content $p -Raw)
|
|
103
|
+
if ($null -eq $raw) { return $null }
|
|
104
|
+
$line = $raw.Trim()
|
|
105
|
+
if ($line -eq '') { return $null }
|
|
106
|
+
return $line
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------- task.json
|
|
110
|
+
|
|
111
|
+
$script:RalphStatuses = @('pending', 'in_progress', 'done')
|
|
112
|
+
|
|
113
|
+
function Get-RalphTaskPath { param([string]$Root) Join-Path (Get-RalphDir $Root) 'task.json' }
|
|
114
|
+
|
|
115
|
+
function Initialize-RalphTasks {
|
|
116
|
+
# Write the task-list snapshot. Each task defaults to status 'pending'.
|
|
117
|
+
param(
|
|
118
|
+
[Parameter(Mandatory)][string]$Root,
|
|
119
|
+
[Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Tasks,
|
|
120
|
+
[string]$Status = 'planning',
|
|
121
|
+
[string]$Phase = 'implement',
|
|
122
|
+
[int]$SprintCurrent = 0,
|
|
123
|
+
[int]$SprintTotal = 0
|
|
124
|
+
)
|
|
125
|
+
$norm = @()
|
|
126
|
+
foreach ($t in $Tasks) {
|
|
127
|
+
$st = if ($t.status) { [string]$t.status } else { 'pending' }
|
|
128
|
+
if ($script:RalphStatuses -notcontains $st) { throw "Invalid task status '$st' (allowed: $($script:RalphStatuses -join ', '))" }
|
|
129
|
+
$norm += [ordered]@{ id = $t.id; name = [string]$t.name; status = $st }
|
|
130
|
+
}
|
|
131
|
+
$snapshot = [ordered]@{
|
|
132
|
+
status = $Status
|
|
133
|
+
phase = $Phase
|
|
134
|
+
sprint = [ordered]@{ current = $SprintCurrent; total = $SprintTotal }
|
|
135
|
+
tasks = $norm
|
|
136
|
+
updated_at = (Get-RalphIso)
|
|
137
|
+
}
|
|
138
|
+
Write-RalphJson (Get-RalphTaskPath $Root) $snapshot
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function Get-RalphTasks {
|
|
142
|
+
param([Parameter(Mandatory)][string]$Root)
|
|
143
|
+
Read-RalphJson (Get-RalphTaskPath $Root)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function Get-RalphNextTask {
|
|
147
|
+
# The first task whose status is not 'done' — the resume entry point.
|
|
148
|
+
param([Parameter(Mandatory)][string]$Root)
|
|
149
|
+
$snap = Get-RalphTasks $Root
|
|
150
|
+
if (-not $snap) { return $null }
|
|
151
|
+
foreach ($t in @($snap.tasks)) {
|
|
152
|
+
if ($t.status -ne 'done') { return $t }
|
|
153
|
+
}
|
|
154
|
+
return $null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function Set-RalphTaskStatus {
|
|
158
|
+
# Idempotently set one task's status and refresh updated_at. Order preserved.
|
|
159
|
+
param(
|
|
160
|
+
[Parameter(Mandatory)][string]$Root,
|
|
161
|
+
[Parameter(Mandatory)]$Id,
|
|
162
|
+
[Parameter(Mandatory)][string]$Status
|
|
163
|
+
)
|
|
164
|
+
if ($script:RalphStatuses -notcontains $Status) { throw "Invalid task status '$Status' (allowed: $($script:RalphStatuses -join ', '))" }
|
|
165
|
+
$snap = Get-RalphTasks $Root
|
|
166
|
+
if (-not $snap) { throw "No task.json under $(Get-RalphDir $Root)" }
|
|
167
|
+
$tasks = @()
|
|
168
|
+
foreach ($t in @($snap.tasks)) {
|
|
169
|
+
$st = if ("$($t.id)" -eq "$Id") { $Status } else { [string]$t.status }
|
|
170
|
+
$tasks += [ordered]@{ id = $t.id; name = [string]$t.name; status = $st }
|
|
171
|
+
}
|
|
172
|
+
$snapshot = [ordered]@{
|
|
173
|
+
status = [string]$snap.status
|
|
174
|
+
phase = [string]$snap.phase
|
|
175
|
+
sprint = [ordered]@{ current = $snap.sprint.current; total = $snap.sprint.total }
|
|
176
|
+
tasks = $tasks
|
|
177
|
+
updated_at = (Get-RalphIso)
|
|
178
|
+
}
|
|
179
|
+
Write-RalphJson (Get-RalphTaskPath $Root) $snapshot
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------- trace.jsonl
|
|
183
|
+
|
|
184
|
+
function Get-RalphTracePath { param([string]$Root) Join-Path (Get-RalphDir $Root) 'trace.jsonl' }
|
|
185
|
+
|
|
186
|
+
function Add-RalphTrace {
|
|
187
|
+
# Append a single minified {ts,phase,event,detail} line. Never rewrites earlier
|
|
188
|
+
# lines — the worst a crash can corrupt is the final line.
|
|
189
|
+
param(
|
|
190
|
+
[Parameter(Mandatory)][string]$Root,
|
|
191
|
+
[Parameter(Mandatory)][string]$Phase,
|
|
192
|
+
[Parameter(Mandatory)][string]$Event,
|
|
193
|
+
[string]$Detail = ''
|
|
194
|
+
)
|
|
195
|
+
New-RalphDir $Root | Out-Null
|
|
196
|
+
$line = ConvertTo-Json -InputObject ([ordered]@{
|
|
197
|
+
ts = (Get-RalphIso); phase = $Phase; event = $Event; detail = $Detail
|
|
198
|
+
}) -Depth 12 -Compress
|
|
199
|
+
$enc = New-Object System.Text.UTF8Encoding($false)
|
|
200
|
+
[IO.File]::AppendAllText((Get-RalphTracePath $Root), ($line + "`n"), $enc)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function Get-RalphTraceTail {
|
|
204
|
+
# Return the last N events (parsed), oldest-first. Empty array if no ledger.
|
|
205
|
+
param([Parameter(Mandatory)][string]$Root, [int]$Count = 1)
|
|
206
|
+
$p = Get-RalphTracePath $Root
|
|
207
|
+
if (-not (Test-Path $p)) { return @() }
|
|
208
|
+
$lines = @(Get-Content $p -Tail $Count | Where-Object { $_.Trim() -ne '' })
|
|
209
|
+
$out = @()
|
|
210
|
+
foreach ($l in $lines) { try { $out += ($l | ConvertFrom-Json) } catch {} }
|
|
211
|
+
return $out
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------- .ralph-state.json (retry counter)
|
|
215
|
+
|
|
216
|
+
function Get-RalphRetryPath { param([string]$Root) Join-Path (Get-RalphDir $Root) '.ralph-state.json' }
|
|
217
|
+
|
|
218
|
+
function Get-RalphRetryState {
|
|
219
|
+
# Defaults to {retries:0, max:5} when the file is absent or malformed.
|
|
220
|
+
param([Parameter(Mandatory)][string]$Root)
|
|
221
|
+
$s = Read-RalphJson (Get-RalphRetryPath $Root)
|
|
222
|
+
$retries = 0; $max = 5; $upd = $null
|
|
223
|
+
if ($s) {
|
|
224
|
+
if ($null -ne $s.retries) { $retries = [int]$s.retries }
|
|
225
|
+
if ($null -ne $s.max) { $max = [int]$s.max }
|
|
226
|
+
$upd = $s.updated_at
|
|
227
|
+
}
|
|
228
|
+
[PSCustomObject]@{ retries = $retries; max = $max; updated_at = $upd }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function Set-RalphRetryState {
|
|
232
|
+
param([Parameter(Mandatory)][string]$Root, [int]$Retries, [int]$Max)
|
|
233
|
+
$obj = [ordered]@{ retries = $Retries; max = $Max; updated_at = (Get-RalphIso) }
|
|
234
|
+
Write-RalphJson (Get-RalphRetryPath $Root) $obj
|
|
235
|
+
[PSCustomObject]@{ retries = $Retries; max = $Max; updated_at = $obj.updated_at }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function Add-RalphRetry {
|
|
239
|
+
# Increment the retry counter, clamped at max. Returns the new state.
|
|
240
|
+
param([Parameter(Mandatory)][string]$Root)
|
|
241
|
+
$st = Get-RalphRetryState $Root
|
|
242
|
+
$n = $st.retries + 1
|
|
243
|
+
if ($n -gt $st.max) { $n = $st.max }
|
|
244
|
+
Set-RalphRetryState -Root $Root -Retries $n -Max $st.max
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function Test-RalphRetryExhausted {
|
|
248
|
+
param([Parameter(Mandatory)][string]$Root)
|
|
249
|
+
$st = Get-RalphRetryState $Root
|
|
250
|
+
return ($st.retries -ge $st.max)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function Reset-RalphRetry {
|
|
254
|
+
param([Parameter(Mandatory)][string]$Root)
|
|
255
|
+
$st = Get-RalphRetryState $Root
|
|
256
|
+
Set-RalphRetryState -Root $Root -Retries 0 -Max $st.max
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------- task bootstrap
|
|
260
|
+
|
|
261
|
+
function Start-RalphTask {
|
|
262
|
+
# Auto-bootstrap a fresh go task: point .current-task, seed an empty task.json
|
|
263
|
+
# (planning/plan — the agent enriches the task list later), open the trace ledger
|
|
264
|
+
# with a task:started event, and reset the retry counter. Idempotent-ish: calling
|
|
265
|
+
# again repoints to a new TaskId and appends another task:started line.
|
|
266
|
+
param(
|
|
267
|
+
[Parameter(Mandatory)][string]$Root,
|
|
268
|
+
[Parameter(Mandatory)][string]$TaskId,
|
|
269
|
+
[string]$Goal = ''
|
|
270
|
+
)
|
|
271
|
+
Set-RalphCurrentTask -Root $Root -TaskId $TaskId
|
|
272
|
+
Initialize-RalphTasks -Root $Root -Tasks @() -Status 'planning' -Phase 'plan'
|
|
273
|
+
Add-RalphTrace -Root $Root -Phase 'plan' -Event 'task:started' -Detail $Goal
|
|
274
|
+
Reset-RalphRetry -Root $Root | Out-Null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# ---------------------------------------------------------------- cold-start recovery
|
|
278
|
+
|
|
279
|
+
function Get-RalphResumeContext {
|
|
280
|
+
# Assemble the deterministic file-based facts a freshly-started agent needs to
|
|
281
|
+
# resume: the active pointer, the task snapshot, the first not-done task, the
|
|
282
|
+
# last ledger event, and the retry state. The agent then reconciles these
|
|
283
|
+
# against `git diff` (code wins) and fixes task.json via Set-RalphTaskStatus.
|
|
284
|
+
param([Parameter(Mandatory)][string]$Root)
|
|
285
|
+
$snap = Get-RalphTasks $Root
|
|
286
|
+
$next = Get-RalphNextTask $Root
|
|
287
|
+
$tail = @(Get-RalphTraceTail -Root $Root -Count 1)
|
|
288
|
+
$last = if ($tail.Count -gt 0) { $tail[0] } else { $null }
|
|
289
|
+
[PSCustomObject]@{
|
|
290
|
+
current_task = (Get-RalphCurrentTask $Root)
|
|
291
|
+
tasks = $snap
|
|
292
|
+
next_task = $next
|
|
293
|
+
last_trace = $last
|
|
294
|
+
all_done = [bool]($snap -and ($null -eq $next))
|
|
295
|
+
retry = (Get-RalphRetryState $Root)
|
|
296
|
+
}
|
|
297
|
+
}
|