harnesstrim 0.0.1

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.
@@ -0,0 +1,242 @@
1
+ """
2
+ HarnessTrim Hermes plugin — tool-output reduction via transform_tool_result hook.
3
+
4
+ When enabled, this plugin intercepts the ``transform_tool_result`` hook after every
5
+ tool call and slims noisy output (test runners, git diffs, build logs) to its signal
6
+ before the result enters the model's context. Dry-run mode logs what *would* be
7
+ reduced; active mode rewrites the result; telemetry (off by default) records every
8
+ reduction as a TrimEvent JSON line.
9
+
10
+ Run the ``harnesstrim reduce`` command via a subprocess so the reducers live in the
11
+ shared TypeScript/Node core rather than being reimplemented in Python.
12
+
13
+ Hook contract
14
+ -------------
15
+ ``transform_tool_result`` is a built-in Hermes plugin hook:
16
+ - ``hermes_cli/plugins.py`` — listed in ``VALID_HOOKS``
17
+ - ``model_tools.py:1313-1345`` — core loop fires it after every tool call;
18
+ first callback to return a string replaces the result
19
+ - ``plugins/security-guidance/__init__.py`` — shipped plugin uses it
20
+ - ``tests/test_transform_tool_result_hook.py`` — dedicated test coverage
21
+
22
+ Callback receives: tool_name, args, result, task_id, session_id, tool_call_id,
23
+ turn_id, api_request_id, duration_ms, status, error_type, error_message.
24
+ Return a string to replace the result, or None to leave unchanged.
25
+ """
26
+
27
+ import json
28
+ import os
29
+ import subprocess
30
+ import sys
31
+ from pathlib import Path
32
+
33
+ PLUGIN_DIR = Path(__file__).parent
34
+
35
+ CONFIG_DEFAULTS = {
36
+ "mode": "dryrun", # "dryrun" | "active" | "off"
37
+ "minLength": 400,
38
+ "telemetry": False, # explicit opt-in: tool output may contain sensitive data
39
+ "debug": False,
40
+ }
41
+
42
+ # Store telemetry in the active Hermes home. HERMES_HOME is profile-aware when set
43
+ # by Hermes; direct CLI usage falls back to the default profile.
44
+ METRICS_PATH = Path(os.environ.get("HERMES_HOME", str(Path.home() / ".hermes"))) / "harnesstrim-metrics.jsonl"
45
+
46
+ # Tool types whose output we consider for reduction.
47
+ REDUCER_TOOLS = frozenset({
48
+ "terminal",
49
+ "read_file",
50
+ "web_extract",
51
+ "search_files",
52
+ "browser_snapshot",
53
+ "vision_analyze",
54
+ })
55
+
56
+
57
+ def register(ctx):
58
+ """Register the HarnessTrim transform_tool_result hook."""
59
+ ctx.register_hook("transform_tool_result", on_tool_result)
60
+
61
+
62
+ def _load_config():
63
+ """Return the active plugin config from the environment (no global registry needed)."""
64
+ cfg = dict(CONFIG_DEFAULTS)
65
+ for key in CONFIG_DEFAULTS:
66
+ env_key = f"HARNESSTRIM_{key.upper()}"
67
+ val = os.environ.get(env_key)
68
+ if val is not None:
69
+ if isinstance(CONFIG_DEFAULTS[key], bool):
70
+ cfg[key] = val.lower() in ("1", "true", "yes")
71
+ elif isinstance(CONFIG_DEFAULTS[key], int):
72
+ try:
73
+ cfg[key] = int(val)
74
+ except ValueError:
75
+ pass
76
+ else:
77
+ cfg[key] = val
78
+ return cfg
79
+
80
+
81
+ def _find_harnesstrim_cli() -> str | None:
82
+ """Locate the ``harnesstrim`` CLI binary on PATH."""
83
+ # Try PATH resolution
84
+ for p in os.environ.get("PATH", "").split(os.pathsep):
85
+ candidate = Path(p) / "harnesstrim"
86
+ if candidate.is_file() and os.access(candidate, os.X_OK):
87
+ return str(candidate.resolve())
88
+ # Check common monorepo dev location via the repo-root sentinel
89
+ return None
90
+
91
+
92
+ def _call_reducer(text: str, min_length: int) -> tuple[str, str | None]:
93
+ """Shell out to ``harnesstrim reduce`` and return (slimmed_text, reducer_name).
94
+
95
+ Falls back to (original_text, None) if the CLI cannot be found or the pipe fails.
96
+ The reducer name is parsed from the ``--stats`` stderr line (e.g. ``test-output-slim``),
97
+ used only for telemetry when enabled.
98
+ """
99
+ cli = _find_harnesstrim_cli()
100
+ if cli is None:
101
+ import warnings
102
+ warnings.warn(
103
+ "[harnesstrim] CLI not found on PATH — output not reduced. "
104
+ "Install CLI with: curl -fsSL https://harnesstrim.dev/install.sh | bash "
105
+ "or build from source: git clone https://github.com/harnesstrim/harnesstrim",
106
+ stacklevel=2,
107
+ )
108
+ return (text, None)
109
+
110
+ if len(text) < min_length:
111
+ return (text, None)
112
+
113
+ try:
114
+ result = subprocess.run(
115
+ [cli, "reduce", "--min-length", str(min_length), "--stats"],
116
+ input=text,
117
+ capture_output=True,
118
+ text=True,
119
+ timeout=30,
120
+ )
121
+ if result.returncode == 0 and result.stdout:
122
+ reducer = None
123
+ # stderr: "[harnesstrim reduce] <reducer>: <before> -> <after> chars"
124
+ for line in result.stderr.splitlines():
125
+ line = line.strip()
126
+ if line.startswith("[harnesstrim reduce]"):
127
+ rest = line[len("[harnesstrim reduce] "):]
128
+ if ":" in rest and "no reduction" not in rest:
129
+ reducer = rest.split(":")[0].strip()
130
+ return (result.stdout.rstrip("\n"), reducer)
131
+ except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
132
+ pass
133
+
134
+ return (text, None)
135
+
136
+
137
+ def _text_targets(payload):
138
+ """Yield mutable payload mappings and text-field names safe to reduce.
139
+
140
+ Hermes tools use different result schemas: terminal returns ``output``, file
141
+ tools ``content``, browser snapshots ``snapshot``, vision ``analysis``, and
142
+ web_extract has one content field per entry in ``results``.
143
+ """
144
+ if not isinstance(payload, dict):
145
+ return []
146
+
147
+ targets = []
148
+ for key in ("output", "content", "snapshot", "analysis"):
149
+ if isinstance(payload.get(key), str) and payload[key]:
150
+ targets.append((payload, key))
151
+ break
152
+
153
+ results = payload.get("results")
154
+ if isinstance(results, list):
155
+ for entry in results:
156
+ if not isinstance(entry, dict):
157
+ continue
158
+ for key in ("content", "output", "text"):
159
+ if isinstance(entry.get(key), str) and entry[key]:
160
+ targets.append((entry, key))
161
+ break
162
+ return targets
163
+
164
+
165
+ def on_tool_result(tool_name, args, result, **kwargs):
166
+ """Reduce recognized textual fields while preserving the original JSON schema."""
167
+ cfg = _load_config()
168
+ if cfg["mode"] == "off" or tool_name not in REDUCER_TOOLS:
169
+ return None
170
+ if not result or not isinstance(result, str):
171
+ return None
172
+
173
+ try:
174
+ payload = json.loads(result)
175
+ except (json.JSONDecodeError, ValueError):
176
+ payload = None
177
+
178
+ if isinstance(payload, str):
179
+ targets = [(None, None, payload)]
180
+ elif payload is None:
181
+ targets = [(None, None, result)]
182
+ else:
183
+ targets = [(container, key, container[key]) for container, key in _text_targets(payload)]
184
+
185
+ if not targets:
186
+ return None
187
+
188
+ changed = False
189
+ for container, key, text in targets:
190
+ before_len = len(text)
191
+ if before_len < cfg["minLength"]:
192
+ continue
193
+ if "[harnesstrim:" in text or "[hermes-trim" in text:
194
+ continue
195
+
196
+ after, reducer = _call_reducer(text, cfg["minLength"])
197
+ if after == text:
198
+ continue
199
+
200
+ changed = True
201
+ if cfg["mode"] == "dryrun":
202
+ print(
203
+ f"[harnesstrim] dryrun: {tool_name} "
204
+ f"{before_len} -> {len(after)} chars (would save {before_len - len(after)})",
205
+ file=sys.stderr,
206
+ )
207
+ continue
208
+
209
+ if container is None:
210
+ payload = after
211
+ else:
212
+ container[key] = after
213
+ if cfg["telemetry"]:
214
+ _write_metric(tool_name, reducer, before_len, len(after))
215
+
216
+ if not changed or cfg["mode"] == "dryrun":
217
+ return None
218
+ return json.dumps(payload, ensure_ascii=False) if not isinstance(payload, str) else payload
219
+
220
+
221
+ def _write_metric(tool: str, reducer: str | None, before: int, after: int) -> None:
222
+ """Append one TrimEvent JSONL line to METRICS_PATH (read by `harnesstrim metrics`).
223
+
224
+ Only called in active mode when telemetry is explicitly enabled. Creates the parent
225
+ directory lazily and swallows any error — telemetry must never crash the plugin.
226
+ """
227
+ import datetime as _dt
228
+
229
+ event = {
230
+ "ts": _dt.datetime.now(_dt.timezone.utc).isoformat(),
231
+ "harness": "hermes",
232
+ "tool": tool,
233
+ "reducer": reducer,
234
+ "beforeChars": before,
235
+ "afterChars": after,
236
+ }
237
+ try:
238
+ METRICS_PATH.parent.mkdir(parents=True, exist_ok=True)
239
+ with open(METRICS_PATH, "a", encoding="utf-8") as f:
240
+ f.write(json.dumps(event, ensure_ascii=False) + "\n")
241
+ except OSError:
242
+ pass # telemetry must never crash the plugin
@@ -0,0 +1,6 @@
1
+ name: harnesstrim
2
+ version: "0.0.1"
3
+ description: >
4
+ HarnessTrim reducer plugin — slims noisy tool output (test logs, git diffs, build output)
5
+ before it reaches the model, via the transform_tool_result hook.
6
+ requires_env: []
@@ -0,0 +1,62 @@
1
+ // HarnessTrim Pi extension — slims noisy tool output via the `tool_result` hook.
2
+ //
3
+ // Pi fires `tool_result` after a tool finishes and before the result reaches the model;
4
+ // handlers chain like middleware and may return a patch ({ content, details, isError }).
5
+ // This extension reduces string `content` for noisy output (test runners, git diffs, ...)
6
+ // by shelling out to `harnesstrim reduce`, so it is self-contained (no workspace imports)
7
+ // and loads from `~/.pi/agent/extensions/` or `<project>/.pi/extensions/`.
8
+ //
9
+ // Requires `harnesstrim` on PATH; if it is missing or fails, the output is passed through
10
+ // unchanged (a reducer must never break a tool result). Config via env:
11
+ // HARNESSTRIM_MODE=dryrun|active|off (default dryrun — logs, does not mutate)
12
+ // HARNESSTRIM_MINLENGTH=<chars> (default 400)
13
+ import { spawnSync } from "node:child_process";
14
+
15
+ interface ToolResultEvent {
16
+ content?: unknown;
17
+ isError?: boolean;
18
+ }
19
+ interface ExtensionAPI {
20
+ on(event: string, handler: (event: ToolResultEvent, ctx: unknown) => unknown): void;
21
+ }
22
+
23
+ const env = globalThis.process?.env ?? {};
24
+ const MODE = env.HARNESSTRIM_MODE ?? "dryrun";
25
+ const MIN_LENGTH = Number(env.HARNESSTRIM_MINLENGTH ?? "400") || 400;
26
+ const MARKER = "[harnesstrim";
27
+
28
+ function reduceViaCli(text: string): string | null {
29
+ try {
30
+ const r = spawnSync("harnesstrim", ["reduce", "--min-length", String(MIN_LENGTH)], {
31
+ input: text,
32
+ encoding: "utf8",
33
+ timeout: 30000,
34
+ });
35
+ if (r.status === 0 && typeof r.stdout === "string" && r.stdout.length > 0) {
36
+ return r.stdout.replace(/\n$/, "");
37
+ }
38
+ } catch {
39
+ /* harnesstrim not on PATH or failed — pass through */
40
+ }
41
+ return null;
42
+ }
43
+
44
+ export default function harnesstrim(pi: ExtensionAPI): void {
45
+ if (MODE === "off") return;
46
+ pi.on("tool_result", async (event) => {
47
+ const content = event.content;
48
+ if (typeof content !== "string" || content.length < MIN_LENGTH) return;
49
+ if (content.includes(MARKER)) return; // already reduced — avoid double work
50
+
51
+ const reduced = reduceViaCli(content);
52
+ if (!reduced || reduced.length >= content.length) return;
53
+
54
+ if (MODE === "dryrun") {
55
+ globalThis.process?.stderr?.write(
56
+ `[harnesstrim] dryrun tool_result: ${content.length} -> ${reduced.length} chars\n`
57
+ );
58
+ return; // dryrun: observe only
59
+ }
60
+ return { content: reduced }; // active: patch the tool result the model sees
61
+ });
62
+ }
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: compact-handoff
3
+ description: This skill should be used when a session is being compacted/summarized (context is full or the user runs a compact command), or when writing any handoff summary that a later session or subagent will continue from. Defines what to preserve versus drop so the continuation doesn't waste tokens re-reading files and re-deriving state. Part of the HarnessTrim token-economy stack; the OpenCode adapter injects this guidance automatically via experimental.session.compacting.
4
+ version: 0.1.0
5
+ license: MIT
6
+ ---
7
+
8
+ # compact-handoff
9
+
10
+ A compaction summary is the seed context for everything that follows. If it drops the wrong
11
+ things, the next turns re-read files, re-run searches, and re-decide settled questions — spending
12
+ far more tokens than the summary ever saved. Optimize the summary for *resumption*, not brevity.
13
+
14
+ ## Preserve
15
+
16
+ 1. **The task and its acceptance criteria** — what "done" means, verbatim if the user stated it.
17
+ 2. **Decisions made and alternatives rejected** — with the reason, so they aren't re-litigated.
18
+ 3. **Hard-won specifics** — exact file paths, symbol names, commands that worked, config values,
19
+ version numbers, API signatures discovered. These are the expensive-to-rediscover facts.
20
+ 4. **Current state** — what is done, what is verified (and how), what is still failing and the
21
+ last error seen.
22
+
23
+ ## Drop
24
+
25
+ - Step-by-step narration of what was already done ("first I opened X, then I ran Y").
26
+ - Raw tool output that has already been acted on (test logs, file dumps, search results).
27
+ - Anything trivially reconstructable from the current diff or a single cheap command.
28
+ - Restatements of the codebase's structure that the next session can read directly.
29
+
30
+ ## Shape
31
+
32
+ Write the summary as short labeled sections (Task / Decisions / Key facts / State), not prose.
33
+ A resuming agent should be able to act from it without re-reading the whole transcript.
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: debug-log-slim
3
+ description: This skill should be used before pasting, printing, or otherwise embedding test-runner output, build logs, or CI output into the conversation — whenever a test suite, linter, or build was just run and its raw output is large. Filters logs down to failure/error signal before they enter context. Part of the HarnessTrim token-economy stack (see packages/core/src/reducers/test-output-slim.ts for the reference implementation the OpenCode adapter automates this with).
4
+ version: 0.1.0
5
+ license: MIT
6
+ ---
7
+
8
+ # debug-log-slim
9
+
10
+ Raw test/build output is mostly noise: hundreds of PASS/OK lines for every one line that
11
+ actually matters. Don't paste it all into context — filter first.
12
+
13
+ ## Rule
14
+
15
+ Before showing test/build/lint output to the user or reasoning over it at length, reduce it to:
16
+
17
+ 1. **Every FAIL / ERROR / Exception / Traceback / assertion line.**
18
+ 2. **A few lines of context around each** (the stack frame or expected/received diff — usually
19
+ 3–5 lines is enough to act on).
20
+ 3. **The final summary line** (e.g. `12 passed, 3 failed`), always kept verbatim.
21
+ 4. **Everything else collapsed** into a single count, e.g. `… 40 passing lines omitted …` — never
22
+ silently dropped without saying how much was cut.
23
+
24
+ ## How to apply it
25
+
26
+ - If the HarnessTrim OpenCode adapter is installed, this happens automatically via
27
+ `tool.execute.before` — no manual filtering needed, this skill is then just a fallback for
28
+ contexts where the adapter isn't wired in.
29
+ - Without the adapter: when a command produces long output, pipe it through a targeted filter
30
+ before reading it in full — e.g. `grep -E 'FAIL|ERROR|Exception' -A 5` (adjust the pattern to
31
+ the test runner's failure markers) — rather than reading the entire raw log.
32
+ - Never summarize-then-discard: if you filtered, the user should still be able to ask "show me
33
+ the full log" and get it — filtering is about what enters *reasoning context* by default, not
34
+ deleting the underlying artifact.
35
+
36
+ ## What NOT to filter out
37
+
38
+ - Warnings that indicate a real (if non-fatal) problem — e.g. deprecation notices tied to the
39
+ change just made.
40
+ - Anything the user explicitly asked to see in full.
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: delegate-bulk
3
+ description: This skill should be used when deciding whether to hand volumetric or noisy work to an isolated subagent (or another harness) instead of doing it in the main context — e.g. reading many files, digesting long logs/docs, wide searches, or bulk mechanical generation. It gives the rule for when isolation saves tokens versus when it just multiplies them. Part of the HarnessTrim token-economy stack.
4
+ version: 0.1.0
5
+ license: MIT
6
+ ---
7
+
8
+ # delegate-bulk
9
+
10
+ Subagents are not free parallelism — each carries its own context and bills its own tokens. The
11
+ right move is surgical isolation of *noise*, not "multi-agent everything."
12
+
13
+ ## Delegate to an isolated subagent when
14
+
15
+ - A task will **read a large volume the main thread doesn't need to retain** — many files, long
16
+ logs, big docs, wide search results — and you only need the *conclusion* back, not the raw bytes.
17
+ - The work is **noisy but self-contained**: exploring a subsystem, triaging a failing test suite,
18
+ digesting API docs. Isolate it so its raw output never pollutes the main context.
19
+ - You need **several independent investigations at once** and each one's detail is irrelevant to the
20
+ others.
21
+
22
+ The token win comes from the subagent returning a short structured answer while its expensive
23
+ reading stays in a context you throw away.
24
+
25
+ ## Do NOT delegate when
26
+
27
+ - The task is small enough that spawning a context costs more than it saves.
28
+ - The main thread needs the **full detail** anyway (delegating then re-reading is pure overhead).
29
+ - You'd spawn many agents that each re-load the same large context — that multiplies tokens, the
30
+ opposite of the goal.
31
+
32
+ ## Cross-harness delegation
33
+
34
+ The same logic extends across harnesses: route bulk scaffolding/boilerplate to a cheaper or
35
+ execution-oriented channel and keep architecture, review, and validation on the main one. Suppress
36
+ the delegated channel's verbose thinking/output from re-entering the main context. Pair with
37
+ [[scaffold-fast]] for the delegated bulk work and [[compact-handoff]] for what the subagent returns.
38
+
39
+ ## Rule of thumb
40
+
41
+ Delegate to **shrink** what the main context must hold, never to **duplicate** it.
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: delta-response
3
+ description: This skill should be used for every response produced in a coding harness (Claude Code, Codex, OpenCode, Pi) — not only when explicitly requested. Enforces terse, structured, information-dense output instead of narrated verbosity, to cut output-token cost. Part of the HarnessTrim token-economy stack (see delta-response/references/examples.md for before/after samples).
4
+ version: 0.1.0
5
+ license: MIT
6
+ ---
7
+
8
+ # delta-response
9
+
10
+ Say the delta, not the story. Output tokens are billed the same as input tokens — verbosity is not free narration, it's cost with no signal.
11
+
12
+ ## Rules
13
+
14
+ 1. **Lead with the result.** State the answer, the change, or the finding first. Don't restate the request or narrate intent ("I will now check...", "Let me look at...") before acting — just act, then report what you found.
15
+ 2. **No filler.** Cut greetings, hedges, and meta-commentary ("Great question!", "As you can see,", "I hope this helps"). Every sentence must carry information the reader doesn't already have.
16
+ 3. **Prefer lists/tables over prose** when reporting more than two related facts.
17
+ 4. **One-line status updates**, not paragraphs, for intermediate progress ("Ran tests — 2 failures in auth.spec.ts", not three sentences describing the act of running tests).
18
+ 5. **End-of-task summary: 1–2 sentences max** — what changed, what's next. No recap of steps already visible in the transcript/diff.
19
+ 6. **Don't over-explain obvious code.** Well-named identifiers and a visible diff already communicate *what*; only add prose for non-obvious *why*.
20
+ 7. **Match length to the question.** A yes/no question gets a direct answer, not headers and sections.
21
+
22
+ ## When NOT to compress
23
+
24
+ Don't cut content that changes a decision: trade-offs the user must choose between, a warning about a risky/irreversible action, or a caveat that changes whether the result can be trusted. Terseness is about removing redundant words, not removing decision-relevant information.
25
+
26
+ See `references/examples.md` for verbose-vs-terse rewrites.
@@ -0,0 +1,33 @@
1
+ # delta-response — before/after examples
2
+
3
+ **Task: "run the tests"**
4
+
5
+ Verbose (avoid):
6
+ > I'll go ahead and run the test suite now to check if everything is passing correctly.
7
+ > [runs tests]
8
+ > Great, I've run the tests for you! Here's what I found: the test suite completed and it
9
+ > looks like there were a couple of failures. Let me break those down for you in detail...
10
+
11
+ Terse (target):
12
+ > Ran tests — 2 failures: `auth.spec.ts:42` (timeout), `billing.spec.ts:110` (assertion mismatch).
13
+
14
+ **Task: "what does this function do?"**
15
+
16
+ Verbose (avoid):
17
+ > So looking at this function, what we can see here is that it takes an input parameter
18
+ > called `items`, and then what it does is it iterates over each item in the array using
19
+ > a for loop, and for each one it checks a condition...
20
+
21
+ Terse (target):
22
+ > Filters `items` to those with `active: true`, then sorts by `createdAt` descending.
23
+
24
+ **Task: "fix the bug and tell me what you did"**
25
+
26
+ Verbose (avoid):
27
+ > I identified the root cause of the issue. The problem was in the validation logic.
28
+ > I've now updated the code to fix this. Here's a summary of the changes I made: first,
29
+ > I changed the regex pattern, then I added a null check, and finally I updated the tests
30
+ > to reflect these changes. Let me know if you have any questions!
31
+
32
+ Terse (target):
33
+ > Fixed: regex in `validate.ts:18` didn't allow `+` in emails; added a null guard for empty input. Tests updated.
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: review-delta
3
+ description: This skill should be used when asked to review code, review a pull request, or review a diff. Focuses the review on problems worth fixing rather than restating or summarizing what changed. Part of the HarnessTrim token-economy stack.
4
+ version: 0.1.0
5
+ license: MIT
6
+ ---
7
+
8
+ # review-delta
9
+
10
+ A code review that restates the diff wastes tokens telling the reader something the diff already
11
+ shows. Review for *problems*, not for *content*.
12
+
13
+ ## Rules
14
+
15
+ 1. **Don't summarize what changed file-by-file.** The diff is already visible; narrating
16
+ "this file now does X, that file now does Y" adds no information.
17
+ 2. **Only report actual findings**: correctness bugs, security issues, missed edge cases,
18
+ inconsistent-with-the-rest-of-the-codebase patterns, risky assumptions. If a file has nothing
19
+ wrong, say nothing about it — don't write "looks good" for every clean file.
20
+ 3. **Rank findings by severity**, most important first.
21
+ 4. **Cite `file:line`** for every finding so it's actionable, not abstract.
22
+ 5. **One or two sentences per finding**: what's wrong and the concrete failure scenario
23
+ (input/state that breaks), plus a fix suggestion only if it's non-obvious.
24
+ 6. **If there are zero findings, say so in one line.** Don't pad a clean review to look thorough.
25
+
26
+ ## Failure scenario, not just a label
27
+
28
+ "This could be a null pointer issue" is not a finding. "`user` can be `undefined` when
29
+ `fetchUser` 404s (see `api.ts:12`), and `line 40` dereferences it without a check — crashes on
30
+ any unknown user id" is a finding: concrete state, concrete break.
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: scaffold-fast
3
+ description: This skill should be used for mechanical, low-novelty coding work — generating boilerplate, wiring up a component from an existing pattern, adding a CRUD endpoint like the others, writing repetitive tests, or applying a rote transformation across files. It keeps reasoning effort low and output terse for work whose shape is already decided. Part of the HarnessTrim token-economy stack; the lean-scaffold preset pairs it with low reasoning effort.
4
+ version: 0.1.0
5
+ license: MIT
6
+ ---
7
+
8
+ # scaffold-fast
9
+
10
+ When the *shape* of the work is already decided and the task is to produce more of it, spend tokens
11
+ on the code, not on deliberation. Deep reasoning on rote work is wasted budget.
12
+
13
+ ## When this applies
14
+
15
+ - Boilerplate: config files, DTOs, barrel exports, index files.
16
+ - "Same as the others" work: a new endpoint/component/model that mirrors existing ones.
17
+ - Repetitive tests following an established pattern.
18
+ - Mechanical transforms: rename a symbol across files, migrate a call signature, reformat.
19
+
20
+ ## Rules
21
+
22
+ 1. **Copy the existing pattern, don't reinvent it.** Find the nearest sibling (the last component,
23
+ the adjacent endpoint) and match its structure, naming, imports, and error handling exactly.
24
+ 2. **Minimal reasoning.** Don't weigh architectural alternatives for work whose design is settled —
25
+ if you find yourself deliberating, that's a signal the task is *not* scaffolding and this skill
26
+ doesn't apply.
27
+ 3. **Terse output.** State what was generated in one line; the diff shows the rest. No walkthrough.
28
+ 4. **Don't gold-plate.** No speculative abstraction, options, or config for needs nobody stated.
29
+ 5. **Batch mechanical edits** rather than narrating each one.
30
+
31
+ ## When to STOP and escalate
32
+
33
+ If the "boilerplate" turns out to require a real decision — an unclear data model, a security
34
+ boundary, an ambiguous requirement — stop scaffolding and surface the decision. Fast is for settled
35
+ work, not for guessing past ambiguity. See [[delegate-bulk]] when the volume itself is the problem.