@flavor-code/superharness 1.0.0 → 1.0.3
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 +1 -0
- package/flavor-plugin.json +1 -1
- package/index.js +30 -14
- package/package.json +4 -6
- package/skills/brainstorm/scripts/mindmap.html +3 -0
- package/skills/brainstorm/scripts/server.cjs +13 -8
- package/skills/onboarding/SKILL.md +179 -0
- package/skills/onboarding/scripts/onboarding-lib.cjs +112 -0
package/HARNESS.md
CHANGED
|
@@ -19,6 +19,7 @@ the Skill tool before doing anything else.**
|
|
|
19
19
|
|-------|-------------|
|
|
20
20
|
| `superharness:go` | The user gives a task goal to complete end-to-end (also triggered by `/superharness:go <goal>`) |
|
|
21
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:onboarding` | When onboarding a newcomer or asked to explain/analyze the workspace's business logic — deep codebase analysis into ONBOARDING.md + interactive module mind map; also via `/onboarding` |
|
|
22
23
|
| `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
24
|
| `superharness:writing-plans` | A multi-step task needs an implementation plan, before touching code |
|
|
24
25
|
| `superharness:using-git-worktrees` | Starting feature work that needs an isolated workspace, before implementation (go Phase 0.5) |
|
package/flavor-plugin.json
CHANGED
package/index.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// superharness flavor-code plugin
|
|
2
2
|
// Registers the superharness skill root so that go, brainstorm, tdd, etc.
|
|
3
|
-
// are discovered
|
|
4
|
-
// lifecycle hooks mirroring the Claude Code hooks:
|
|
3
|
+
// are discovered by flavor-code, and registers eight lifecycle hooks:
|
|
5
4
|
// SessionStart inject HARNESS.md (+ STACK.md when present) as additionalContext
|
|
6
5
|
// UserPromptSubmit auto-bootstrap ralph tracking on `/superharness:go <goal>`
|
|
7
6
|
// Stop append a 'round' heartbeat to trace.jsonl while a go task runs
|
|
7
|
+
// SessionEnd checkpoint without clearing the active resumable task
|
|
8
|
+
// Before/AfterPlan record plan boundaries
|
|
9
|
+
// SubagentStart/Stop record child lifecycle and real completion status
|
|
8
10
|
// All hooks are best-effort and always return { decision: "allow" }.
|
|
9
11
|
|
|
10
|
-
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
11
13
|
import { dirname, join } from "node:path";
|
|
12
14
|
import { fileURLToPath } from "node:url";
|
|
13
15
|
|
|
@@ -37,9 +39,13 @@ function isoNow() {
|
|
|
37
39
|
|
|
38
40
|
function atomicWrite(path, text) {
|
|
39
41
|
mkdirSync(dirname(path), { recursive: true });
|
|
40
|
-
const tmp = `${path}.tmp`;
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
43
|
+
try {
|
|
44
|
+
writeFileSync(tmp, text, "utf8");
|
|
45
|
+
renameSync(tmp, path);
|
|
46
|
+
} finally {
|
|
47
|
+
try { rmSync(tmp, { force: true }); } catch { /* ignore */ }
|
|
48
|
+
}
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
function readJson(path) {
|
|
@@ -67,8 +73,7 @@ function appendTrace(root, phase, traceEvent, detail) {
|
|
|
67
73
|
const line = JSON.stringify({ ts: isoNow(), phase, event: traceEvent, detail: detail ?? "" });
|
|
68
74
|
const path = join(ralphDir(root), "trace.jsonl");
|
|
69
75
|
mkdirSync(dirname(path), { recursive: true });
|
|
70
|
-
|
|
71
|
-
writeFileSync(path, `${existing}${line}\n`, "utf8");
|
|
76
|
+
appendFileSync(path, `${line}\n`, "utf8");
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
// Parse a go invocation at the start of the prompt. flavor-code invokes skills
|
|
@@ -115,6 +120,13 @@ function onSessionStart(event) {
|
|
|
115
120
|
if (stack !== undefined) {
|
|
116
121
|
context += `\n\n<EXTREMELY_IMPORTANT>\nThis project targets a specific tech stack. Follow this guidance.\n\n${stack}\n</EXTREMELY_IMPORTANT>`;
|
|
117
122
|
}
|
|
123
|
+
// One-line onboarding nudge when the workspace has neither the generated
|
|
124
|
+
// doc nor an analysis cache. Never auto-analyze; the skill is manual-only.
|
|
125
|
+
if (typeof workspace === "string" && workspace.length > 0
|
|
126
|
+
&& !existsSync(join(workspace, "ONBOARDING.md"))
|
|
127
|
+
&& !existsSync(join(workspace, ".flavor", "superharness", "onboarding", "cache.json"))) {
|
|
128
|
+
context += `\n\n<superharness-onboarding-hint>\nNo onboarding guide for this workspace yet. Run /onboarding (superharness:onboarding) to analyze the codebase, map module business relationships, and generate ONBOARDING.md plus an interactive module mind map. The agent decides when to run it - nothing is analyzed automatically.\n</superharness-onboarding-hint>`;
|
|
129
|
+
}
|
|
118
130
|
return { decision: "allow", additionalContext: context };
|
|
119
131
|
}
|
|
120
132
|
|
|
@@ -152,8 +164,9 @@ function onStop(event) {
|
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
// Called when the session ends (host fires SessionEnd). Records a final
|
|
155
|
-
// trace
|
|
156
|
-
//
|
|
167
|
+
// trace checkpoint. It deliberately preserves .current-task: ending a host
|
|
168
|
+
// session is not the same as completing the go task, and cold-start resume
|
|
169
|
+
// relies on that pointer.
|
|
157
170
|
function onSessionEnd(event) {
|
|
158
171
|
const root = projectRoot(event);
|
|
159
172
|
const current = getCurrentTask(root);
|
|
@@ -161,8 +174,6 @@ function onSessionEnd(event) {
|
|
|
161
174
|
const tasks = readJson(join(ralphDir(root), "task.json"));
|
|
162
175
|
const phase = typeof tasks?.phase === "string" && tasks.phase.length > 0 ? tasks.phase : "go";
|
|
163
176
|
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
177
|
return ALLOW;
|
|
167
178
|
}
|
|
168
179
|
|
|
@@ -208,8 +219,13 @@ function onSubagentStop(event) {
|
|
|
208
219
|
if (current === undefined) return ALLOW;
|
|
209
220
|
const tasks = readJson(join(ralphDir(root), "task.json"));
|
|
210
221
|
const phase = typeof tasks?.phase === "string" && tasks.phase.length > 0 ? tasks.phase : "go";
|
|
211
|
-
const
|
|
212
|
-
|
|
222
|
+
const status = typeof event?.payload?.status === "string"
|
|
223
|
+
? event.payload.status
|
|
224
|
+
: typeof event?.payload?.outcome === "string" ? event.payload.outcome : "completed";
|
|
225
|
+
const error = typeof event?.payload?.error === "string" && event.payload.error.length > 0
|
|
226
|
+
? `: ${event.payload.error}`
|
|
227
|
+
: "";
|
|
228
|
+
appendTrace(root, phase, "subagent:stop", `subagent ${status}${error}`);
|
|
213
229
|
return ALLOW;
|
|
214
230
|
}
|
|
215
231
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flavor-code/superharness",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "superharness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
7
7
|
"files": [
|
|
@@ -11,11 +11,9 @@
|
|
|
11
11
|
"keywords": [
|
|
12
12
|
"flavor-code",
|
|
13
13
|
"flavor-plugin",
|
|
14
|
-
"
|
|
15
|
-
"skills",
|
|
16
|
-
"hooks"
|
|
14
|
+
"superharness"
|
|
17
15
|
],
|
|
18
|
-
"author": "
|
|
16
|
+
"author": "yachuanwzh",
|
|
19
17
|
"repository": "C:\\Users\\wangzh\\Desktop\\idea\\superharness",
|
|
20
18
|
"flavorPlugin": {
|
|
21
19
|
"apiVersion": "1"
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
.node { cursor: pointer; }
|
|
20
20
|
.node text { font-size: 13px; fill: var(--text); }
|
|
21
21
|
.node.rejected text { fill: var(--muted); text-decoration: line-through; }
|
|
22
|
+
.node.stale { opacity: 0.45; }
|
|
23
|
+
.node.stale text { fill: var(--muted); font-style: italic; }
|
|
22
24
|
.link { fill: none; stroke: #4a4e5e; stroke-width: 1.5; }
|
|
23
25
|
#hint { position: fixed; bottom: 10px; right: 14px; color: var(--muted); font-size: 12px; z-index: 10; }
|
|
24
26
|
</style>
|
|
@@ -48,6 +50,7 @@ const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
|
48
50
|
const KIND_COLORS = {
|
|
49
51
|
topic: '#3d5afe', question: '#8e6cf0', option: '#2a9d8f', decision: '#e9933a',
|
|
50
52
|
requirement: '#4fa3e3', risk: '#e35d6a', note: '#6c757d',
|
|
53
|
+
module: '#00b4d8', flow: '#90be6d', entity: '#b388eb',
|
|
51
54
|
};
|
|
52
55
|
const svg = document.getElementById('canvas');
|
|
53
56
|
const viewport = document.getElementById('viewport');
|
|
@@ -19,7 +19,10 @@ const EVENTS_FILE = path.join(STATE_DIR, 'events');
|
|
|
19
19
|
const EDITS_FILE = path.join(STATE_DIR, 'edits');
|
|
20
20
|
const INFO_FILE = path.join(STATE_DIR, 'server-info');
|
|
21
21
|
const STOPPED_FILE = path.join(STATE_DIR, 'server-stopped');
|
|
22
|
-
|
|
22
|
+
// Let the OS allocate an available ephemeral port unless the caller explicitly
|
|
23
|
+
// pins one. Randomly guessing a high port can still collide under parallel tests
|
|
24
|
+
// or when several brainstorm sessions start together.
|
|
25
|
+
const PORT = process.env.SUPERHARNESS_PORT ? Number(process.env.SUPERHARNESS_PORT) : 0;
|
|
23
26
|
const HOST = process.env.SUPERHARNESS_HOST || '127.0.0.1';
|
|
24
27
|
const IDLE_TIMEOUT_MS = Number(process.env.SUPERHARNESS_IDLE_TIMEOUT_MS) || 30 * 60 * 1000;
|
|
25
28
|
const IDLE_CHECK_MS = Math.min(5000, IDLE_TIMEOUT_MS);
|
|
@@ -192,13 +195,15 @@ setInterval(() => {
|
|
|
192
195
|
if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) shutdown(0);
|
|
193
196
|
}, IDLE_CHECK_MS).unref();
|
|
194
197
|
|
|
195
|
-
server.listen(PORT, HOST, () => {
|
|
196
|
-
try { fs.unlinkSync(STOPPED_FILE); } catch {}
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
server.listen(PORT, HOST, () => {
|
|
199
|
+
try { fs.unlinkSync(STOPPED_FILE); } catch {}
|
|
200
|
+
const address = server.address();
|
|
201
|
+
const actualPort = typeof address === 'object' && address !== null ? address.port : PORT;
|
|
202
|
+
const urlHost = HOST === '127.0.0.1' ? 'localhost' : HOST;
|
|
203
|
+
const info = {
|
|
204
|
+
type: 'server-started',
|
|
205
|
+
port: actualPort,
|
|
206
|
+
url: 'http://' + urlHost + ':' + actualPort,
|
|
202
207
|
pid: process.pid,
|
|
203
208
|
content_dir: CONTENT_DIR,
|
|
204
209
|
state_dir: STATE_DIR,
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: onboarding
|
|
3
|
+
description: Use when the user runs /superharness:onboarding or /onboarding, or asks to onboard a newcomer / understand the codebase's business logic - deeply analyzes the workspace codebase (module responsibilities, cross-module business flows, data models) and produces ONBOARDING.md plus a live interactive mind map; supports incremental re-runs via cache
|
|
4
|
+
argument-hint: [optional focus: module name, flow, or "drill <node>"]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Superharness Onboarding — deep business-logic analysis for newcomers
|
|
8
|
+
|
|
9
|
+
**Focus:** $ARGUMENTS
|
|
10
|
+
|
|
11
|
+
**Announce at start:** "Superharness onboarding engaged. Analyzing workspace: <goal or 'full overview'>."
|
|
12
|
+
|
|
13
|
+
Turn the current workspace into something a new team member can understand fast:
|
|
14
|
+
a committed `ONBOARDING.md` + per-topic docs, mirrored live to an interactive
|
|
15
|
+
mind map in the browser. The analysis is **layered** (overview → drill-down) and
|
|
16
|
+
**incremental** (cache keyed by git hash). Deep business-logic interpretation is
|
|
17
|
+
done by you (the agent); mechanical graph work is delegated to the astgraph
|
|
18
|
+
plugin when available, with a mandatory fallback when it is not.
|
|
19
|
+
|
|
20
|
+
**Cache path (exact, per host — do not double-nest):** the SessionStart hooks
|
|
21
|
+
and incremental runs all key off ONE file per host:
|
|
22
|
+
|
|
23
|
+
- Claude Code: `.claude/superharness/onboarding/cache.json`
|
|
24
|
+
- flavor-code: `.flavor/superharness/onboarding/cache.json`
|
|
25
|
+
|
|
26
|
+
(the directory above the filename is referred to as the onboarding cache dir
|
|
27
|
+
below). Both are gitignored by the superharness installers. Generated docs
|
|
28
|
+
(`ONBOARDING.md`, `docs/onboarding/`) are committed repo files; the cache is not.
|
|
29
|
+
|
|
30
|
+
<HARD-GATE>
|
|
31
|
+
Never modify source files. This skill only reads code and writes: the
|
|
32
|
+
onboarding cache (state root, gitignored), ONBOARDING.md, docs/onboarding/*,
|
|
33
|
+
and mind-map snapshots. Do not refactor, fix, or "improve" the analyzed code.
|
|
34
|
+
</HARD-GATE>
|
|
35
|
+
|
|
36
|
+
## Phase 0 — Engine detection (never blocks on astgraph)
|
|
37
|
+
|
|
38
|
+
1. Determine the workspace root and state root.
|
|
39
|
+
2. Decide the analysis engine — pipe a JSON object with booleans
|
|
40
|
+
`astToolsAvailable` / `indexDbExists` to the deterministic helper (write the
|
|
41
|
+
JSON to a temp file and pipe it; raw `echo '...'` quoting differs between
|
|
42
|
+
cmd.exe and POSIX shells):
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
<json-file> | node <this skill's base directory>/scripts/onboarding-lib.cjs engine
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- `astToolsAvailable` = this session actually exposes `ast_search /
|
|
49
|
+
ast_callers / ast_callees / ast_impact / ast_context` (flavor-code with the
|
|
50
|
+
astgraph plugin; Claude Code hosts normally do NOT).
|
|
51
|
+
- `indexDbExists` = `.flavor/astgraph/index.db` exists in the workspace.
|
|
52
|
+
3. Outcomes:
|
|
53
|
+
- `{engine:"astgraph"}` → use astgraph retrieval throughout Phase B.
|
|
54
|
+
- `{engine:"fallback", suggestAstInit:true}` → tools exist but no graph yet:
|
|
55
|
+
**tell the user they can run `/ast init` for sharper analysis later, and
|
|
56
|
+
continue right now with the fallback engine** — never wait for it.
|
|
57
|
+
- `{engine:"fallback"}` → no astgraph at all (plugin not installed, Claude
|
|
58
|
+
Code host, or non-TS/JS project): fallback is the plan, not a failure.
|
|
59
|
+
4. Fallback engine = Glob/Grep/Read + LSP (`LspFindRefs`, `LspHover`). Record
|
|
60
|
+
the engine in the cache and stamp each generated doc header with
|
|
61
|
+
`分析引擎: astgraph|fallback` so readers know the precision level.
|
|
62
|
+
|
|
63
|
+
## Phase 1 — Session + cache
|
|
64
|
+
|
|
65
|
+
1. Read the onboarding cache (`.claude/superharness/onboarding/cache.json` or
|
|
66
|
+
`.flavor/superharness/onboarding/cache.json`) if present (may be empty/absent).
|
|
67
|
+
2. Get `git rev-parse HEAD` and the changed-file set:
|
|
68
|
+
- with cache: `git diff --name-only <cache.gitHash> HEAD` plus
|
|
69
|
+
`git status --porcelain` (dirty files);
|
|
70
|
+
- no cache → full pass.
|
|
71
|
+
3. Plan the pass with the helper:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
<json-file: {"cache":<cache-or-null>,"headHash":"<head>","changedFiles":[...]}> | node <skill base>/scripts/onboarding-lib.cjs refresh
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`{full:true}` → module discovery for everything; otherwise only the
|
|
78
|
+
`changed` module ids get (re-)analyzed in depth.
|
|
79
|
+
|
|
80
|
+
**Mind map session** (like brainstorm; degrade gracefully to terminal-only if node/scripts fail):
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File "<superharness skills dir>/brainstorm/scripts/start-server.ps1" -ProjectDir "<project root>"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Save `url`, `content_dir`, `state_dir` from the JSON it prints, tell the user to
|
|
87
|
+
open the URL, and remind them the brainstorm session dir (`.claude/superharness/brainstorm/`
|
|
88
|
+
or `.flavor/superharness/brainstorm/`) should be gitignored. Push snapshots using
|
|
89
|
+
the brainstorm message protocol (`type:"mindmap:snapshot"`); node `kind` values
|
|
90
|
+
additionally used here are `module`, `flow`, `entity`; stale entries are pushed
|
|
91
|
+
with `state: "stale"` (the viewer greys them out).
|
|
92
|
+
|
|
93
|
+
## Phase 2 — Phase A: module overview (shallow, bounded cost)
|
|
94
|
+
|
|
95
|
+
1. **Discover modules**: directory/package structure (`package.json`, workspace
|
|
96
|
+
configs, top-level dirs), entry points, and import/dependency edges — via
|
|
97
|
+
`ast_search` when astgraph is live, else Grep for import/require statements.
|
|
98
|
+
Cluster into modules by directory/package boundary (keep the project's own
|
|
99
|
+
vocabulary for names).
|
|
100
|
+
2. **Rank by connectivity**; deep-analyze at most the top 8 modules this run
|
|
101
|
+
(spec: control LLM cost on first pass). Mark the rest `待下钻` in the map.
|
|
102
|
+
3. For each analyzed module produce the cached row:
|
|
103
|
+
`{ "summary": 一句话职责, "files": [ownership paths], "deps": [module ids], "doc": docs/onboarding/<id>.md }`
|
|
104
|
+
and note **business relations** — who calls it in which scenario, what data
|
|
105
|
+
crosses the boundary (read the code; do not guess).
|
|
106
|
+
4. Push the overview snapshot (root = project name, children = module nodes with
|
|
107
|
+
one-line labels, dependency edges as `note` children on the callee), and
|
|
108
|
+
write/update `ONBOARDING.md`:
|
|
109
|
+
- 项目一句话 + 技术栈
|
|
110
|
+
- Mermaid module map (`graph LR` with dependency arrows)
|
|
111
|
+
- 模块速览表(职责 / 关键入口 file:line / 依赖)
|
|
112
|
+
- 推荐上手路径(读代码顺序,3–5 步,每步给 file:line)
|
|
113
|
+
- 核心业务流程索引(链接 docs/onboarding/*.md)
|
|
114
|
+
- 头部署名:`分析引擎` + 缓存 git hash(短格式)
|
|
115
|
+
|
|
116
|
+
## Phase 3 — Phase B: drill-down (deep, demand-driven)
|
|
117
|
+
|
|
118
|
+
Two entry paths, same loop:
|
|
119
|
+
- **Terminal** (always available): user asks, e.g. "深入 install 模块" /
|
|
120
|
+
"一次 go 任务从头到尾经过哪些模块" / `"/onboarding drill <node-or-flow>"`.
|
|
121
|
+
- **Browser clicks**: after each snapshot, poll `<state_dir>/events` (like
|
|
122
|
+
brainstorm edit rounds). A `node:click` on a `module` / `flow` node = drill
|
|
123
|
+
request for that node.
|
|
124
|
+
|
|
125
|
+
Per drill-down:
|
|
126
|
+
1. Pick 1–3 representative **business flows** through the target (entry → ... →
|
|
127
|
+
persistence/output). For each hop, record `file:line`.
|
|
128
|
+
- astgraph: `ast_callers` / `ast_callees` walk the chain; `ast_impact
|
|
129
|
+
--hops 3` gives the cross-module blast radius; `ast_context` reads ranges.
|
|
130
|
+
- fallback: `LspFindRefs` for call sites, Grep for dynamic/dispatch calls,
|
|
131
|
+
Read for the semantics. Say explicitly in the doc when a link is inferred
|
|
132
|
+
rather than graph-verified.
|
|
133
|
+
2. Identify **data flow**: the entities that cross module boundaries, where
|
|
134
|
+
they're created, transformed, stored. Add `entity` nodes to the map.
|
|
135
|
+
3. Write `docs/onboarding/<topic>.md`: 场景说明 → 调用链(编号步骤 + file:line)
|
|
136
|
+
→ 模块协作图(Mermaid sequenceDiagram)→ 数据流转 → 改动影响面(ast_impact
|
|
137
|
+
结果或 Grep 依据)→ 常见坑(来自真实代码观察)。
|
|
138
|
+
4. Push an updated snapshot (rev+1) with the new branch expanded, update cache,
|
|
139
|
+
and refresh the `ONBOARDING.md` flow index.
|
|
140
|
+
|
|
141
|
+
Loop until the user stops asking or the map is fully explored. Keep each drill
|
|
142
|
+
focused; do not re-analyze cached unchanged modules.
|
|
143
|
+
|
|
144
|
+
## Phase 4 — Phase C: self-check + finish
|
|
145
|
+
|
|
146
|
+
1. **Stale sweep**:
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
<json-file: {"cache":<cache>,"existingFiles":[...workspace file list...]}> | node <skill base>/scripts/onboarding-lib.cjs stale
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Mark returned entries `stale: true` in cache and grey them in the map;
|
|
153
|
+
offer to re-analyze them (they'll appear as `changed` next refresh).
|
|
154
|
+
2. Save cache atomically with the new `gitHash` + engine + timestamp.
|
|
155
|
+
3. Stop the mind-map server:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File "<superharness skills dir>/brainstorm/scripts/stop-server.ps1" -SessionDir "<session directory>"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
4. Report: which docs were created/updated, engine used, what was marked stale.
|
|
162
|
+
Remind the user that `ONBOARDING.md` and `docs/onboarding/` are plain repo
|
|
163
|
+
files they (or the team) decide whether to commit — this skill never commits.
|
|
164
|
+
|
|
165
|
+
## Incremental re-runs
|
|
166
|
+
|
|
167
|
+
Second `/onboarding` invocation: refresh-plan → only changed/deep-missing
|
|
168
|
+
modules re-analyzed, stale sweep runs, map is re-pushed from cache. If
|
|
169
|
+
`git` is unavailable, treat everything as changed (full pass) and note it.
|
|
170
|
+
|
|
171
|
+
## Red Flags
|
|
172
|
+
|
|
173
|
+
| Thought | Reality |
|
|
174
|
+
|---------|---------|
|
|
175
|
+
| "No astgraph, let me bail" | Fallback engine is a first-class path. Continue. |
|
|
176
|
+
| "Let me ask them to /ast init first" | Hint it, then proceed with fallback — never block. |
|
|
177
|
+
| "I'll analyze every module deeply now" | Top-8 by connectivity, rest on demand. Cost discipline. |
|
|
178
|
+
| "This call link looks obvious, skip verification" | Guessing creates docs that mislead newcomers. Cite file:line. |
|
|
179
|
+
| "I'll tidy this messy module while here" | Read-only. HARD-GATE: no source edits. |
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Superharness onboarding skill — deterministic analysis helpers.
|
|
2
|
+
// The agent (SKILL.md) drives the semantic deep dives; this library owns the
|
|
3
|
+
// mechanical decisions so they are testable and stable across hosts:
|
|
4
|
+
// engine — pick astgraph vs. fallback (fallback is mandatory-capable)
|
|
5
|
+
// refresh — what actually needs re-analysis given cache + changed files
|
|
6
|
+
// stale — cached docs/anchors that no longer match the working tree
|
|
7
|
+
//
|
|
8
|
+
// CLI: node onboarding-lib.cjs <engine|refresh|stale> (reads JSON on stdin)
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const STALE_PREFIX = 'flow:';
|
|
13
|
+
|
|
14
|
+
function normalizeRel(p) {
|
|
15
|
+
return String(p || '').replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Double detection (spec): ast_* tool availability AND .flavor/astgraph/index.db
|
|
19
|
+
// existence. Tools without an index never block the run: fall back immediately
|
|
20
|
+
// and merely suggest `/ast init` for future precision.
|
|
21
|
+
function detectEngine({ astToolsAvailable = false, indexDbExists = false } = {}) {
|
|
22
|
+
if (astToolsAvailable && indexDbExists) return { engine: 'astgraph' };
|
|
23
|
+
const result = { engine: 'fallback' };
|
|
24
|
+
if (astToolsAvailable && !indexDbExists) result.suggestAstInit = true;
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Module membership: a cached module owns the file when its recorded file list
|
|
29
|
+
// contains it (prefix rules like "dir/*" match by directory).
|
|
30
|
+
function moduleOwnsFile(module, file) {
|
|
31
|
+
const f = normalizeRel(file);
|
|
32
|
+
return (module.files || []).some((owned) => {
|
|
33
|
+
const o = normalizeRel(owned);
|
|
34
|
+
if (o.endsWith('/*')) return f.startsWith(o.slice(0, -1));
|
|
35
|
+
if (o.endsWith('/')) return f.startsWith(o);
|
|
36
|
+
return f === o;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// cache: { gitHash, modules: { id: { files: [...] } } } | null
|
|
41
|
+
// changedFiles: workspace-relative paths (git diff --name-only HEAD <hash> etc.)
|
|
42
|
+
function planRefresh({ cache, headHash, changedFiles = [] } = {}) {
|
|
43
|
+
if (!cache || !cache.modules) {
|
|
44
|
+
return { full: true, headHash, changed: [] };
|
|
45
|
+
}
|
|
46
|
+
if (cache.gitHash === headHash && changedFiles.length === 0) {
|
|
47
|
+
return { full: false, headHash, changed: [] };
|
|
48
|
+
}
|
|
49
|
+
const changed = new Set();
|
|
50
|
+
for (const file of changedFiles) {
|
|
51
|
+
for (const [id, module] of Object.entries(cache.modules)) {
|
|
52
|
+
if (moduleOwnsFile(module, file)) changed.add(id);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { full: false, headHash, changed: [...changed] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Walk cached docs and flow anchors; anything the fileExists probe rejects is
|
|
59
|
+
// reported as stale (flows are prefixed with "flow:" to keep ids unique).
|
|
60
|
+
function staleCheck(cache, { fileExists } = {}) {
|
|
61
|
+
const stale = [];
|
|
62
|
+
const exists = typeof fileExists === 'function' ? fileExists : () => true;
|
|
63
|
+
const modules = (cache && cache.modules) || {};
|
|
64
|
+
const flows = (cache && cache.flows) || {};
|
|
65
|
+
for (const [id, m] of Object.entries(modules)) {
|
|
66
|
+
if (m && typeof m.doc === 'string' && !exists(m.doc)) stale.push(id);
|
|
67
|
+
}
|
|
68
|
+
for (const [id, f] of Object.entries(flows)) {
|
|
69
|
+
if (f && typeof f.doc === 'string' && !exists(f.doc)) { stale.push(STALE_PREFIX + id); continue; }
|
|
70
|
+
const anchors = (f && f.anchors) || [];
|
|
71
|
+
const broken = anchors.some((a) => {
|
|
72
|
+
const file = String(a).split('#')[0];
|
|
73
|
+
return file.length > 0 && !exists(file);
|
|
74
|
+
});
|
|
75
|
+
if (broken) stale.push(STALE_PREFIX + id);
|
|
76
|
+
}
|
|
77
|
+
return { stale };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readStdin() {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
let text = '';
|
|
83
|
+
process.stdin.on('data', (chunk) => { text += chunk; });
|
|
84
|
+
process.stdin.on('end', () => resolve(text));
|
|
85
|
+
process.stdin.on('error', reject);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const COMMANDS = {
|
|
90
|
+
engine: detectEngine,
|
|
91
|
+
refresh: planRefresh,
|
|
92
|
+
stale: (input) => staleCheck(input.cache, { fileExists: (p) => new Set((input.existingFiles || []).map(normalizeRel)).has(normalizeRel(p)) }),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (require.main === module) {
|
|
96
|
+
(async () => {
|
|
97
|
+
const command = process.argv[2];
|
|
98
|
+
const run = COMMANDS[command];
|
|
99
|
+
if (run === undefined) {
|
|
100
|
+
process.stderr.write(`unknown command "${command ?? ''}"; expected one of: ${Object.keys(COMMANDS).join(', ')}\n`);
|
|
101
|
+
process.exit(2);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const input = JSON.parse((await readStdin()) || '{}');
|
|
105
|
+
process.stdout.write(JSON.stringify(run(input)));
|
|
106
|
+
})().catch((err) => {
|
|
107
|
+
process.stderr.write(`${err.message}\n`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { detectEngine, planRefresh, staleCheck, normalizeRel };
|