Agentic GitHub Workflow Reference
These are the GitHub Actions definitions removed from this repository. Recreate their schedules, event filters, permissions, setup steps, prompts, and follow-up behavior in the external orchestrator.
.github/workflows/abstraction-improver.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Abstraction Improver
# Flattens abstractions that carry no variation: an interface with one
# implementation, a factory that returns one thing, a wrapper that only
# forwards, a barrel that only re-exports, a type parameter used once.
#
# Each of them costs a hop when reading and a file when navigating, and pays for
# it only if a second implementation ever arrives. This routine removes the ones
# where it did not. The seams that carry real variation — LLM providers, storage
# backends, vector stores, execution surfaces — are explicitly out of scope.
on:
schedule:
- cron: "30 8 * * *" # Daily at 08:30 UTC
workflow_dispatch:
jobs:
abstraction-improver:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Find abstractions with one implementation
id: scan
continue-on-error: true
shell: bash
run: |
node --input-type=module - <<'JS' > thin-abstractions.log
import { readFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
const files = execFileSync(
"bash",
[
"-c",
"git ls-files 'web/src/*.ts' 'web/src/*.tsx' 'electron/src/*.ts' " +
"'mobile/src/*.ts' 'mobile/src/*.tsx' 'packages/*/src/*.ts' " +
"| grep -vE '__tests__|\\.test\\.|\\.spec\\.|/generated/|\\.d\\.ts$'",
],
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024 },
)
.split("\n")
.filter(Boolean);
const sources = new Map(files.map((file) => [file, readFileSync(file, "utf8")]));
const corpus = [...sources.values()].join("\n");
const countRefs = (name) =>
(corpus.match(new RegExp(`\\b${name}\\b`, "g")) ?? []).length;
console.log("## Interfaces with exactly one implementer");
// A props/options/result type is a record shape, not an abstraction —
// it has nothing to flatten. What matters here is an interface that
// declares behavior and has one implementation of it.
const SHAPE_SUFFIX = /(Props|Options|Params|Result|Deps|Config|State|Snapshot|Payload|Args)$/;
for (const [file, source] of sources) {
for (const match of source.matchAll(
/export (?:interface|abstract class) ([A-Z][A-Za-z0-9]*)[^{]*\{([\s\S]*?)\n\}/g,
)) {
const [, name, body] = match;
if (SHAPE_SUFFIX.test(name)) continue;
const methods = (body.match(/^\s+[a-zA-Z][A-Za-z0-9]*(\??\s*\(|\??:\s*\()/gm) ?? []).length;
if (methods === 0) continue;
const implementers = (corpus.match(new RegExp(`(?:implements|extends) ${name}\\b`, "g")) ?? []).length;
if (implementers === 1) {
console.log(` ${file} ${name} methods=${methods} implementers=1 refs=${countRefs(name)}`);
}
}
}
console.log("");
console.log("## Barrel files that only re-export");
for (const [file, source] of sources) {
const lines = source.split("\n").filter((line) => line.trim() && !line.trim().startsWith("//"));
if (lines.length < 3) continue;
if (lines.every((line) => /^\s*export .*from ["']/.test(line))) {
// Importers of the barrel, by the directory it fronts. Counting
// the word "index" instead would match every occurrence of it in
// the repo and report a number that means nothing.
const dir = file.replace(/\/index\.tsx?$/, "").split("/").pop();
const importers = (corpus.match(new RegExp(`from "[^"]*/${dir}(/index)?"`, "g")) ?? []).length;
console.log(` ${file} ${lines.length} re-exports importers=${importers}`);
}
}
console.log("");
console.log("## Exported functions that only forward to one call");
for (const [file, source] of sources) {
// export const f = (a) => g(a) / export function f(a) { return g(a) }
for (const match of source.matchAll(
/export (?:const ([a-zA-Z][A-Za-z0-9]*) *(?::[^=]+)?= *(?:async )?\([^)]*\) *=> *([a-zA-Z][A-Za-z0-9.]*)\([^)]*\);?|function ([a-zA-Z][A-Za-z0-9]*)\([^)]*\)[^{]*\{\s*return ([a-zA-Z][A-Za-z0-9.]*)\([^)]*\);?\s*\})/g,
)) {
const name = match[1] ?? match[4];
const delegate = match[2] ?? match[5];
if (!name || name === delegate) continue;
console.log(` ${file} ${name} -> ${delegate} refs=${countRefs(name)}`);
}
}
console.log("");
console.log("## Single-method classes");
for (const [file, source] of sources) {
for (const match of source.matchAll(/export class ([A-Z][A-Za-z0-9]*)[^{]*\{([\s\S]*?)\n\}/g)) {
const [, name, body] = match;
const methods = (body.match(/^\s{2}(?:public |private |protected )?(?:async )?[a-zA-Z][A-Za-z0-9]*\s*\(/gm) ?? []).length;
if (methods === 1 && !/extends|implements/.test(match[0].slice(0, 120))) {
console.log(` ${file} ${name} methods=1 refs=${countRefs(name)}`);
}
}
}
JS
echo "## Thin Abstractions" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
head -70 thin-abstractions.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
# The scan prints its four section headers whatever it finds, so
# emptiness is measured by the indented findings under them.
FINDINGS=$(grep -c "^ " thin-abstractions.log || true)
echo "Findings: $FINDINGS" >> $GITHUB_STEP_SUMMARY
if [ "${FINDINGS:-0}" -gt 0 ]; then echo "THIN_FOUND=1" >> $GITHUB_ENV; else echo "THIN_FOUND=0" >> $GITHUB_ENV; fi
- name: Run quality checks baseline
if: env.THIN_FOUND == '1'
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
if: env.THIN_FOUND == '1'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, and prose throat-clearing."
prompt: |
# Flatten an Over-Engineered Abstraction
`thin-abstractions.log` groups candidates into four kinds:
interfaces with a single implementer, barrel files that only
re-export, exported functions that only forward to one call, and
classes with one method.
Read `.claude/skills/codebase-design/SKILL.md` for the vocabulary
this repo uses to talk about module depth. Prefer removing a shallow
layer over deepening it.
Pick **one** abstraction and remove the indirection.
## What to remove
- **Interface with one implementer** and no second one in sight:
delete the interface, use the concrete type. Keep it when it exists
to break a dependency cycle or to let tests substitute a fake —
check for both before deleting.
- **Wrapper that only forwards**: inline it at the call sites and
delete it. Keep it when it adapts types, narrows a surface
deliberately, or is a package's public entry point.
- **Barrel that only re-exports**: point the importers at the real
modules and delete the barrel. Keep a package's own `index.ts` —
that is its API.
- **Class with one method** that holds no state: make it a function.
- **Generic parameter used once**: replace it with the concrete type.
- **Config object with one caller** passing the same literal every
time: take the value as an argument.
## What to leave alone
These seams carry real variation and are load-bearing:
- `packages/runtime/src/providers/**` — one interface, many providers.
- Storage backends, vector stores, secret stores.
- `@nodetool-ai/execution`'s `ExecutionSession` facade — several
surfaces go through it on purpose
(`scripts/check-execution-boundary.mjs` enforces that).
- `web/src/components/ui_primitives/**` — the primitives layer is
mandatory by AGENTS.md, however thin an individual primitive looks.
- The protocol types shared by web, mobile and the backend.
- Anything a public API, a plugin, or a sandbox pack depends on.
An abstraction with one implementation **today** but an obvious
second one arriving (a provider, a backend, an OS) stays.
## Rules
- Behavior-preserving. No feature or bug-fix changes.
- Update every call site. Do not leave a deprecated alias behind.
- Do not replace one abstraction with another. The diff should be
net-negative in lines and in files.
- One abstraction per PR. Maximum 15 files.
- Do not touch CI workflow files.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if an abstraction PR
is already open.
## Verification
```bash
npm run typecheck && npm run lint && npm run test
```
Backend packages also need `npm run build:packages && npm run test:packages`.
## Submit
Create a PR titled "refactor: flatten <abstraction>" explaining what
the layer was for, why nothing needs it, and what now calls what.
- name: Post-change verification
if: always() && env.THIN_FOUND == '1'
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/abstraction-police.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Abstraction Police
# Enforces the layering AGENTS.md describes: the package dependency order, the
# `@nodetool-ai/<package>` import rule, no reaching into another package's
# `dist/` or `src/`, the ui_primitives boundary in the frontend, and the
# Electron main/renderer split.
#
# The repo already ships boundary checks — check:deps, check:circular,
# check:keys, check:coupled-deps, check:execution-boundary — and they run in
# `npm run check`. This runs them plus the greps for violations no script covers
# yet, and has the agent fix the layering rather than widen an allowlist.
on:
schedule:
- cron: "30 4 * * *" # Daily at 04:30 UTC
workflow_dispatch:
jobs:
abstraction-police:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Run the repo's boundary checks
id: checks
continue-on-error: true
shell: bash
run: |
echo "## Boundary Checks" >> $GITHUB_STEP_SUMMARY
: > layering-violations.log
VIOLATIONS=0
for check in check:deps check:circular check:keys check:coupled-deps check:execution-boundary; do
if npm run "$check" > "/tmp/$check.log" 2>&1; then
echo "- OK: $check" >> $GITHUB_STEP_SUMMARY
else
VIOLATIONS=1
echo "- FAIL: $check" >> $GITHUB_STEP_SUMMARY
{ echo "### npm run $check"; echo '```'; tail -60 "/tmp/$check.log"; echo '```'; echo; } >> layering-violations.log
fi
done
echo "SCRIPT_VIOLATIONS=$VIOLATIONS" >> $GITHUB_ENV
- name: Grep for violations no script covers
id: grep
continue-on-error: true
shell: bash
run: |
echo "## Import-Rule Violations" >> $GITHUB_STEP_SUMMARY
FOUND=0
record() {
local title="$1"; local file="$2"
if [ -s "$file" ]; then
FOUND=1
COUNT=$(wc -l < "$file" | tr -d ' ')
echo "- $title: $COUNT" >> $GITHUB_STEP_SUMMARY
{ echo "### $title"; echo '```'; head -40 "$file"; echo '```'; echo; } >> layering-violations.log
else
echo "- $title: 0" >> $GITHUB_STEP_SUMMARY
fi
}
# AGENTS.md: never import from dist/, and never reach into another
# package's src/ — inter-package imports go through the package name.
grep -rEn "from \"[^\"]*@nodetool-ai/[a-z-]+/(dist|src)/" \
--include="*.ts" --include="*.tsx" \
packages/*/src web/src electron/src mobile/src 2>/dev/null > /tmp/deep-imports.log || true
record "Deep imports into another package's dist/ or src/" /tmp/deep-imports.log
# Relative paths that climb out of one workspace into another.
grep -rEn "from \"(\.\./)+(packages|web|electron|mobile)/" \
--include="*.ts" --include="*.tsx" \
packages/*/src web/src electron/src mobile/src 2>/dev/null > /tmp/relative-crossing.log || true
record "Relative imports crossing a workspace boundary" /tmp/relative-crossing.log
# AGENTS.md: raw MUI is allowed only inside ui_primitives/ and editor_ui/.
grep -rEn "from \"@mui/(material|icons-material)" \
--include="*.tsx" --include="*.ts" web/src 2>/dev/null \
| grep -vE "web/src/components/(ui_primitives|editor_ui)/" > /tmp/raw-mui.log || true
record "Raw MUI imports outside ui_primitives/ and editor_ui/" /tmp/raw-mui.log
# Node builtins in the renderer: the frontend has no Node runtime, and
# reaching for one means the boundary was crossed somewhere upstream.
grep -rEn "from \"(node:)?(fs|path|child_process|os|net|worker_threads)\"" \
--include="*.ts" --include="*.tsx" web/src mobile/src 2>/dev/null > /tmp/node-builtins.log || true
record "Node builtins imported in a renderer surface" /tmp/node-builtins.log
# Electron: the renderer talks to the main process through the
# contextBridge preload, never through electron's own module.
grep -rEn "from \"electron\"" --include="*.ts" --include="*.tsx" \
electron/src/renderer 2>/dev/null > /tmp/electron-renderer.log || true
record "electron imported from the renderer" /tmp/electron-renderer.log
echo "GREP_VIOLATIONS=$FOUND" >> $GITHUB_ENV
- name: Decide whether there is anything to fix
run: |
if [ "$SCRIPT_VIOLATIONS" = "1" ] || [ "$GREP_VIOLATIONS" = "1" ]; then
echo "LAYERING_VIOLATIONS=1" >> $GITHUB_ENV
else
echo "LAYERING_VIOLATIONS=0" >> $GITHUB_ENV
fi
- name: Run quality checks baseline
if: env.LAYERING_VIOLATIONS == '1'
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
if: env.LAYERING_VIOLATIONS == '1'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, and prose throat-clearing."
prompt: |
# Fix Layering Violations
`layering-violations.log` holds the output of the repo's boundary
checks and the import greps that found something.
## The layering, from AGENTS.md
Package dependency order — imports go left to right, never back:
```
protocol → config → security → auth → storage
→ runtime → kernel → node-sdk → base-nodes
→ models → agents → chat → websocket ← cli
```
Plus:
- Inter-package imports use `@nodetool-ai/<package>`. Never
`dist/`, never another package's `src/`, never a relative path
that climbs out of the workspace.
- `web/` and `mobile/` are renderer surfaces: no Node builtins.
- Mobile shares exactly two packages: `@nodetool-ai/protocol` and
`@nodetool-ai/app-runtime`.
- Raw MUI imports belong only in `web/src/components/ui_primitives/`
and `editor_ui/`. Everything else uses the primitives.
- The Electron renderer reaches the main process through the
`contextBridge` preload API, never by importing `electron`.
- `WorkflowRunner` is constructed only in `@nodetool-ai/execution`
(`scripts/check-execution-boundary.mjs`).
## How to fix a violation
Fix the direction of the dependency, not the check:
- **Wrong-direction import** → move the shared type or helper down to
a package both sides already depend on (often `protocol` or
`config`), or invert the dependency by passing the value in.
- **Deep import** → export the symbol from the package's entry point
and import it by package name. If it is not meant to be public,
that is the real finding: say so and move the caller instead.
- **Relative crossing** → same fix, via the workspace package name.
Add the dependency to the importing package's `package.json`
(`npm run check:deps` verifies that).
- **Node builtin in a renderer** → move the work behind the API the
renderer already talks to (HTTP route, IPC handler, preload).
- **Raw MUI** → use the primitive, or add one to `ui_primitives/`
following its STRATEGY.md.
- **Cycle** → break it by extracting the shared piece into the lower
package. Do not paper over it with a dynamic import.
## Rules
- **Never widen an allowlist to make a check pass.** The grandfathered
list in `check-execution-boundary.mjs` may only shrink. If a
violation genuinely cannot be fixed now, leave it, and say why in
the PR body — do not add an entry.
- Do not delete or weaken a check script.
- Do not move code across a package boundary to hide a violation
unless the destination is where the code belongs on its own merits.
- One boundary per PR. Maximum 15 files.
- Do not touch CI workflow files.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a layering PR is
already open.
## Verification
```bash
npm run check:deps && npm run check:circular && npm run check:keys && \
npm run check:coupled-deps && npm run check:execution-boundary
npm run build:packages && npm run typecheck && npm run lint && npm run test
```
Re-run the grep that found your violation and show it returns nothing.
## Submit
Create a PR titled "refactor: fix <boundary> layering violation"
naming the rule, the violating imports, and the direction the
dependency now runs.
- name: Post-change verification
if: always() && env.LAYERING_VIOLATIONS == '1'
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/anti-slop-ratchet.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Anti-Slop Ratchet
# Drives (rule, tree) pairs of the anti-slop backlog to zero and ratchets what
# it wins. See AGENTS.md § anti-slop for the backlog/enforced config split.
#
# The measurement is the slow part: one oxlint invocation per tree, run three
# times over a full pass (scan, `:write`, `:check`). Budget for that, not for
# the edits.
on:
schedule:
- cron: "30 9 * * *" # Daily at 09:30 UTC
workflow_dispatch:
jobs:
anti-slop-ratchet:
runs-on: ubuntu-latest
timeout-minutes: 120
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Measure the backlog and pick targets
id: targets
run: |
npm run lint:anti-slop:targets > /tmp/anti-slop-targets.md
cat /tmp/anti-slop-targets.md >> $GITHUB_STEP_SUMMARY
# Read the count rather than infer it. An unreadable measurement must
# fail here: parsed as "0 pairs left" it would skip the agent and
# report a clean nightly, which is the one wrong answer available.
REMAINING=$(sed -n 's/^\([0-9]\{1,\}\) non-zero pairs remain.*/\1/p' /tmp/anti-slop-targets.md)
if [ -z "$REMAINING" ]; then
echo "Could not read the pair count from the measurement." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "REMAINING_PAIRS=$REMAINING" >> $GITHUB_ENV
echo "$REMAINING non-zero (rule, tree) pairs remain." >> $GITHUB_STEP_SUMMARY
# Which end of the backlog this run works. Decided here rather than
# in the prompt: the mode has to be legible in the run summary, and
# an agent asked to derive it from a run number is one arithmetic
# slip away from never picking the large pairs at all.
if [ $(( $ % 4 )) -eq 0 ]; then
PAIR_MODE=large
else
PAIR_MODE=cheap
fi
echo "PAIR_MODE=$PAIR_MODE" >> $GITHUB_ENV
echo "Target selection for this run: **$PAIR_MODE**." >> $GITHUB_STEP_SUMMARY
- name: Run quality checks baseline
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0; PKG_TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
npm run test:packages 2>&1 || PKG_TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
echo "PKG_TEST_PRE_EXIT=$PKG_TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
if: env.REMAINING_PAIRS != '0'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff."
prompt: |
# Drive anti-slop (rule, tree) pairs to zero
The vendored anti-slop Oxlint plugin runs through two configs: a
backlog (`.oxlintrc.anti-slop.json`) and an enforced config
(`.oxlintrc.anti-slop-enforced.json`) carrying every (rule, tree)
pair already at zero. Read AGENTS.md § anti-slop first — it explains
the split, the generated overrides, and which rules are holdouts.
Your job: take pairs from backlog to enforced, by fixing the code.
## Today's measurement
`npm run lint:anti-slop:targets` has already run. Read
`/tmp/anti-slop-targets.md` — it holds the per-rule table, the trees
closest to zero, and the cheapest remaining pairs. **Use it. Do not
re-derive these numbers**: counting findings off a raw `oxlint` run
silently mixes in oxlint's own default-rule diagnostics, which
report through the same channel and are not backlog findings.
To see the findings for one tree:
```bash
npx oxlint --config .oxlintrc.anti-slop.json --format json packages/<name>/src \
| jq '.diagnostics[] | select(.code | startswith("anti-slop"))'
```
## Picking scope
**This run's target selection is `$`** — one run
in four is `large`, the rest are `cheap`.
**`cheap`:** prefer finishing one whole tree over scattering single
fixes. One typecheck and one test run then cover several pairs at
once, and a tree at zero on every rule cannot regress on any of
them. Take the top tree from the "closest to zero" table that you
can finish; if none is finishable, take cheap pairs from one
package. Keep it to one package (plus any caller you must update).
**`large`:** ignore both tables above and take the top row of
"Largest (rule, tree) pairs" that no open PR is already working.
Those pairs are never cheap and their trees are never nearly-done,
so nothing in `cheap` mode ever selects them — `web` sat at
thousands of findings and had a separate nightly workflow built for
it, which these runs replace.
A large pair does not finish in one PR, so bound it the same way:
one directory of the tree, every finding in that directory fixed,
the pair's before/after count in the PR body. It ratchets only when
the whole pair reaches zero — say so in the PR rather than implying
the win is held.
When the pair is `require-safety-comment-for-type-assertion` in
`web`, `electron` or `mobile`, expect **test files**: those trees
have no `as any` in production code at all. Type the double instead
of commenting the assertion — `selectorOver(state)` for a mocked
zustand hook, a real fixture type or a factory in
`web/src/test-utils/doubles.ts` for a partial object, a declared
member instead of `(global as any).fetch = …`. A deliberately
invalid fixture keeps its assertion **and** gets the `SAFETY:`
comment saying which invalid input it is proving the code survives.
Never weaken an assertion to make a fixture compile: typing the
double is how `ChatView` was found asserting against `PlanningUpdate`
payloads the app cannot produce.
## What a real fix looks like
Fix the typing, do not annotate around the finding. Four shapes
cover nearly everything:
1. **Name the value crossing a boundary.** A function taking
`Record<string, unknown>` or `unknown` usually has a real
contract nobody wrote down. Write it, and make the caller meet it
at its own boundary.
2. **Give a `typeof` narrowing a predicate that takes a domain
type.** `no-runtime-typeof` allows `typeof` inside a function
returning `v is T`. But a predicate taking bare `unknown` just
moves the finding into `no-unknown-parameters` — take `T`, a
named union, or an indexed access like `Foo["bar"]` instead.
3. **Delete guards that check what the type already declares.** A
`typeof params.seconds === "number"` against a declared
`seconds?: number` is a defensive check on a trusted path and
usually collapses into `??`.
4. **Point a type at its owner.** `params?: RunWorkflowOptions["params"]`
beats a second `Record<string, unknown>` that can drift from it.
5. **Write down the return type the body already has.** For
`no-implicit-return-type`, read what the function returns and
annotate it — or annotate the binding, which is the same answer.
If the inferred type is unpleasant to write, that is the finding
talking: name it. Do not annotate `unknown` to silence this rule;
that just moves the finding to `no-unknown-returns`.
A genuine assertion that cannot go away keeps a `SAFETY:` comment
stating the invariant TypeScript cannot express. Note the rule's
comment walk stops at the `VariableDeclaration`, so for an exported
const the comment goes inside, before the asserted expression.
## Rules
- **Never** weaken the plugin, the backlog config, or a rule's
options to make findings disappear. Do not add
`oxlint-disable` comments. The only legitimate way a finding goes
away is the code getting better.
- Do not change runtime behavior. Where a check looks dead, confirm
it against tests before removing it — some are load-bearing for
injected stubs.
- Do not touch CI workflow files.
- Respect the documented holdouts in AGENTS.md; if a pair is a
holdout, say so in the PR and pick another.
## Before starting
Run `gh pr list --state open --limit 30` and skip the run entirely
if an anti-slop ratchet PR is already open.
## Ratchet what you win
After the code is fixed, regenerate the enforced overrides from a
fresh measurement and commit the result:
```bash
npm run lint:anti-slop:write
```
Then update the backlog table, the pair count, and the
list of trees at zero on all eight rules in AGENTS.md to match what
it printed. Those numbers are checked, so they must not be guessed.
## Prove the new ratchet can fail
A check that has only ever been green is indistinguishable from one
that examines nothing. For one pair you just ratcheted,
re-introduce the violation, confirm `npm run lint:anti-slop:enforced`
reports it and exits non-zero, then revert and confirm it is green
again. Put both observations in the PR body. If the enforced lint
stays green with the violation present, the ratchet did not take —
stop and report that instead of opening the PR.
## Verification
All of these must pass, and `:check` must agree with the config you
committed:
```bash
npm run typecheck && npm run lint && npm run test && npm run test:packages
npm run lint:anti-slop:check
```
`electron/tsconfig.json` excludes `src/__tests__` and `**/*.test.ts`,
so `npm run typecheck` reads none of the electron test files. If you
edited one, only `npm run test` (through ts-jest) checks it — a green
typecheck there proves nothing.
Some suites fail on a runner for reasons that predate your change.
If one does, confirm it against a clean tree before calling it
unrelated, and say which in the PR.
## Submit
Open a PR titled "refactor(lint): drive <tree> to zero on <n> anti-slop rules".
In the body give the before/after pair count and backlog total, what
each fix named rather than annotated, the failure you induced to
prove the ratchet works, and anything you deliberately left as a
holdout.
- name: Post-change verification
if: always() && env.REMAINING_PAIRS != '0'
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0; PKG_TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
npm run test:packages 2>&1 || PKG_TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$PKG_TEST_PRE_EXIT" -eq 0 ] && [ $PKG_TEST_EXIT -ne 0 ]; then
echo "New package test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
# The enforced config is generated from a measurement. A pair driven
# to zero without `:write` being run leaves it unratcheted, and a
# regression leaves it claiming a zero that is gone — both are drift,
# and both look like success without this.
npm run lint:anti-slop:check
.github/workflows/claude-code-review.yml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Claude Code Review
# One review per PR, not one per push. The review is an `npm ci` plus a long
# Opus run, so `synchronize` held a runner slot on every push while the account
# job cap was already saturated. Re-review on demand with `@claude review` in a
# PR comment (claude.yml).
on:
pull_request:
types: [opened, ready_for_review]
# A second `opened`/`ready_for_review` supersedes the review still running.
concurrency:
group: claude-review-$
cancel-in-progress: true
jobs:
claude-review:
# `opened` fires for drafts too; review them when they are marked ready.
if: $
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review $/pull/$'
claude_args: |
--model claude-opus-5
--append-system-prompt "When reviewing this PR, also flag any AI-slop introduced by the change using the unslop skill (.claude/skills/unslop/SKILL.md) — speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, useEffect+fetch instead of useQuery, and prose throat-clearing in the PR body."
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
.github/workflows/claude.yml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: write # Required so Claude can push commits back to the branch (works around extraheader-override issue in claude-code-action that doesn't cover values from includeIf-included git configs used by actions/checkout)
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
claude_args: |
--model claude-opus-5
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, useEffect+fetch instead of useQuery, and prose throat-clearing."
.github/workflows/crash-fuzzer.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Crash Fuzzer
# Feeds malformed documents to the surfaces that parse untrusted input —
# `validateGraph`, the Code-node analyzer, the node property validator — and
# reports the cases that crash instead of failing cleanly.
#
# A validator is allowed to reject its input. It is not allowed to throw a
# TypeError, blow the stack, or hang: those reach users as an unhandled
# exception in the editor, the CLI, or the server. The oracle is therefore the
# *shape* of the failure, never a non-zero exit code.
#
# The fuzzer runs in two legs, both built on the repo's Stryker setup:
#
# sweep — the crash corpus at a rotating seed. Finds crashes on inputs no
# previous run has produced. Fast (seconds), and it is the same
# suite that runs on every PR at the pinned seed.
# mutants — Stryker mutates the parser sources and runs the crash corpus as
# its only test suite. A surviving mutant is a branch no fuzzed
# document distinguishes: the corpus never reaches it, or reaching
# it changes nothing the oracle asserts. Either way it is a hole,
# and it is where the next crash will come from.
#
# Corpus and oracle live in `packages/node-sdk/tests/fuzz/`; the Stryker leg is
# `packages/node-sdk/stryker.crash.config.json`.
on:
schedule:
- cron: "0 5 * * *" # Daily at 05:00 UTC
workflow_dispatch:
inputs:
seed:
description: "Sweep PRNG seed (default: the run number) — reuse to replay a crash"
required: false
default: ""
mutants:
description: "Mutants to generate per seed document in the sweep"
required: false
default: "40"
skip_stryker:
description: "Run the sweep only (skip the mutation-testing leg)"
type: boolean
required: false
default: false
jobs:
crash-fuzzer:
runs-on: ubuntu-latest
timeout-minutes: 120
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
defaults:
run:
working-directory: packages/node-sdk
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install workspace dependencies
working-directory: .
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
# The corpus is hermetic — it builds its own registry from seed
# documents and imports the validators from source — so no
# build:packages is needed.
- name: Sweep fresh inputs
id: sweep
continue-on-error: true
env:
FUZZ_SEED: $
FUZZ_COUNT: $
run: |
echo "## Crash Fuzzer" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Sweep seed \`$FUZZ_SEED\`, $FUZZ_COUNT mutants per seed document." >> "$GITHUB_STEP_SUMMARY"
echo "FUZZ_SEED=$FUZZ_SEED" >> "$GITHUB_ENV"
echo "FUZZ_COUNT=$FUZZ_COUNT" >> "$GITHUB_ENV"
# Overwritten by the mutation leg; a skipped leg must not read as
# "survivors found".
echo "FUZZ_SURVIVORS=0" >> "$GITHUB_ENV"
npm run test:crash-fuzz -- --reporter=default --reporter=json \
--outputFile=../../crash-sweep.json 2>&1 | tee ../../crash-sweep.log
- name: Report the sweep
if: always()
run: |
if [ "$" = "success" ]; then
echo "✅ No crash on this seed." >> "$GITHUB_STEP_SUMMARY"
echo "FUZZ_CRASHED=0" >> "$GITHUB_ENV"
else
echo "❌ The sweep found a crash. Replay it by dispatching this workflow with seed \`$FUZZ_SEED\` and $FUZZ_COUNT mutants, or locally:" >> "$GITHUB_STEP_SUMMARY"
echo '```bash' >> "$GITHUB_STEP_SUMMARY"
echo "FUZZ_SEED=$FUZZ_SEED FUZZ_COUNT=$FUZZ_COUNT npm run test:crash-fuzz --workspace=packages/node-sdk" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "FUZZ_CRASHED=1" >> "$GITHUB_ENV"
fi
- name: Mutation-test the parsers against the crash corpus
if: github.event.inputs.skip_stryker != 'true'
continue-on-error: true
run: npm run test:mutation:crash
- name: Report the mutation score
if: always() && github.event.inputs.skip_stryker != 'true'
working-directory: .
run: |
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "### 🧬 Parser mutants killed by the crash corpus" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
node scripts/mutation-score.mjs --report-dir reports/crash-fuzz node-sdk >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
node scripts/crash-fuzz-survivors.mjs --limit 40 >> "$GITHUB_STEP_SUMMARY"
node scripts/crash-fuzz-survivors.mjs --json > crash-survivors.json
SURVIVORS=$(node -e 'console.log(JSON.parse(require("fs").readFileSync("crash-survivors.json","utf8")).length)')
echo "FUZZ_SURVIVORS=$SURVIVORS" >> "$GITHUB_ENV"
- name: Upload the crash corpus report
if: always()
uses: actions/upload-artifact@v7
with:
name: crash-fuzz-report
path: |
crash-sweep.log
crash-sweep.json
crash-survivors.json
packages/node-sdk/reports/crash-fuzz/mutation.json
if-no-files-found: warn
retention-days: 30
- name: Run quality checks baseline
if: env.FUZZ_CRASHED == '1' || env.FUZZ_SURVIVORS != '0'
id: pre-check
continue-on-error: true
working-directory: .
run: |
LINT_EXIT=0; TEST_EXIT=0
npm run lint 2>&1 || LINT_EXIT=1
npm run test:packages 2>&1 || TEST_EXIT=1
echo "LINT_PRE_EXIT=$LINT_EXIT" >> "$GITHUB_ENV"
echo "TEST_PRE_EXIT=$TEST_EXIT" >> "$GITHUB_ENV"
- name: Run Claude Code
if: env.FUZZ_CRASHED == '1' || env.FUZZ_SURVIVORS != '0'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, and prose throat-clearing."
prompt: |
# Crash Fuzzer Follow-up
The crash fuzzer runs a mutated-document corpus
(`packages/node-sdk/tests/fuzz/`) against `validateGraph`, the
Code-node analyzer, and the node property validator, then uses that
corpus as Stryker's test oracle over the parser sources.
This run: sweep crashed = `$` at seed
`$` (`$` mutants per seed
document); `$` parser mutants survived the
corpus.
## Priority 1 — a crash, if the sweep failed
`crash-sweep.log` holds the failing assertion. Reproduce it first:
```bash
FUZZ_SEED=$ FUZZ_COUNT=$ \
npm run test:crash-fuzz --workspace=packages/node-sdk
```
Rejecting bad input is correct; crashing on it is not. A failure
here is an unhandled `TypeError` / `RangeError`, a stack overflow, a
report whose `counts` disagree with its `issues`, or a document that
blew the time budget.
- Read the stack trace to the frame in NodeTool's own code and fix
it **there** — not at the call site, and not with a catch-all
wrapper. `try { … } catch { return [] }` around the crashing call
hides the bug and is not acceptable.
- Do not make the validator accept invalid input to stop it
crashing. The correct outcome is a reported issue, not a pass.
- Add the minimal triggering input to
`packages/node-sdk/tests/fuzz/seeds.ts` as a literal so the corpus
covers it at every seed from now on, and re-run at the pinned seed
to refresh the snapshots (`npm run test:crash-fuzz -- -u`).
## Priority 2 — surviving mutants
`crash-survivors.json` lists parser mutants the corpus failed to
kill, each with file, line, mutator, and replacement. Each one is a
branch the corpus does not distinguish. For the ones that matter:
- Add a seed document or a mutation operator that reaches the branch
and makes the mutant observable. New operators go in
`tests/fuzz/corpus.ts`; new documents in `tests/fuzz/seeds.ts`.
- A mutant that is genuinely equivalent (no input can tell the two
versions apart) gets a `// Stryker disable next-line <Mutator>:
<why>` comment naming the reason — the same convention
`src/validation.ts` already uses. Do not disable one you merely
found hard to kill.
- Prefer widening the corpus over adding hand-written unit tests:
the point is that fuzzed input reaches the branch.
## Rules
- One concern per commit. Maximum 6 fixes per PR — several
survivors usually share one root cause, so group them.
- Do not touch the CI workflow files.
- Do not lower `stryker.crash.config.json` thresholds or narrow its
`mutate` list to make the number look better.
- Check `gh pr list --state open --limit 20` first and skip anything
an open fuzzer PR already covers.
## Verification
```bash
npm run lint
npm run test:packages
npm run test:crash-fuzz --workspace=packages/node-sdk
npm run test:mutation:crash --workspace=packages/node-sdk
```
The mutation score must not drop.
## Submit
Open a PR titled "fix: crash on malformed <input kind>" (Priority 1)
or "test: close crash-corpus gaps in <file>" (Priority 2). List each
crash or survivor, the stack frame or branch behind it, and what now
covers it.
- name: Post-change verification
if: always() && (env.FUZZ_CRASHED == '1' || env.FUZZ_SURVIVORS != '0')
working-directory: .
run: |
LINT_EXIT=0; TEST_EXIT=0
npm run lint 2>&1 || LINT_EXIT=1
npm run test:packages 2>&1 || TEST_EXIT=1
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> "$GITHUB_STEP_SUMMARY"; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> "$GITHUB_STEP_SUMMARY"; exit 1
fi
.github/workflows/dead-code-cleanup.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Dead Code Cleanup
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
jobs:
dead-code-cleanup:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Run quality checks baseline
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, useEffect+fetch instead of useQuery, and prose throat-clearing."
prompt: |
# Dead Code Cleanup
Find and remove dead code across the NodeTool codebase.
## Scope
- `/web` — React frontend
- `/electron` — desktop app (Electron)
- `/mobile` — React Native app
## What to find and remove
**Unused exports**: Functions, classes, constants, and types that are exported but never imported anywhere in the project. Use grep/ripgrep to verify zero consumers before removing.
**Unused imports**: Imports that are not referenced in the file. The linter may catch some, but check for type-only imports and namespace imports too.
**Unreachable code**: Code after unconditional `return`, `throw`, `break`, or `continue` statements.
**Unused local variables and parameters**: Variables assigned but never read. Be careful with destructuring patterns — only remove if truly unused.
**Commented-out code blocks**: Large blocks of commented-out code (5+ lines) that are not documentation. Leave short inline comments that explain why something is done.
**Empty/no-op functions**: Functions with empty bodies or that only return undefined, unless they serve as intentional callbacks or interface implementations.
## Rules
- Only remove genuinely dead code. Do not remove code that is used dynamically (e.g. string-based lookups, plugin registries, event handlers registered by name).
- Do not remove `@ts-expect-error` / `@ts-ignore` directives.
- Do not remove `test.skip` / `describe.skipIf` / `test.fixme` markers.
- Do not touch CI workflow files.
- Keep changes focused — maximum 15 files per PR.
- Verify each removal with grep to confirm zero references.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a dead code cleanup PR is already open.
## Verification
After cleanup, all checks must still pass:
```bash
npm run typecheck && npm run lint && npm run test
```
## Submit
Create a PR titled "chore: remove dead code" listing each removed item and why it's unused.
- name: Post-change verification
if: always()
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/dependency-cleanup.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Dependency Cleanup
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
jobs:
dependency-cleanup:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Run quality checks baseline
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, useEffect+fetch instead of useQuery, and prose throat-clearing."
prompt: |
# Dependency Cleanup
Find and clean up unnecessary or outdated dependencies in the NodeTool codebase.
## Scope
- `/web/package.json`
- `/electron/package.json`
- `/mobile/package.json`
## What to fix
**Unused dependencies**: For each dependency in `dependencies` and `devDependencies`, search the source code to verify it's actually imported/required. Remove any that have zero imports. Be careful with:
- Vite/build plugins (referenced in config files, not source)
- Type packages (`@types/*`) — check if the parent package is used
- PostCSS/Babel/ESLint plugins (referenced in config files)
- Packages used only in scripts defined in package.json
**Duplicate dependencies**: Packages that appear in multiple workspaces with different versions. Align to a single version where possible.
**Minor/patch updates**: Update dependencies to their latest minor/patch version (not major). Run `npm outdated` to find candidates. Only update if:
- The update is within the same major version
- Tests still pass after the update
**Misplaced dependencies**: Runtime packages in `devDependencies` or dev-only packages in `dependencies`. Move them to the correct section.
## Rules
- Do not bump major versions. Major upgrades need manual review.
- Do not remove packages you cannot verify are unused — search source, configs, and scripts.
- Do not touch version pins that have comments explaining why they're pinned.
- Do not touch CI workflow files.
- Run `npm ci` after any package.json changes to verify the lockfile is consistent.
- Keep changes focused — one PR per cleanup run.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a dependency cleanup PR is already open.
## Verification
After changes, reinstall and verify:
```bash
npm ci && cd mobile && npm ci && cd ..
npm run typecheck && npm run lint && npm run test
```
## Submit
Create a PR titled "chore: clean up dependencies" listing each package added/removed/updated and why.
- name: Post-change verification
if: always()
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/docs-completeness.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Documentation Completeness
# Features ship faster than their docs. A CLI command lands, an HTTP route is
# added, a package appears, an env var starts gating behavior — and nothing in
# CI notices that no page mentions any of it.
#
# This job inventories the surfaces users touch (CLI commands, HTTP routes,
# NODETOOL_* env vars, workspace packages), diffs each inventory against what the
# docs mention, and asks Claude to document the gaps it can verify from the code.
# Markdown only. Opens a PR, never auto-merges.
on:
schedule:
- cron: "40 5 * * *" # Daily at 05:40 UTC
workflow_dispatch:
jobs:
docs-completeness:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
# The CLI enumerates its commands from the built node registry.
- name: Build packages
run: npm run build:packages
- name: Inventory documented vs. undocumented surfaces
id: coverage
continue-on-error: true
run: |
set -uo pipefail
mkdir -p docs-facts
DOCS="docs README.md AGENTS.md"
mentioned() {
grep -rqI -- "$1" $DOCS 2>/dev/null
}
# --- CLI commands -------------------------------------------------
npm run --silent dev:nodetool -- --help 2>/dev/null \
| awk '/^Commands:/{f=1;next} f && /^ [a-z]/{print $1}' \
| sort -u > docs-facts/cli-commands.txt || true
: > docs-facts/undocumented-cli.txt
while read -r cmd; do
[ -n "$cmd" ] || continue
mentioned "nodetool $cmd" || echo "$cmd" >> docs-facts/undocumented-cli.txt
done < docs-facts/cli-commands.txt
# --- HTTP routes --------------------------------------------------
grep -rhoE '\.(get|post|put|patch|delete)\(\s*[`"]/[^`"]*' \
packages/websocket/src --include="*.ts" 2>/dev/null \
| sed -E 's/^[^`"]*[`"]//' | sort -u > docs-facts/http-routes.txt || true
: > docs-facts/undocumented-routes.txt
while read -r route; do
[ -n "$route" ] || continue
mentioned "$route" || echo "$route" >> docs-facts/undocumented-routes.txt
done < docs-facts/http-routes.txt
# --- NODETOOL_* environment variables ------------------------------
grep -rhoE 'NODETOOL_[A-Z0-9_]+' packages web/src electron/src \
--include="*.ts" --include="*.tsx" --include="*.mjs" 2>/dev/null \
| sort -u > docs-facts/env-vars.txt || true
: > docs-facts/undocumented-env.txt
while read -r var; do
[ -n "$var" ] || continue
mentioned "$var" || echo "$var" >> docs-facts/undocumented-env.txt
done < docs-facts/env-vars.txt
# --- Packages without a README -------------------------------------
: > docs-facts/packages-without-readme.txt
for pkg in packages/*/; do
[ -f "$pkg/package.json" ] || continue
[ -f "$pkg/README.md" ] || echo "${pkg%/}" >> docs-facts/packages-without-readme.txt
done
CLI_GAPS=$(wc -l < docs-facts/undocumented-cli.txt)
ROUTE_GAPS=$(wc -l < docs-facts/undocumented-routes.txt)
ENV_GAPS=$(wc -l < docs-facts/undocumented-env.txt)
PKG_GAPS=$(wc -l < docs-facts/packages-without-readme.txt)
TOTAL=$((CLI_GAPS + ROUTE_GAPS + ENV_GAPS + PKG_GAPS))
{
echo "## Documentation Completeness"
echo ""
echo "| Surface | Undocumented |"
echo "|---|---|"
echo "| CLI commands | $CLI_GAPS |"
echo "| HTTP routes | $ROUTE_GAPS |"
echo "| NODETOOL_* env vars | $ENV_GAPS |"
echo "| Packages without README | $PKG_GAPS |"
} >> $GITHUB_STEP_SUMMARY
echo "DOC_GAPS=$TOTAL" >> $GITHUB_ENV
- name: Record base commit
run: echo "BASE_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
- name: Run Claude Code
if: env.DOC_GAPS != '0'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "All prose you write follows docs/WRITING_STYLE.md: concise, concrete, no AI slop (leverage, seamless, robust, comprehensive, it's worth noting, rule-of-three padding). Document what the code does, in the voice the surrounding docs already use."
prompt: |
# Documentation Completeness
Document the NodeTool surfaces that no page currently mentions.
## Gaps found for you
- `docs-facts/undocumented-cli.txt` — CLI commands no doc mentions
- `docs-facts/undocumented-routes.txt` — HTTP routes no doc mentions
- `docs-facts/undocumented-env.txt` — `NODETOOL_*` env vars no doc mentions
- `docs-facts/packages-without-readme.txt` — packages with no README
These lists are a plain-text grep, so they over-report: a route
documented as `/api/workflows/:id` will not match a grep for
`/api/workflows/:workflow_id`. Confirm each gap is real before writing
anything, and drop the ones that are already covered under another
spelling.
## Where things belong
Read the existing page first and extend it in its own structure rather
than starting a new page:
- CLI commands → `docs/cli.md`, and the CLI section of `AGENTS.md`
- HTTP routes → `docs/api-reference.md` / `docs/api.md`
- Env vars → `docs/configuration.md`
- A package's own behavior → that package's `README.md`
A new page is justified only when a subsystem has no home at all. If you
add one, link it from `docs/index.md` so it is reachable.
## What to write
For each gap: what it is, when someone reaches for it, and a working
example — a real command with real flags, a real request/response shape,
a real env var value. Derive every detail from the code (`--help` output,
the route handler, the place the env var is read). Never invent a flag,
a default, or a response field.
Internal-only surfaces do not need user docs. If a route is internal
plumbing or an env var is a test hook, say so in the PR body and skip it
rather than writing a page nobody needs.
## Rules
- **Markdown only.** Do not change code to match documentation.
- Match the surrounding page's heading depth, code-fence style, and voice.
- Do not rewrite documentation that already exists — this pass adds what
is missing.
- Do not touch CI workflow files.
- Keep changes focused — at most 8 gaps per PR, prioritizing CLI commands
and HTTP routes (what users hit first) over package READMEs.
- Do not commit the `docs-facts/` directory.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a documentation
completeness PR is already open.
## Verification
Every command you document must run as written. Check flags against
`nodetool <command> --help`, and check that any relative link you add
resolves on disk.
## Submit
Create a PR titled "docs: document <surface>" whose body lists each gap
filled, where the documentation went, and where in the code you read the
behavior. List the gaps you deliberately skipped and why.
# A docs job that edits code has done something it was told not to. Catch it
# here rather than in review — the PR is already open by this point, so this
# step failing is the signal to close it.
- name: Guard — Markdown-only changes
if: always()
run: |
set -uo pipefail
rm -rf docs-facts
CHANGED=$( { git diff --name-only "${BASE_SHA:-HEAD}" HEAD; git status --porcelain | awk '{print $NF}'; } | sort -u )
CODE_CHANGES=$(echo "$CHANGED" | grep -v -E '\.(md|markdown)$' | grep -v '^$' || true)
if [ -n "$CODE_CHANGES" ]; then
echo "::error::Non-Markdown files were modified by a docs-only job:"
echo "$CODE_CHANGES"
exit 1
fi
.github/workflows/docs-correctness.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Documentation Correctness
# Docs drift silently: a flag gets renamed, a script disappears, a package moves,
# and the page describing it keeps saying the old thing. Nothing in CI catches
# that — `docs-ci.yml` proves the site builds and its links resolve, not that its
# sentences are true.
#
# This job collects ground truth from the repo itself (CLI help, npm scripts,
# workspace list, the week's code changes), hands it to Claude, and asks it to
# check the docs that describe those surfaces against what the code actually
# does. Markdown only — it never edits code to make a doc true. Opens a PR,
# never auto-merges.
on:
schedule:
- cron: "10 5 * * *" # Daily at 05:10 UTC
workflow_dispatch:
jobs:
docs-correctness:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
# The CLI loads the node registry from dist/ (decorator packages), so its
# help output is only complete after a build.
- name: Build packages
run: npm run build:packages
- name: Collect ground truth
id: facts
continue-on-error: true
run: |
set -uo pipefail
mkdir -p docs-facts
# Every CLI command's help text — the reference the CLI docs and
# AGENTS.md command blocks are supposed to match.
{
npm run --silent dev:nodetool -- --help
npm run --silent dev:nodetool -- --help 2>/dev/null \
| awk '/^Commands:/{f=1;next} f && /^ [a-z]/{print $1}' \
| sort -u \
| while read -r cmd; do
echo ""
echo "===== nodetool $cmd --help ====="
npm run --silent dev:nodetool -- "$cmd" --help 2>&1 || true
done
} > docs-facts/cli-help.txt 2>&1 || true
node -e 'const s=require("./package.json").scripts||{};for(const k of Object.keys(s).sort())console.log(k+" = "+s[k])' \
> docs-facts/npm-scripts.txt 2>/dev/null || true
node -e 'const {workspaces=[]}=require("./package.json");console.log(workspaces.join("\n"))' \
> docs-facts/workspaces.txt 2>/dev/null || true
ls -1 packages >> docs-facts/workspaces.txt 2>/dev/null || true
# Docs describing code that moved this week are the likeliest to be stale.
git log --since="7 days ago" --name-only --pretty=format:"--- %h %s" \
> docs-facts/recent-changes.txt 2>/dev/null || true
{
echo "## Documentation Correctness — ground truth"
echo ""
echo "- CLI help: $(wc -l < docs-facts/cli-help.txt 2>/dev/null || echo 0) lines"
echo "- npm scripts: $(wc -l < docs-facts/npm-scripts.txt 2>/dev/null || echo 0)"
echo "- Commits in the last 7 days: $(git log --since='7 days ago' --oneline | wc -l)"
} >> $GITHUB_STEP_SUMMARY
- name: Record base commit
run: echo "BASE_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
- name: Run Claude Code
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "All prose you write follows docs/WRITING_STYLE.md: concise, concrete, no AI slop (leverage, seamless, robust, comprehensive, it's worth noting, rule-of-three padding). Fix slop you pass in paragraphs you are already editing."
prompt: |
# Documentation Correctness
Check NodeTool's documentation against what the code actually does, and
fix the statements that are wrong.
## Ground truth collected for you
- `docs-facts/cli-help.txt` — every CLI command's real help output
- `docs-facts/npm-scripts.txt` — the npm scripts that actually exist
- `docs-facts/workspaces.txt` — the real workspace/package list
- `docs-facts/recent-changes.txt` — files changed in the last 7 days
Docs describing code that changed this week are the likeliest to be
stale. Start there, then widen.
## Scope
- `docs/**/*.md`
- `README.md`, `AGENTS.md`, and per-package `AGENTS.md` / `README.md`
## What counts as an error
**Commands that do not exist or take different flags.** Every `npm run …`
and `nodetool …` invocation in a doc must exist with the flags shown.
Check against `docs-facts/npm-scripts.txt` and `docs-facts/cli-help.txt`,
not memory.
**Paths and file names that no longer resolve.** Every path a doc names
(`packages/foo/src/bar.ts`, `web/src/components/…`, script names, config
files) must exist. Verify each with `ls`/`test -e`.
**Symbols that were renamed or removed.** Class, function, hook, store,
node type, and env var names quoted in prose must appear in the code.
Grep for each before trusting it.
**Descriptions contradicted by the code.** Defaults, ports, ordering,
required versions, return shapes, message formats. Read the code path
before rewriting the sentence.
**Internal links that point nowhere.** Relative Markdown links to files
or headings that no longer exist.
## Rules
- **Markdown only.** Never edit code to make a doc true — if a doc
describes better behavior than the code has, leave the code alone and
note it in the PR body instead of rewriting the doc into fiction.
- Verify every claim you change. If you cannot confirm what the correct
statement is, delete the wrong sentence rather than guessing a new one.
- Do not restructure, reword, or "improve" prose that is already correct.
This PR is a correction pass, not an editing pass.
- Do not touch CI workflow files.
- Do not add new documentation pages — `docs-completeness.yaml` covers gaps.
- Keep changes focused — maximum 15 files per PR.
- Do not commit the `docs-facts/` directory.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a documentation
correctness PR is already open.
## Submit
If you found nothing wrong, say so and open no PR.
Otherwise create a PR titled "docs: fix stale documentation" whose body
lists each correction as: the file, what the doc claimed, what the code
actually does, and how you verified it. Add a separate "Code that
contradicts its own docs" section for anything you chose not to fix.
# A docs job that edits code has done something it was told not to. Catch it
# here rather than in review — the PR is already open by this point, so this
# step failing is the signal to close it.
- name: Guard — Markdown-only changes
if: always()
run: |
set -uo pipefail
rm -rf docs-facts
CHANGED=$( { git diff --name-only "${BASE_SHA:-HEAD}" HEAD; git status --porcelain | awk '{print $NF}'; } | sort -u )
CODE_CHANGES=$(echo "$CHANGED" | grep -v -E '\.(md|markdown)$' | grep -v '^$' || true)
if [ -n "$CODE_CHANGES" ]; then
echo "::error::Non-Markdown files were modified by a docs-only job:"
echo "$CODE_CHANGES"
exit 1
fi
.github/workflows/duplicate-unifier.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Duplicate Unifier
# Finds the same implementation living in two or more places and merges it into
# one. Copies drift: a fix lands in one and not the other, and the two behave
# differently for a year before anyone notices.
#
# Detection is a sliding-window hash over normalized lines (identifiers kept,
# whitespace and string contents dropped), so it catches copies that were
# renamed but not restructured. It reports; the agent decides whether a match is
# a real duplicate or two things that merely rhyme.
on:
schedule:
- cron: "0 7 * * *" # Daily at 07:00 UTC
workflow_dispatch:
inputs:
window:
description: "Minimum duplicated lines to report"
required: false
default: "14"
jobs:
duplicate-unifier:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Detect duplicated blocks
id: scan
continue-on-error: true
shell: bash
run: |
WINDOW="$"
if [ -z "$WINDOW" ]; then WINDOW=14; fi
DUP_WINDOW="$WINDOW" node --input-type=module - <<'JS' > duplicate-blocks.log
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
const window = Number(process.env.DUP_WINDOW) || 14;
const files = execFileSync(
"bash",
[
"-c",
"git ls-files 'web/src/*.ts' 'web/src/*.tsx' 'electron/src/*.ts' " +
"'mobile/src/*.ts' 'mobile/src/*.tsx' 'packages/*/src/*.ts' " +
"| grep -vE '/generated/|\\.d\\.ts$'",
],
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024 },
)
.split("\n")
.filter(Boolean);
// Keep identifiers and structure, drop formatting, string contents and
// numbers: a copy that was reindented and had its literals swapped is
// still a copy.
const normalize = (line) =>
line
.replace(/\/\/.*$/, "")
.replace(/(["'`])(?:\\.|(?!\1).)*\1/g, "S")
.replace(/\b\d+(\.\d+)?\b/g, "N")
.replace(/\s+/g, " ")
.trim();
const blocks = new Map();
for (const file of files) {
const lines = readFileSync(file, "utf8").split("\n").map(normalize);
for (let i = 0; i + window <= lines.length; i += 1) {
const slice = lines.slice(i, i + window);
// A window that is mostly imports, braces or blanks is noise.
const substantive = slice.filter(
(line) => line.length > 12 && !/^(import|export|\}|\{|\)|\],?)/.test(line),
);
if (substantive.length < window * 0.6) continue;
const key = createHash("sha1").update(slice.join("\n")).digest("hex");
const seen = blocks.get(key) ?? [];
// Overlapping windows in one file are the same block, not a copy.
if (seen.some((hit) => hit.file === file && Math.abs(hit.line - (i + 1)) < window)) continue;
seen.push({ file, line: i + 1 });
blocks.set(key, seen);
}
}
// Copies spread across files come first: a block repeated inside one
// file is usually per-case boilerplate (the decorator-declared nodes
// look like this), while the same block in two files is drift waiting
// to happen.
const spread = (hits) => new Set(hits.map((hit) => hit.file)).size;
const groups = [...blocks.values()]
.filter((hits) => hits.length > 1)
.sort((a, b) => spread(b) - spread(a) || b.length - a.length);
// A file pair that duplicates ten adjacent windows is one finding.
const reported = new Set();
let count = 0;
for (const hits of groups) {
const pair = hits.map((hit) => hit.file).sort().join(" | ");
if (reported.has(pair)) continue;
reported.add(pair);
count += 1;
const files = spread(hits);
console.log(
`### ${hits.length} copies of a ${window}-line block across ${files} file${files === 1 ? "" : "s"}`,
);
for (const hit of hits.slice(0, 10)) console.log(` ${hit.file}:${hit.line}`);
if (hits.length > 10) console.log(` … ${hits.length - 10} more`);
console.log("");
if (count >= 25) break;
}
JS
echo "## Duplicated Blocks (window ${WINDOW})" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
head -60 duplicate-blocks.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
if [ -s duplicate-blocks.log ]; then
echo "DUPES_FOUND=1" >> $GITHUB_ENV
else
echo "DUPES_FOUND=0" >> $GITHUB_ENV
fi
- name: Run quality checks baseline
if: env.DUPES_FOUND == '1'
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
if: env.DUPES_FOUND == '1'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, and prose throat-clearing."
prompt: |
# Merge Duplicated Implementations
`duplicate-blocks.log` lists blocks of code that appear in more than
one place, with the file and line of each copy.
Pick **one** group and merge the copies into a single implementation.
## Decide first: is it really a duplicate?
Read every copy. Merge only when the copies do the same thing for the
same reason. Leave them alone when:
- They are alike by coincidence — two reducers with the same shape
over unrelated domains. Coupling them makes the next change worse.
- They sit on either side of a package boundary that exists on
purpose. `packages/protocol` may not import from `web/`, mobile
shares only `@nodetool-ai/protocol` and `@nodetool-ai/app-runtime`.
See the dependency order in AGENTS.md.
- One copy is a deliberate fork with diverging requirements, and the
divergence is documented.
- The duplicate is generated (`packages/*/src/generated/**`, DSL
codegen, provider manifests). Fix the generator or leave it.
## Where the merged code goes
- Same package → a shared module in that package.
- Two backend packages → the nearest common dependency in the order
`protocol → config → security → auth → storage → runtime → kernel →
node-sdk → base-nodes`. Never introduce a back edge.
- Web and backend → a package both already depend on, or leave the
copies alone. Do not create a new package for this.
- React components → a primitive in
`web/src/components/ui_primitives/` when the duplication is UI
(see its STRATEGY.md).
## The copies must differ somewhere — resolve it
Two copies of a function that drifted usually differ in one branch,
one default, or one error path. Diff them line by line, decide which
behavior is correct, and say so in the PR. If the callers genuinely
need both behaviors, take a parameter — but only when a caller
actually passes both values today.
## Rules
- One duplicate group per PR. Maximum 10 files.
- Every call site must go through the merged implementation. Do not
leave one copy behind "for now".
- Keep the tests of every copy. Point them at the merged code; when
they overlap exactly, keep one and say which you dropped.
- No new abstraction layer. One function replacing three copies of
itself, not a strategy interface.
- Do not touch CI workflow files.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a unification PR
is already open.
## Verification
```bash
npm run typecheck && npm run lint && npm run test
```
Backend packages also need `npm run test:packages`.
## Submit
Create a PR titled "refactor: unify duplicated <thing>" listing every
copy that was merged, where the single implementation now lives, and
each behavioral difference you found and how you resolved it.
- name: Post-change verification
if: always() && env.DUPES_FOUND == '1'
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/flaky-test-fixer.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Flaky Test Fixer
# Finds tests that pass and fail on the same code, and fixes the cause.
#
# Two sources of evidence. The GitHub API says which jobs went red and then
# green on a re-run of the same commit — the definition of a flake, straight
# from CI history. Locally, the suites run twice in randomized order, which
# surfaces the largest class of flakes in this repo: order dependence through
# shared module state.
#
# The only acceptable fix is the cause. Adding a retry, widening a timeout, or
# skipping the test hides a race that will reappear in production.
on:
schedule:
- cron: "0 2 * * *" # Daily at 02:00 UTC
workflow_dispatch:
inputs:
repeats:
description: "Randomized repeat runs of each suite"
required: false
default: "2"
jobs:
flaky-test-fixer:
runs-on: ubuntu-latest
timeout-minutes: 120
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Build packages
run: npm run build:packages
- name: Read flakes out of CI history
id: history
continue-on-error: true
env:
GH_TOKEN: $
shell: bash
run: |
echo "## Flakes in CI History" >> $GITHUB_STEP_SUMMARY
: > ci-flakes.log
# A run with more than one attempt whose final conclusion is success
# went red and then green on the same commit.
gh api "repos/$/actions/runs?branch=$&per_page=100" \
--jq '.workflow_runs[] | select(.run_attempt > 1) | "\(.name)\t\(.head_sha[0:8])\tattempts=\(.run_attempt)\t\(.conclusion)\t\(.html_url)"' \
>> ci-flakes.log 2>/dev/null || echo "(GitHub API unavailable)" >> ci-flakes.log
RERUNS=$(grep -c "attempts=" ci-flakes.log || true)
echo "Re-run runs on the default branch (last 100): $RERUNS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
head -25 ci-flakes.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
- name: Repeat the suites in randomized order
id: repeat
continue-on-error: true
shell: bash
run: |
REPEATS="$"
if [ -z "$REPEATS" ]; then REPEATS=2; fi
: > flaky-runs.log
FLAKY=0
# Jest randomizes case order within a file with --randomize; Vitest
# does the same with --sequence.shuffle. Order dependence — a module
# mock, a module-level cache, a global left dirty — shows up as a
# failure that moves between runs.
for i in $(seq 1 "$REPEATS"); do
echo "::group::web attempt $i"
if ! npm test --workspace=web -- --randomize > "/tmp/web-$i.log" 2>&1; then
FLAKY=1
{ echo "### web attempt $i failed"; echo '```'; grep -E "●|✕|FAIL|Expected|Received" "/tmp/web-$i.log" | head -60; echo '```'; echo; } >> flaky-runs.log
fi
echo "::endgroup::"
echo "::group::packages attempt $i"
if ! npx turbo run test --filter="./packages/*" -- --sequence.shuffle > "/tmp/pkg-$i.log" 2>&1; then
FLAKY=1
{ echo "### packages attempt $i failed"; echo '```'; grep -E "FAIL|✕|AssertionError|Expected|Received" "/tmp/pkg-$i.log" | head -60; echo '```'; echo; } >> flaky-runs.log
fi
echo "::endgroup::"
done
echo "## Randomized Repeat Runs" >> $GITHUB_STEP_SUMMARY
if [ "$FLAKY" -eq 0 ]; then
echo "All $REPEATS randomized runs passed." >> $GITHUB_STEP_SUMMARY
else
echo "At least one randomized run failed — see flaky-runs.log." >> $GITHUB_STEP_SUMMARY
fi
echo "REPEAT_FAILURES=$FLAKY" >> $GITHUB_ENV
- name: Decide whether there is anything to fix
shell: bash
run: |
# CI history alone is enough to work from: a re-run that went green is
# a flake even if today's randomized runs happened to pass.
RERUNS=$(grep -c "attempts=" ci-flakes.log || true)
if [ "$REPEAT_FAILURES" = "1" ] || [ "${RERUNS:-0}" -gt 0 ]; then
echo "FLAKES_FOUND=1" >> $GITHUB_ENV
else
echo "FLAKES_FOUND=0" >> $GITHUB_ENV
fi
- name: Upload run logs
if: env.FLAKES_FOUND == '1'
uses: actions/upload-artifact@v7
with:
name: flaky-run-logs
path: |
ci-flakes.log
flaky-runs.log
if-no-files-found: warn
retention-days: 30
- name: Run Claude Code
if: env.FLAKES_FOUND == '1'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, and prose throat-clearing."
prompt: |
# Root-Cause a Flaky Test
Two sources of evidence:
- `ci-flakes.log` — runs on the default branch that needed more than
one attempt. Same commit, red then green.
- `flaky-runs.log` — failures from running the suites twice with
randomized order (`jest --randomize`,
`vitest --sequence.shuffle`).
Pick **one** flaky test and fix what makes it non-deterministic.
## Reproduce it first
A flake you cannot reproduce is a flake you cannot claim to have
fixed. Try, in order:
```bash
npm test --workspace=web -- --randomize -t "<test name>"
npm run test --workspace=packages/<name> -- --sequence.shuffle
# order dependence: run the suspected neighbour first
npm test --workspace=web -- <fileA> <fileB>
```
Loop it 20 times and count the failures. Record the rate before and
after your fix. If it never reproduces, say so and inspect the CI
logs from `ci-flakes.log` instead — `gh run view <id> --log-failed`.
## The usual causes, in this repo's terms
- **Shared module state** between test files: a module-level cache, a
Zustand store that is never reset, a registry that accumulates.
- **Unawaited promises**: an assertion running before the effect it
depends on; a `void`-ed promise that settles into the next test.
- **Real timers and real time**: `setTimeout`-based waits, dates near
midnight, durations asserted with `toBe`.
- **`waitFor` misuse**: asserting on the first render instead of the
settled state, or a `waitFor` with a side effect inside it.
- **Order or index assumptions** over unordered data: `Object.keys`,
a `Map` iteration, results from a query with no `ORDER BY`.
- **Ports, files, and databases** shared between parallel workers.
- **The network**: a test that reaches a real endpoint. Mock it.
- **WebSocket and MsgPack framing**: assertions that assume one
message per frame, or a fixed arrival order.
## Fix the cause
Acceptable: reset the shared state in `beforeEach`, await what the
code awaits, use fake timers, assert on the settled state, sort
before comparing, isolate the resource, mock the boundary.
**Not acceptable**, and a PR that does any of these will be rejected:
- retrying the test (`jest.retryTimes`, `test.retry`)
- `test.skip` / `describe.skip` / `.fixme` on a flaky test
- raising a timeout without showing the operation genuinely needs
longer
- loosening the assertion until it always passes
- `await new Promise(r => setTimeout(r, 500))` as a synchronization
device
If the flake is caused by a **product** race rather than a test bug,
fix the product code and say so — that race reaches users too.
## Rules
- One flake per PR. Maximum 8 files.
- Show the failure rate before and after, from an actual loop.
- Do not touch CI workflow files.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a flaky-test PR
is already open.
## Verification
```bash
npm run lint && npm run test && npm run test:packages
```
Plus the repeat loop for the test you fixed.
## Submit
Create a PR titled "fix(test): deflake <test>" naming the source of
non-determinism, the reproduction command, the failure rate before
and after, and why the fix removes the cause rather than the symptom.
- name: Post-change verification
if: always() && env.FLAKES_FOUND == '1'
run: |
echo "## Post-Change Suites" >> $GITHUB_STEP_SUMMARY
FAIL=0
npm run lint || FAIL=1
npm run test || FAIL=1
npm run test:packages || FAIL=1
if [ $FAIL -eq 0 ]; then
echo "Suites passed" >> $GITHUB_STEP_SUMMARY
exit 0
fi
# The randomized repeats above are this run's baseline. If they were
# already failing, a red suite here says nothing new — the whole point
# of this workflow is that these suites fail intermittently.
if [ "$REPEAT_FAILURES" = "1" ]; then
echo "Suites failed, but they were already failing before the change" >> $GITHUB_STEP_SUMMARY
exit 0
fi
echo "Suites failed after the deflake change, having passed before it" >> $GITHUB_STEP_SUMMARY
exit 1
.github/workflows/genspend-pricing.yml
Original GitHub Actions definition, preserved for the external orchestrator.
name: GenSpend Pricing Sync
# Nightly: pull generative-media prices from the GenSpend catalog
# (https://genspend.io/api/v1/export) into
# `packages/model-pricing/src/generated/genspend-pricing.json`, and open a PR
# when a price moved.
#
# A price change is a diff a human should see before it gates a run's budget, so
# this opens a PR and never pushes to main. When nothing moved the script
# rewrites nothing and no PR is opened.
#
# Opening that PR needs a token GitHub lets create pull requests, and the
# default `GITHUB_TOKEN` is not one unless the repository enables Settings >
# Actions > General > "Allow GitHub Actions to create and approve pull
# requests". Without it the API answers "GitHub Actions is not permitted to
# create or approve pull requests" and the branch is pushed with nothing to
# review. Three paths, tried in order:
#
# 1. `PRICING_SYNC_TOKEN` (a PAT or App token with `contents: write` and
# `pull-requests: write`), if the secret is set.
# 2. `GITHUB_TOKEN`, which works where the repository setting above is on.
# 3. The Claude GitHub App, via `anthropics/claude-code-action`. This is how
# the nightly agent workflows here open PRs as `claude[bot]`: the action
# trades an OIDC token for an App installation token, and App tokens are
# not covered by the Actions restriction. It needs no repository admin and
# no new secret, at the cost of one small model run — only on the nights a
# price actually moved, and only when 1 and 2 both failed.
#
# `create-pull-request` pushes the branch before it calls the API, so by the
# time path 3 runs the commit already exists and the agent only has to open the
# PR against it. If every path fails the job prints the compare link and exits
# non-zero — a moved price with no PR is not a green run.
#
# The sync matches GenSpend models against the models each provider enumerates
# in NodeTool, so it needs the backend packages built. No provider API keys: the
# listings it reads are static or manifest-backed, and the GenSpend API is
# public and read-only.
on:
workflow_dispatch: {}
schedule:
# 03:20 UTC nightly — off the hour so it doesn't contend with the other
# scheduled jobs.
- cron: "20 3 * * *"
permissions:
# id-token: the Claude action exchanges an OIDC token for a Claude GitHub App
# installation token. That identity may open pull requests; GITHUB_TOKEN may
# not unless the repository allows it.
id-token: write
contents: write
pull-requests: write
concurrency:
group: genspend-pricing
cancel-in-progress: false
jobs:
sync:
name: Refresh the GenSpend price catalog
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install root dependencies
run: npm ci
- name: Build backend packages
# The sync reads each provider's model listing from the built runtime.
run: npm run build:packages
- name: Sync prices
run: npm run sync:genspend -- --report genspend-coverage.json
- name: Check our arithmetic against GenSpend's calculator
# The catalog is a snapshot we price locally from, so the risk is that
# our port of the selection rules drifts from theirs. This re-prices
# fixed cases from the file just written and asserts each equals
# POST /api/v1/quote. It needs the network, which is why it lives here
# and not in the PR quality gate. A drift fails the job rather than
# opening a PR that ships a wrong number.
run: node scripts/genspend/parity-check.mjs
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v7
with:
name: genspend-coverage
path: genspend-coverage.json
if-no-files-found: warn
- name: Detect changes
id: diff
# The PR body goes to a file rather than a step output: both the
# create-pull-request path and the Claude fallback read the same bytes,
# so the two cannot describe the same diff differently.
run: |
set -euo pipefail
if git diff --quiet -- packages/model-pricing/src/generated/genspend-pricing.json; then
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "changed=true" >> "$GITHUB_OUTPUT"
{
echo "Nightly refresh of the GenSpend price catalog. Prices via"
echo "[genspend.io](https://genspend.io)."
echo
node -e "
const c = require('./packages/model-pricing/src/generated/genspend-pricing.json');
const report = require('./genspend-coverage.json');
console.log(\`\${c.pricedModels} priced model ids across \${c.providers.length} providers, from \${c.catalogOfferings} offerings over \${c.catalogModels} tracked models (prices last moved \${c.updatedAt}).\`);
console.log('');
console.log('| Provider | Offerings priced | Model ids | Unresolved |');
console.log('| --- | --- | --- | --- |');
for (const [provider, s] of Object.entries(report.coverage).sort()) {
console.log(\`| \${provider} | \${s.priced}/\${s.offerings} | \${s.ids} | \${s.unresolved} |\`);
}
"
echo
echo "These numbers feed \`getModelUnitPrice\`, so they are what the editor's"
echo "cost preview shows and what a run's budget check is gated on. Read"
echo "the diff before merging:"
echo
echo "- [ ] No price moved by an order of magnitude (GenSpend quarantines"
echo " >3× swings, but a unit change would slip through)"
echo "- [ ] \`unit_class\` values still match the \`billing_unit\` they map to"
echo "- [ ] No model lost its price because the provider's receipt page moved"
echo "- [ ] New \`match: \"catalog\"\` entries name the model they claim to price"
echo
echo "Unresolved offerings are listed in the run log and the"
echo "\`genspend-coverage\` artifact — pin any that should be priced in"
echo "\`scripts/genspend/aliases.json\`."
} > "$RUNNER_TEMP/pr-body.md"
- name: Open PR
id: pr
if: steps.diff.outputs.changed == 'true'
continue-on-error: true
uses: peter-evans/create-pull-request@v8
with:
# Falls back to GITHUB_TOKEN, which only opens a PR where the
# repository allows Actions to. See the header comment.
token: $
branch: chore/genspend-pricing-sync
base: main
add-paths: packages/model-pricing/src/generated/genspend-pricing.json
commit-message: "chore(pricing): sync GenSpend price catalog"
title: "chore(pricing): sync GenSpend price catalog"
body-path: $/pr-body.md
delete-branch: true
- name: Does the PR exist?
id: check
if: steps.diff.outputs.changed == 'true'
# Read the state rather than trust the step outcome. create-pull-request
# reports failure for a refused *creation* and for a refused push alike,
# and only the first is worth a model run.
env:
GH_TOKEN: $
run: |
set -euo pipefail
BRANCH=chore/genspend-pricing-sync
PUSHED=$(gh api "repos/$/branches/$BRANCH" --jq .commit.sha 2>/dev/null || true)
OPEN=$(gh pr list --head "$BRANCH" --base main --state open --json number --jq 'length')
echo "pushed=${PUSHED:-}" >> "$GITHUB_OUTPUT"
echo "open=$OPEN" >> "$GITHUB_OUTPUT"
echo "Branch head: ${PUSHED:-<not pushed>} · open PRs for it: $OPEN"
- name: Open PR as the Claude GitHub App
# Only when the branch is pushed and nothing points at it. The App token
# this action mints is the one identity in this job GitHub lets open a
# PR without a repository setting or a new secret.
if: >-
steps.diff.outputs.changed == 'true'
&& steps.check.outputs.pushed != ''
&& steps.check.outputs.open == '0'
continue-on-error: true
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Read,CreatePullRequest"
prompt: |
# Open one pull request. Change nothing else.
The branch `chore/genspend-pricing-sync` is already pushed to this
repository and already carries the commit. Your only job is to open
a pull request for it, because the token that pushed it is not
allowed to open one.
- head: `chore/genspend-pricing-sync`
- base: `main`
- title: `chore(pricing): sync GenSpend price catalog`
- body: the exact contents of `$/pr-body.md`
Read that file and use it verbatim as the body. Do not summarize it,
add to it, or write your own. Do not edit any file in the working
tree, do not commit, and do not push — the diff under review was
generated by this workflow and must reach the reviewer untouched.
If a pull request for that branch already exists, do nothing and say
so.
- name: Report a blocked PR
# Re-read the state instead of trusting the fallback's outcome. Whatever
# happened above, the question is the same one: does a moved price have
# a PR pointing at it?
if: steps.diff.outputs.changed == 'true'
env:
GH_TOKEN: $
BRANCH: chore/genspend-pricing-sync
run: |
set -euo pipefail
NUM=$(gh pr list --head "$BRANCH" --base main --state open --json number --jq '.[0].number // empty')
if [ -n "$NUM" ]; then
echo "Pull request #$NUM is open for $BRANCH." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
COMPARE="$/$/compare/main...$BRANCH?expand=1"
{
echo "## GenSpend price sync: prices moved, PR not opened"
echo
echo "The refreshed catalog is on \`$BRANCH\`. Open it by hand:"
echo
echo "$COMPARE"
echo
echo "Every path failed. To fix the next run, do one of:"
echo
echo "- enable **Settings > Actions > General > Allow GitHub Actions to create and approve pull requests**,"
echo "- add a \`PRICING_SYNC_TOKEN\` secret (PAT or App token with \`contents: write\` and \`pull-requests: write\`), or"
echo "- check the **Open PR as the Claude GitHub App** step above — a bad \`CLAUDE_CODE_OAUTH_TOKEN\` or a missing App installation shows there."
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::Prices moved but no pull request was opened. Review $COMPARE"
exit 1
.github/workflows/internal-only-shipper.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Internal-Only Feature Shipper
# Features that only exist in a dev build are features nobody uses. Each one
# still costs: it compiles, it ships in the bundle, it drifts, and it makes the
# code around it harder to read.
#
# This finds surfaces reachable only behind a dev/internal gate — `NODETOOL_ENV
# === "development"`, `import.meta.env.DEV`, `__DEV__`, `NODE_ENV !==
# "production"`, hidden routes, undocumented CLI commands — and forces a
# decision on each: ship it (drop the gate and document it) or delete it. The
# one outcome not allowed is leaving it as it is.
on:
schedule:
- cron: "30 5 * * *" # Daily at 05:30 UTC
workflow_dispatch:
jobs:
internal-only-shipper:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Find internal-only gates
id: scan
continue-on-error: true
shell: bash
run: |
echo "## Internal-Only Gates" >> $GITHUB_STEP_SUMMARY
: > internal-only-gates.log
PATTERN='import\.meta\.env\.DEV|__DEV__|NODE_ENV *[!=]== *["'"'"']production|NODETOOL_ENV *[=!]== *["'"'"']development|isDev(elopment)?\b|INTERNAL_ONLY|internalOnly'
grep -rEn "$PATTERN" \
--include="*.ts" --include="*.tsx" \
web/src electron/src mobile/src packages/*/src 2>/dev/null \
| grep -vE "\.test\.|\.spec\.|__tests__|/tests/" \
> internal-only-gates.log || true
GATES=$(wc -l < internal-only-gates.log | tr -d ' ')
echo "Gated call sites: $GATES" >> $GITHUB_STEP_SUMMARY
# Last-touched date per gated file: a gate nobody has touched in a
# year is evidence of abandonment, not of caution.
: > gate-age.log
cut -d: -f1 internal-only-gates.log | sort -u | while IFS= read -r file; do
LAST=$(git log -1 --format=%ad --date=short -- "$file" 2>/dev/null)
echo "$LAST $file" >> gate-age.log
done
sort gate-age.log | head -30 >> $GITHUB_STEP_SUMMARY
if [ "$GATES" -gt 0 ]; then echo "GATES_FOUND=1" >> $GITHUB_ENV; else echo "GATES_FOUND=0" >> $GITHUB_ENV; fi
- name: Run quality checks baseline
if: env.GATES_FOUND == '1'
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
if: env.GATES_FOUND == '1'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, and prose throat-clearing."
prompt: |
# Ship or Delete an Internal-Only Feature
`internal-only-gates.log` lists every call site gated to a dev or
internal build. `gate-age.log` gives the last commit date per file,
oldest first.
Pick **one** feature — not one line, the whole feature behind the
gate — and either ship it or delete it.
## Choose the feature
Prefer the oldest gate that is a real user-facing surface: a panel,
a menu entry, a route, a CLI command, a node, an API route. Skip:
- debug logging and dev-server plumbing (`vite`, HMR, source maps)
- React StrictMode and devtools wiring
- test-only helpers and fixtures
- anything under `__tests__/`, `tests/`, or `scripts/`
## Gather the evidence, then decide
For the feature you picked, answer each of these with a command:
- When was it last touched? `git log -5 --oneline -- <paths>`
- Who references it? `grep -rn "<symbol>" --include="*.ts*" .`
- Is it documented? Search `docs/`, `AGENTS.md`, `README.md`.
- Is it covered by tests? Search `__tests__/` and `tests/`.
- Does it still work? Drive it headlessly — the CLI harnesses in
AGENTS.md (`nodetool validate`, `node run`, `app debug`) or the
package's own test suite.
**Ship it** when the feature works, is complete, and a user would
want it: remove the gate, wire it into the normal UI/CLI surface,
document it where its siblings are documented, and add a test if it
has none.
**Delete it** when it is abandoned — no commits for many months, no
references outside its own files, no docs, and no working path
through it. Remove the feature, its gate, its dead branches, its
styles, its types, and its now-unused imports.
State which one you chose and why, with the commands you ran, in the
PR body. "Unclear" is not a verdict — if the evidence is genuinely
split, delete it: it can be recovered from git, and an unused gate
cannot.
## Rules
- One feature per PR. Do not sweep every gate in the log.
- Shipping means shipping: no gate left behind, no `if (false)`.
- Do not delete a gate that guards *correctness* rather than
visibility (dev-only assertions, mock providers used by tests,
error overlays). Those are not features.
- Do not touch CI workflow files.
- Maximum 15 files per PR.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a shipper PR is
already open.
## Verification
```bash
npm run typecheck && npm run lint && npm run test
```
For a shipped feature, also drive it once headlessly and paste what
it produced.
## Submit
Create a PR titled "feat: ship <feature>" or "chore: remove abandoned
<feature>", with the evidence table and the verdict.
- name: Post-change verification
if: always() && env.GATES_FOUND == '1'
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/issue-triage.yml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Issue Triage
# When a new issue is opened, Claude triages it: applies labels, checks for
# likely duplicates, and — for bug reports missing a reproduction — asks for the
# specifics needed to act. Read-only on code; it only comments and labels.
#
# Skips issues that tagged @claude (claude.yml handles those) and issues opened
# by the automation itself.
on:
issues:
types: [opened]
jobs:
triage:
if: |
!contains(github.event.issue.body, '@claude') &&
!contains(github.event.issue.title, '@claude')
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: Run Claude Code
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Read,Grep,Glob"
prompt: |
# Triage a New Issue
A new issue was opened in $:
- Number: #$
- Title: $
Read the full issue with `gh issue view $`.
Do the following, using the `gh` CLI:
## 1. Label it
- Run `gh label list` to see available labels. Only apply labels that
already exist — do not create new ones.
- Apply the labels that fit: kind (`bug`, `enhancement`, `question`,
`documentation`), and area if the labels support it (e.g. web,
electron, mobile, backend/packages, a specific provider or node).
- Infer area from the codebase: search `packages/`, `web/src`,
`electron/`, `mobile/` for the files, nodes, or providers the issue
names.
- Apply with `gh issue edit $ --add-label "<label>"`.
## 2. Check for duplicates
- Search existing issues:
`gh issue list --search "<keywords>" --state all --limit 20`.
- If you find a clear duplicate or strongly-related issue, post ONE
comment linking it (`Possible duplicate of #NNN — ...`). Do not close
the issue; leave that to a maintainer.
## 3. Request missing repro (bugs only)
- If it's a bug report missing what's needed to reproduce (version /
platform, exact steps, expected vs actual, the workflow or node
involved, logs), post ONE friendly comment asking for the specific
missing items. Be concrete — list exactly what you need.
## Rules
- Read-only on code. Never edit files, never push, never open a PR.
- Post at most ONE comment total. If nothing needs a comment (clear,
complete, non-duplicate), just label and stop.
- Do not restate the issue back to the author or add filler. No comment
is better than a low-value one.
- Do not @-mention maintainers or assign anyone.
.github/workflows/logic-bugfixer.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Logic Bugfixer
# Picks one piece of tricky logic, builds an independent model of what it should
# do, and runs both over the whole input space. Every divergence is either a bug
# in the code or a bug in the model — and finding out which is the work.
#
# The candidates are pure, decidable functions: predicates, reducers, comparators,
# state transitions, window and range math, retry and backoff, the streaming
# fold. Anything whose behavior depends on a network, a clock, or a model is out
# of scope: this routine only reports what it can prove.
on:
schedule:
- cron: "30 6 * * *" # Daily at 06:30 UTC
workflow_dispatch:
inputs:
target:
description: "Optional file or symbol to model (default: pick from the scan)"
required: false
default: ""
jobs:
logic-bugfixer:
runs-on: ubuntu-latest
timeout-minutes: 90
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Build packages
run: npm run build:packages
- name: Nominate decidable logic
id: scan
continue-on-error: true
shell: bash
run: |
echo "## Candidate Logic" >> $GITHUB_STEP_SUMMARY
: > logic-candidates.log
# Exported functions whose names say "this is a decision, and it is
# decidable": predicates, comparators, folds, range and state math.
grep -rEn "export (async )?function (is|has|can|should|needs|match|compare|merge|fold|clamp|resolve|normalize|select|next|derive|apply|reduce)[A-Z][A-Za-z0-9]*" \
--include="*.ts" \
packages/*/src web/src electron/src mobile/src 2>/dev/null \
| grep -vE "__tests__|\.test\.|\.spec\.|/generated/" \
>> logic-candidates.log || true
CANDIDATES=$(wc -l < logic-candidates.log | tr -d ' ')
echo "Candidates: $CANDIDATES" >> $GITHUB_STEP_SUMMARY
# Which of them nothing tests yet — those are where divergences live.
: > untested-candidates.log
while IFS= read -r hit; do
SYMBOL=$(echo "$hit" | sed -E 's/.*export (async )?function ([A-Za-z0-9_]+).*/\2/')
[ -z "$SYMBOL" ] && continue
if ! grep -rqE "\b$SYMBOL\b" --include="*.test.ts" --include="*.test.tsx" \
--include="*.spec.ts" packages web/src electron mobile/src 2>/dev/null; then
echo "$hit" >> untested-candidates.log
fi
done < logic-candidates.log
UNTESTED=$(wc -l < untested-candidates.log | tr -d ' ')
echo "Untested candidates: $UNTESTED" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
head -20 untested-candidates.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
if [ "$CANDIDATES" -gt 0 ]; then echo "LOGIC_FOUND=1" >> $GITHUB_ENV; else echo "LOGIC_FOUND=0" >> $GITHUB_ENV; fi
- name: Run quality checks baseline
if: env.LOGIC_FOUND == '1'
id: pre-check
continue-on-error: true
run: |
LINT_EXIT=0; TEST_EXIT=0; PKG_TEST_EXIT=0
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
npm run test:packages 2>&1 || PKG_TEST_EXIT=1
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
echo "PKG_TEST_PRE_EXIT=$PKG_TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
if: env.LOGIC_FOUND == '1'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, and prose throat-clearing."
prompt: |
# Model Tricky Logic and Fix What It Gets Wrong
Requested target: `$` (empty means
choose one yourself).
`logic-candidates.log` lists exported predicates, comparators,
folds, and state transitions. `untested-candidates.log` is the
subset nothing tests yet — prefer those.
## Method
1. **Pick one function** that is pure and decidable: its result
depends only on its arguments, and its interesting input space is
small enough to enumerate (a handful of enums, booleans, small
integers, short arrays) or to sample densely.
2. **Write down the specification** in the PR body, from the docs,
the call sites, and the types — not from the implementation.
If the spec is genuinely ambiguous, say which reading you chose.
3. **Write an independent model** in a scratch file under
`/tmp` — a second implementation of the spec, written from the
spec, deliberately structured differently from the code.
4. **Enumerate the input space** and compare the model against the
real function, imported from its real module. Never copy the
implementation into the scratch file: a copy proves nothing.
```bash
npx tsx /tmp/model-check.ts
```
Cover the boundaries explicitly: empty, one element, duplicates,
equal keys, min/max, negative, zero, `undefined` vs missing,
NaN, unicode, and the transitions in and out of every state.
5. **Triage every divergence.** For each one decide whether the code
or the model is wrong, and say how you know — a doc, a call site
that depends on it, a downstream test. A divergence you cannot
adjudicate goes in the PR body as an open question, not into a
fix.
6. **Ship the reproduction with the fix.** For each real bug: add a
failing test in the repo's own suite first (`__tests__/` next to
the code, Vitest for `packages/`, Jest for `web`/`electron`),
watch it fail, then fix the code and watch it pass. AGENTS.md
requires the reproduction — a fix without one does not ship.
## Rules
- No behavior changes without a failing test that proves the bug.
- Do not "fix" a divergence by weakening the test or by rewriting
the model to agree with the code.
- Do not change a public signature to make modelling easier.
- The scratch model stays in `/tmp`. What lands in the repo is the
regression tests and the fix.
- If the enumeration finds nothing, that is a fine outcome: land the
enumeration as a table-driven test instead, and say the code held.
- Do not touch CI workflow files.
- One function per PR.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a logic-bugfix
PR is already open.
## Verification
```bash
npm run lint && npm run test && npm run test:packages
```
## Submit
Create a PR titled "fix: <function> mishandles <case>" (or
"test: pin <function> behavior" when nothing diverged) containing the
spec, the input space enumerated, every divergence and its verdict,
and the tests that now cover them.
- name: Post-change verification
if: always() && env.LOGIC_FOUND == '1'
run: |
LINT_EXIT=0; TEST_EXIT=0; PKG_TEST_EXIT=0
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
npm run test:packages 2>&1 || PKG_TEST_EXIT=1
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$PKG_TEST_PRE_EXIT" -eq 0 ] && [ $PKG_TEST_EXIT -ne 0 ]; then
echo "New package test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/logic-simplifier.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Logic Simplifier
# Ranks source files by branch density and nesting depth, then rewrites the
# worst offender's logic without changing what it does.
#
# The ranking is a crude proxy — a switch over twenty node kinds scores high and
# is fine as it is — so the scan only nominates candidates. The agent reads the
# code and decides whether there is a simplification worth making, and proves
# behavior is unchanged by running the existing tests, or by adding
# characterization tests first when there are none.
on:
schedule:
- cron: "0 6 * * *" # Daily at 06:00 UTC
workflow_dispatch:
jobs:
logic-simplifier:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Rank complexity hotspots
id: scan
continue-on-error: true
shell: bash
run: |
node --input-type=module - <<'JS' > complexity-hotspots.log
import { readFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
const files = execFileSync(
"bash",
[
"-c",
"git ls-files 'web/src/*.ts' 'web/src/*.tsx' 'electron/src/*.ts' " +
"'mobile/src/*.ts' 'mobile/src/*.tsx' 'packages/*/src/*.ts' " +
"| grep -vE '__tests__|\\.test\\.|\\.spec\\.|/generated/|/configs/|/data/|\\.d\\.ts$'",
],
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024 },
)
.split("\n")
.filter(Boolean);
const BRANCH = /\b(if|else if|switch|case|catch|while|for)\b|&&|\|\||\?\?|\?\./g;
const scored = [];
for (const file of files) {
const source = readFileSync(file, "utf8");
const lines = source.split("\n");
if (lines.length < 60) continue;
const branches = (source.match(BRANCH) ?? []).length;
// A 5000-line lookup table has no logic to simplify, however long
// it is. Branches are what this routine can act on.
if (branches < 15) continue;
// Max indent depth, tabs counted as one level, comments skipped.
let depth = 0;
for (const line of lines) {
if (!line.trim() || line.trim().startsWith("*") || line.trim().startsWith("//")) continue;
const indent = line.match(/^[ \t]*/)[0].replace(/\t/g, " ").length;
depth = Math.max(depth, Math.floor(indent / 2));
}
// Longest run of consecutive non-blank lines: a 200-line function
// body and a 200-line file of small functions read very differently.
let run = 0;
let longestRun = 0;
for (const line of lines) {
run = line.trim() ? run + 1 : 0;
longestRun = Math.max(longestRun, run);
}
const score = branches + depth * 4;
scored.push({ file, score, branches, depth, longestRun, lines: lines.length });
}
scored.sort((a, b) => b.score - a.score);
for (const entry of scored.slice(0, 25)) {
console.log(
`${String(entry.score).padStart(5)} branches=${entry.branches} depth=${entry.depth} ` +
`longest-run=${entry.longestRun} lines=${entry.lines} ${entry.file}`,
);
}
JS
echo "## Complexity Hotspots" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
head -25 complexity-hotspots.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
if [ -s complexity-hotspots.log ]; then
echo "HOTSPOTS_FOUND=1" >> $GITHUB_ENV
else
echo "HOTSPOTS_FOUND=0" >> $GITHUB_ENV
fi
- name: Run quality checks baseline
if: env.HOTSPOTS_FOUND == '1'
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
if: env.HOTSPOTS_FOUND == '1'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, and prose throat-clearing."
prompt: |
# Simplify Convoluted Logic
`complexity-hotspots.log` ranks source files by branch count, nesting
depth, and longest unbroken run of code. It is a proxy, not a
verdict — read the code before touching it.
Pick **one** function or closely related group of functions and make
the logic easier to follow without changing what it does.
## What to look for
- **Deep nesting** that inverts cleanly: guard clauses and early
returns instead of an arrow of `if`s.
- **Boolean expressions** carrying three or more terms: name the
sub-conditions, or express them as a small predicate.
- **Repeated condition chains**: the same `a && b.c && b.c.d` test in
five places, computed once instead.
- **Flag arguments** that make a function do two unrelated things:
split it in two.
- **Manual loops** that are a `map`/`filter`/`find`/`some` in
disguise, and reduces that reimplement `Object.fromEntries`.
- **State machines written as booleans**: three `isLoading` /
`isError` / `isDone` flags with implicit invariants become one
discriminated union (AGENTS.md prefers this).
- **Dead branches**: conditions that cannot hold given the types.
## What NOT to touch
- Exhaustive `switch` over a union. That is the language working.
- Ordering that looks arbitrary but is load-bearing (initialization
order, event ordering, `await` sequencing). If you cannot explain
why an order is safe to change, leave it.
- Anything whose complexity is inherent: parsers, schedulers, the
streaming fold, protocol framing.
- Generated code (`packages/*/src/generated/**`, DSL codegen output).
## Method
1. Read the function and write down, in the PR body, what it does —
inputs, outputs, side effects, error paths.
2. Find its tests. If there are none, **write characterization tests
first** against the current behavior, in their own commit, and
confirm they pass before you change anything.
3. Refactor in small steps, running the tests after each.
4. Diff the behavior, not the shape: every branch the old code took
must still be reachable, including the error paths.
## Rules
- Behavior-preserving only. No bug fixes, no new features. If you
find a bug, leave it and report it in the PR body.
- No new abstractions. This routine removes indirection; it does not
add a strategy object to flatten an `if`.
- One function or one tight cluster per PR. Maximum 6 files.
- Do not touch CI workflow files.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a simplification
PR is already open.
## Verification
```bash
npm run typecheck && npm run lint && npm run test
```
Backend packages also need `npm run test:packages`.
## Submit
Create a PR titled "refactor: simplify <function> in <area>" with the
before/after description of the logic and the tests that pin it.
- name: Post-change verification
if: always() && env.HOTSPOTS_FOUND == '1'
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/opencode.yml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Interactive Assistant
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
jobs:
assistant:
if: |
contains(github.event.comment.body, ' /oc') ||
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Run Claude Code
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,WebFetch,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, useEffect+fetch instead of useQuery, and prose throat-clearing."
prompt: |
# NodeTool Assistant
You are an assistant for NodeTool, a React/TypeScript visual AI workflow builder.
## Project
- Stack: React 18.2, TypeScript 5.7, Zustand, ReactFlow, MUI v7
- Layout: `/web` (React app), `/electron` (desktop), `/mobile` (React Native)
- Backend is a separate Python repo — this repo is frontend only
- Read `AGENTS.md` and `.github/copilot-instructions.md` for coding conventions
## Respond to the comment
Read the issue/PR comment and do what was asked. This could be:
- Answering a question about the codebase
- Investigating a bug
- Implementing a fix or small feature
- Reviewing code
## If you make code changes
1. Keep changes minimal and focused on what was asked
2. Run `npm run typecheck && npm run lint && npm run test` — all must pass
3. Create a PR linking back to the issue/PR where `/oc` was invoked
4. Do not refactor or "improve" code beyond what was requested
.github/workflows/performance-optimization.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Performance Optimization
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
jobs:
performance-optimization:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Run quality checks baseline
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, useEffect+fetch instead of useQuery, and prose throat-clearing."
prompt: |
# Performance Optimization
Find and fix React/TypeScript performance issues in the NodeTool codebase.
## Scope
- `/web/src` — React frontend (primary focus)
- `/electron/src` — desktop app
- `/mobile/src` — React Native app
## What to fix
**Missing React.memo**: Components that receive the same props frequently but re-render unnecessarily. Look for components rendered inside lists or frequently-updating parents. Only add memo where there's a clear benefit (expensive renders or stable parent with changing siblings).
**Missing useMemo/useCallback**: Expensive computations or object/array literals created on every render that are passed as props to child components or used as effect dependencies. Focus on:
- Object/array literals in JSX props: `<Child items={data.filter(...)} />`
- Functions defined inline and passed as props: `<Child onClick={() => handle(id)} />`
- Expensive `.filter()`, `.map()`, `.reduce()`, `.sort()` chains on large datasets
**Inefficient useEffect patterns**:
- Effects that run on every render due to missing or incorrect dependency arrays
- Effects that could be replaced with useMemo or event handlers
- Multiple effects that could be consolidated
**Unnecessary re-renders from Zustand**: Selectors that return new object references on every call. Fix by selecting primitive values or using shallow comparison.
**Large bundle imports**: Importing entire libraries when only specific functions are needed (e.g. `import _ from 'lodash'` vs `import debounce from 'lodash/debounce'`).
## Rules
- Only fix clear, measurable performance issues. Do not premature-optimize.
- Do not change component APIs or public interfaces.
- Do not touch `@ts-expect-error` / `@ts-ignore` directives.
- Do not touch CI workflow files.
- Keep changes focused — maximum 10 files per PR.
- Each optimization must be justified in the PR description.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a performance PR is already open.
- Focus on the most impactful files: large components, frequently-rendered components, store selectors.
## Verification
After changes, all checks must still pass:
```bash
npm run typecheck && npm run lint && npm run test
```
## Submit
Create a PR titled "perf: optimize <area>" listing each change and why it improves performance.
- name: Post-change verification
if: always()
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/quality-assurance.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Quality Assurance
on:
schedule:
- cron: "0 */6 * * *"
workflow_dispatch:
jobs:
quality-assurance:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Run quality checks
id: quality-check
continue-on-error: true
run: |
echo "## Quality Check Results" >> $GITHUB_STEP_SUMMARY
FAILED=0
npm run lint:fix 2>&1 || true
if npm run typecheck 2>&1 | tee typecheck.log; then
echo "- typecheck: PASS" >> $GITHUB_STEP_SUMMARY
else
FAILED=1
echo "- typecheck: FAIL" >> $GITHUB_STEP_SUMMARY
fi
if npm run lint 2>&1 | tee lint.log; then
echo "- lint: PASS" >> $GITHUB_STEP_SUMMARY
else
FAILED=1
echo "- lint: FAIL" >> $GITHUB_STEP_SUMMARY
fi
if npm run test 2>&1 | tee test.log; then
echo "- test: PASS" >> $GITHUB_STEP_SUMMARY
else
FAILED=1
echo "- test: FAIL" >> $GITHUB_STEP_SUMMARY
fi
echo "QUALITY_FAILED=$FAILED" >> $GITHUB_ENV
- name: Run Claude Code
if: env.QUALITY_FAILED == '1'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, useEffect+fetch instead of useQuery, and prose throat-clearing."
prompt: |
# Fix Broken Quality Checks
The quality checks for this repo are failing. Your job is to fix them.
Read the error logs to understand what's broken, then fix it:
- `typecheck.log` — TypeScript compilation errors
- `lint.log` — ESLint violations (run `npm run lint:fix` first, then fix the rest manually)
- `test.log` — failing test cases
## Rules
- Only fix actual errors. Do not refactor, improve, or "clean up" anything.
- Do not touch code unrelated to the failing checks.
- Do not add features, comments, or documentation.
- Keep changes minimal — the smallest diff that makes checks pass.
- If a test is failing because the code is wrong, fix the code. If a test is wrong, fix the test. Use git blame and recent commits to determine which.
## Before starting
- Run `gh pr list --state open --limit 20` — if an open PR already fixes these errors, stop.
- Run `git branch -a | grep fix` — avoid duplicate branches.
## Verification
After fixing, run all three and confirm exit code 0:
```bash
npm run typecheck
npm run lint
npm run test
```
## Submit
If you made changes, create a PR with a title like "fix: resolve typecheck/lint/test failures" describing what broke and how you fixed it.
If everything was already passing (logs show no errors), do nothing.
- name: Post-change verification
if: always() && env.QUALITY_FAILED == '1'
run: |
echo "## Post-Fix Verification" >> $GITHUB_STEP_SUMMARY
if npm run typecheck && npm run lint && npm run test; then
echo "All checks passing." >> $GITHUB_STEP_SUMMARY
else
echo "Checks still failing after fix attempt." >> $GITHUB_STEP_SUMMARY
exit 1
fi
.github/workflows/security-audit.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Security Audit
on:
workflow_dispatch:
jobs:
security-audit:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
security-events: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Run npm audit
continue-on-error: true
run: |
echo "## npm audit results" >> $GITHUB_STEP_SUMMARY
echo "### workspace root (web, electron, packages/*)" >> $GITHUB_STEP_SUMMARY
npm audit --json > npm-audit.json 2>/dev/null || true
npm audit 2>/dev/null | tail -20 >> $GITHUB_STEP_SUMMARY || true
echo "### mobile" >> $GITHUB_STEP_SUMMARY
cd mobile && npm audit --json > npm-audit.json 2>/dev/null || true
npm audit 2>/dev/null | tail -20 >> $GITHUB_STEP_SUMMARY || true
- name: Run quality checks baseline
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, useEffect+fetch instead of useQuery, and prose throat-clearing."
prompt: |
# Security Audit
Audit NodeTool for security vulnerabilities and fix what you find.
## Scope
- `/web` — React frontend
- `/electron` — desktop app (Electron)
- `/mobile` — React Native app
## What to check
**Dependencies**: Read `*/npm-audit.json` for known CVEs. Fix critical/high severity by updating packages. Do not bump major versions without checking for breaking changes.
**Code patterns** (search for these):
- `dangerouslySetInnerHTML` without sanitization (DOMPurify)
- `eval()` or `new Function()` with user input
- Hardcoded API keys, tokens, or passwords in source files
- User input passed directly to DOM or URLs without validation
- File upload handlers without type/size validation
**Electron-specific**:
- `nodeIntegration` should be `false`
- `contextIsolation` should be `true`
- IPC messages should be validated
## Rules
- Fix real vulnerabilities only. Do not add speculative defenses or wrap everything in try/catch.
- One PR per audit. Keep changes focused and reviewable.
- Maximum 10 files changed.
- Do not touch CI workflows.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a security PR is already open.
## Verification
After fixing, all checks must still pass:
```bash
npm run typecheck && npm run lint && npm run test
```
## Submit
Create a PR titled "security: <brief description>" listing each vulnerability fixed and its severity.
- name: Post-change verification
if: always()
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/seo-seed.yml
Original GitHub Actions definition, preserved for the external orchestrator.
name: SEO Showcase Seed
# Layer-3 CI glue for the programmatic-SEO showcase pages: generate → guard →
# upload videos to R2 → ingest → open a review PR. Merging the PR is the index
# gate; NOTHING here auto-merges.
#
# Two triggers:
# - workflow_dispatch: a maintainer seeds a specific batch on demand.
# - schedule (weekly): a small default batch keeps the corpus growing. With no
# inputs, "Resolve run parameters" rotates one image template by ISO week.
#
# Required repo secrets:
# CLAUDE_CODE_OAUTH_TOKEN prompt writing (seed.ts, via the Claude Agent SDK)
# FAL_API_KEY image/video rendering (seed.ts)
# CLOUDFLARE_API_TOKEN R2 upload (wrangler) — same token marketing-ci uses
# CLOUDFLARE_ACCOUNT_ID R2 account (wrangler)
# The repo/org setting "Allow GitHub Actions to create and approve pull
# requests" must be ON for the PR step to post with GITHUB_TOKEN.
on:
workflow_dispatch:
inputs:
templates:
description: "Comma-separated template slugs (movie-posters, product-shots, album-covers, product-trailers)"
required: false
type: string
models:
description: "Comma-separated model slugs, e.g. flux-schnell,flux-dev"
required: false
type: string
count:
description: "Assets per template×model"
required: false
default: "5"
type: string
budget_usd:
description: "Hard USD cap per template batch"
required: false
default: "10"
type: string
duel:
description: "Duel two models over a curated prompt set, e.g. flux-schnell,flux-dev (overrides models)"
required: false
type: string
schedule:
# Weekly, Monday 07:00 UTC — a small default batch.
- cron: "0 7 * * 1"
# Least-privilege: the job reads the repo, then peter-evans opens the PR with the
# default GITHUB_TOKEN (needs write + PR create).
permissions:
contents: write
pull-requests: write
concurrency:
group: seo-seed
cancel-in-progress: false
jobs:
seed:
name: Seed, guard, ingest, open PR
runs-on: ubuntu-latest
env:
# R2 target for video assets. Override here if the bucket / media domain
# differ from the defaults baked into upload-videos-r2.mjs.
R2_BUCKET: nodetool-media
R2_PUBLIC_BASE: https://media.nodetool.ai
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Resolve run parameters
id: params
env:
IN_TEMPLATES: $
IN_MODELS: $
IN_COUNT: $
IN_BUDGET: $
IN_DUEL: $
RUN_NUMBER: $
run: |
set -euo pipefail
# Scheduled runs carry no inputs → rotate one image template by ISO
# week so the weekly batch varies. flux-schnell is the cheap default.
WEEK=$(date -u +%V)
ROTATION=(movie-posters product-shots album-covers)
DEFAULT_TEMPLATE=${ROTATION[$((10#$WEEK % 3))]}
TEMPLATES="${IN_TEMPLATES:-$DEFAULT_TEMPLATE}"
MODELS="${IN_MODELS:-flux-schnell}"
COUNT="${IN_COUNT:-5}"
BUDGET="${IN_BUDGET:-10}"
DUEL="${IN_DUEL:-}"
BATCH_DATE=$(date -u +%Y%m%d)
{
echo "templates=$TEMPLATES"
echo "models=$MODELS"
echo "count=$COUNT"
echo "budget=$BUDGET"
echo "duel=$DUEL"
echo "batch_suffix=$BATCH_DATE-$RUN_NUMBER"
} >> "$GITHUB_OUTPUT"
echo "Resolved: templates=$TEMPLATES models=$MODELS count=$COUNT budget=$BUDGET duel=${DUEL:-<none>}"
- name: Install root dependencies
run: npm ci
- name: Build backend packages
# seed.ts imports @nodetool-ai/* from dist/.
run: npm run build:packages
- name: Install marketing dependencies
working-directory: marketing
run: npm ci
- name: Seed batches
working-directory: marketing
env:
CLAUDE_CODE_OAUTH_TOKEN: $
FAL_API_KEY: $
TEMPLATES: $
MODELS: $
COUNT: $
BUDGET: $
DUEL: $
BATCH_SUFFIX: $
run: |
set -euo pipefail
IFS=',' read -ra TPL <<< "$TEMPLATES"
for t in "${TPL[@]}"; do
t=$(echo "$t" | xargs) # trim
[ -z "$t" ] && continue
BATCH="$t-$BATCH_SUFFIX"
if [ -n "$DUEL" ]; then
echo "::group::seed $t (duel $DUEL)"
npm run seo:seed -- --template "$t" --duel "$DUEL" \
--budget-usd "$BUDGET" --batch "$BATCH"
else
echo "::group::seed $t (models $MODELS)"
npm run seo:seed -- --template "$t" --models "$MODELS" \
--count "$COUNT" --budget-usd "$BUDGET" --batch "$BATCH"
fi
echo "::endgroup::"
done
- name: Guard manifest (row count + spend vs cap)
working-directory: marketing
run: npm run seo:guard -- --budget-usd "$"
- name: Upload videos to R2
working-directory: marketing
env:
CLOUDFLARE_API_TOKEN: $
CLOUDFLARE_ACCOUNT_ID: $
run: npm run seo:upload-videos
- name: Ingest into page data
working-directory: marketing
run: npm run seo:ingest
- name: Open review PR
uses: peter-evans/create-pull-request@v8
with:
branch: seo/seed-$
base: main
# Commit ONLY the ingested page data + published images. Raw batches
# (marketing/seo/out) and R2-hosted videos are deliberately excluded.
add-paths: |
marketing/src/data/showcaseEntries.generated.ts
marketing/public/showcase/**
commit-message: "chore(seo): seed showcase batch $"
title: "SEO showcase seed: $ ($)"
body: |
Automated showcase seed batch. **Review is the index gate — this does not auto-merge.**
Run parameters:
- Templates: `$`
- Models: `$`
- Duel: `$`
- Count: `$`
- Budget cap (per batch): `$$`
Review checklist before merging:
- [ ] Outputs eyeballed — no broken, blank, or off-brand assets
- [ ] Prompts read as human-written, not templated boilerplate
- [ ] No duplicate slugs / near-duplicate prompts slipped the gate
- [ ] Committed image weight is within budget (each `<= 300KB`)
- [ ] Total spend is at or under the cap (see the guard step log)
- [ ] Every video `src` resolves from R2 (not a local path)
delete-branch: true
.github/workflows/shipped-feature-inliner.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Shipped Feature Inliner
# A flag for a feature that shipped is a branch nobody takes, a config knob
# nobody sets, and a second code path nobody tests. This finds gates that are
# permanently on — constants initialized to `true`, env vars that default to
# enabled and are never set, settings whose "off" branch no test and no caller
# exercises — and inlines the live branch.
#
# Inlining a flag deletes the fallback. That is the point, and it is also the
# risk: the routine only removes a flag when the off-path is provably unused,
# and the rollback is a revert.
on:
schedule:
- cron: "0 8 * * *" # Daily at 08:00 UTC
workflow_dispatch:
jobs:
shipped-feature-inliner:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Find flags that are always on
id: scan
continue-on-error: true
shell: bash
run: |
node --input-type=module - <<'JS' > shipped-flags.log
import { readFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
// The config corpus is tens of megabytes; the 1 MB default throws ENOBUFS.
const sh = (command) =>
execFileSync("bash", ["-c", command], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
const sourceFiles = sh(
"git ls-files 'web/src/*.ts' 'web/src/*.tsx' 'electron/src/*.ts' " +
"'mobile/src/*.ts' 'mobile/src/*.tsx' 'packages/*/src/*.ts' " +
"| grep -vE '__tests__|\\.test\\.|\\.spec\\.|__mocks__|/generated/'",
)
.split("\n")
.filter(Boolean);
// Everything that could set a variable outside the source itself:
// env files, CI, container and deploy config, scripts, docs.
const configCorpus = sh(
"git ls-files | grep -vE '^(web|electron|mobile)/src/|^packages/[^/]+/src/' " +
"| grep -E '\\.(env|env\\..*|ya?ml|json|toml|sh|mjs|md|Dockerfile.*)$|Dockerfile|\\.env' " +
"| head -400 | xargs -r cat 2>/dev/null",
);
// Boolean-shaped gates only. A key or a URL read from the environment
// is configuration; `X === "1"` is a switch.
const GATE =
/(?:process|import\.meta)\.env\.([A-Z][A-Z0-9_]*)\s*(?:!==|===|==|!=)\s*["'](?:1|0|true|false)["']|(?:process|import\.meta)\.env\.([A-Z][A-Z0-9_]*)\s*(?:\?\?|\|\|)\s*["'](?:1|0|true|false)["']|Boolean\(\s*(?:process|import\.meta)\.env\.([A-Z][A-Z0-9_]*)\s*\)/g;
const gates = new Map();
for (const file of sourceFiles) {
const source = readFileSync(file, "utf8");
for (const match of source.matchAll(GATE)) {
const name = match[1] ?? match[2] ?? match[3];
const entry = gates.get(name) ?? { reads: 0, files: new Set() };
entry.reads += 1;
entry.files.add(file);
gates.set(name, entry);
}
}
const rows = [...gates.entries()]
.map(([name, entry]) => ({
name,
reads: entry.reads,
files: [...entry.files],
// Set anywhere outside the reading code? Then somebody flips it.
setInConfig: new RegExp(`${name}\\s*[:=]`).test(configCorpus),
}))
.sort((a, b) => Number(a.setInConfig) - Number(b.setInConfig) || a.reads - b.reads);
console.log("## Boolean env gates (never set in the repo first)");
for (const row of rows) {
console.log(
` ${row.name} reads=${row.reads} set-in-config=${row.setInConfig} ${row.files.join(" ")}`,
);
}
console.log("");
console.log("## Constants pinned to a boolean literal");
for (const file of sourceFiles) {
const source = readFileSync(file, "utf8");
for (const match of source.matchAll(
/^(?:export )?const ([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=]+)?=\s*(true|false)(?: as const)?;/gm,
)) {
console.log(` ${file} ${match[1]} = ${match[2]}`);
}
}
JS
echo "## Candidate Flags" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
head -50 shipped-flags.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
# The two section headers print unconditionally; findings are indented.
FLAGS=$(grep -c "^ " shipped-flags.log || true)
echo "Findings: $FLAGS" >> $GITHUB_STEP_SUMMARY
if [ "${FLAGS:-0}" -gt 0 ]; then echo "FLAGS_FOUND=1" >> $GITHUB_ENV; else echo "FLAGS_FOUND=0" >> $GITHUB_ENV; fi
- name: Run quality checks baseline
if: env.FLAGS_FOUND == '1'
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
if: env.FLAGS_FOUND == '1'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, and prose throat-clearing."
prompt: |
# Inline a Shipped Feature Flag
`shipped-flags.log` has two sections. **Boolean env gates** —
environment variables compared against `"1"`/`"true"`, with the
number of read sites and whether anything in the repo sets them;
those never set, read in one place, are listed first. **Constants
pinned to a boolean literal** — a gate whose value is in the source.
Pick **one** flag whose feature has fully shipped and remove it.
## Establish that it has shipped
Answer each with a command, in the PR body:
- What value does it actually take in every build? Read the
declaration and every override (`.env*`, `fly.toml`,
`docker-compose.yml`, `electron-builder.json`, CI workflows,
`packages/config`).
- Does anything set the other value? `grep -rn "<FLAG>" .` including
docs, scripts, and workflow files.
- When did it last change? `git log --oneline -- <file>` and
`git log -S "<FLAG>" --oneline | tail -5` for when it was
introduced. A gate added last month is not a shipped feature.
- Do any tests exercise the off-path? If they do, that path is alive
— pick a different flag.
A flag qualifies only when every build takes one branch, nothing in
the repo sets the other, and the feature has been live long enough
that reverting the flag is not the rollback plan anymore.
## Then inline it
- Replace every read with the value it always has, and delete the
branch that value never takes.
- Delete the flag's declaration, its type, its config plumbing, its
env-var documentation, and any settings-UI control for it.
- Delete the now-unreachable implementation on the other side —
including files, components, and helpers only that path used.
- Delete the tests that only covered the dead path. Keep, and if
necessary un-parameterize, the tests covering the live one.
- Update the docs that mention the flag: `docs/`, `AGENTS.md`,
`README.md`, `.env.example`.
## Rules
- One flag per PR. Maximum 20 files.
- Never inline a **kill switch** or a safety gate: rate limits, spend
caps, SSRF and sandbox guards, `NODETOOL_TRUST_*`, auth mode. Those
are configuration, not shipped features.
- Never inline a flag that selects between platforms or environments
(`NODETOOL_ENV`, `process.platform`, dev vs production).
- Never inline a flag that any test sets.
- Do not touch CI workflow files.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a flag-inlining
PR is already open.
## Verification
```bash
npm run typecheck && npm run lint && npm run test
```
Backend packages also need `npm run test:packages`. Confirm the flag
name appears nowhere afterwards: `grep -rn "<FLAG>" . | grep -v .git`
## Submit
Create a PR titled "chore: inline the <feature> flag" with the
evidence that it shipped, the branch that was removed, and everything
deleted along with it.
- name: Post-change verification
if: always() && env.FLAGS_FOUND == '1'
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/test-coverage.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Test Coverage Improvement
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
jobs:
test-coverage:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Run coverage report
id: coverage
continue-on-error: true
run: |
echo "## Coverage Report" >> $GITHUB_STEP_SUMMARY
cd web && npx vitest run --coverage --reporter=json --outputFile=coverage-report.json 2>&1 | tee coverage.log || true
echo "### Web Coverage" >> $GITHUB_STEP_SUMMARY
tail -20 coverage.log >> $GITHUB_STEP_SUMMARY || true
cd ..
cd electron && npx vitest run --coverage --reporter=json --outputFile=coverage-report.json 2>&1 | tee coverage.log || true
echo "### Electron Coverage" >> $GITHUB_STEP_SUMMARY
tail -20 coverage.log >> $GITHUB_STEP_SUMMARY || true
cd ..
- name: Run quality checks baseline
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, useEffect+fetch instead of useQuery, and prose throat-clearing."
prompt: |
# Improve Test Coverage
Add meaningful tests for untested or under-tested code in the NodeTool codebase.
## Scope
- `/web/src` — React frontend
- `/electron/src` — desktop app
## How to find gaps
1. Read the coverage reports: `web/coverage.log` and `electron/coverage.log`
2. Identify source files with low or zero coverage
3. Prioritize: utility functions > store logic > hooks > components
## What to test
**Utility functions**: Pure functions in utils/helpers directories. Test edge cases, error conditions, and typical usage.
**Zustand store actions**: Test store actions produce the correct state changes. Use the pattern from existing tests.
**Custom hooks**: Test hooks using `renderHook` from React Testing Library. Test state changes and side effects.
**React components**: Test rendering, user interactions, and conditional display. Follow existing patterns using React Testing Library.
## Rules
- Follow existing test patterns and conventions in the codebase. Read existing test files first.
- Place test files adjacent to source files as `__tests__/<filename>.test.ts(x)` matching the existing convention.
- Use the same test utilities and mocking patterns already in the codebase.
- Do not mock what you can test directly. Prefer integration-style tests.
- Do not add tests for trivial code (simple pass-through components, re-exports).
- Do not modify existing tests unless they are broken.
- Do not touch `@ts-expect-error` / `@ts-ignore` directives.
- Do not touch `test.skip` / `describe.skipIf` / `test.fixme` markers.
- Do not touch CI workflow files.
- Keep changes focused — maximum 5 new test files per PR.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a test coverage PR is already open.
- Read 2-3 existing test files to understand the project's testing conventions.
## Verification
After adding tests, all checks must pass:
```bash
npm run typecheck && npm run lint && npm run test
```
## Submit
Create a PR titled "test: add coverage for <area>" listing each new test file and what it covers.
- name: Post-change verification
if: always()
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/ui-primitives-compliance.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: UI Primitives & Design Tokens
# Enforces the two MANDATORY frontend rules from AGENTS.md that no generic
# linter covers:
# 1. Never import raw MUI components outside ui_primitives/ or editor_ui/
# (use the primitives in web/src/components/ui_primitives/ instead).
# 2. Never hardcode design tokens — border radii, transition strings, font
# sizes, or off-grid spacing. Use SPACING / TYPOGRAPHY / BORDER_RADIUS /
# MOTION / Z_INDEX.
#
# There is a large existing backlog, so this migrates a small, verified batch
# per run rather than trying to fix everything at once. Reporting + a focused
# PR — it never auto-merges.
on:
schedule:
- cron: "0 3 * * 2" # Tuesdays at 03:00 UTC
workflow_dispatch:
jobs:
ui-primitives-compliance:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Scan for design-system violations
id: ds-scan
continue-on-error: true
run: |
echo "## UI Primitives & Design Token Scan" >> $GITHUB_STEP_SUMMARY
ISSUES=0
# Raw MUI imports outside the allowed directories.
RAW_MUI=$(grep -rlE "from ['\"]@mui/material" web/src --include="*.tsx" --include="*.ts" 2>/dev/null \
| grep -v "ui_primitives/" | grep -v "editor_ui/" | wc -l || echo 0)
echo "- Files with raw @mui/material imports (outside ui_primitives/editor_ui): $RAW_MUI" >> $GITHUB_STEP_SUMMARY
if [ "$RAW_MUI" -gt 0 ]; then ISSUES=1; fi
# Hardcoded transition strings.
RAW_TRANSITION=$(grep -rlE "transition:\s*['\"][^'\"]*[0-9]+m?s" web/src --include="*.tsx" --include="*.ts" 2>/dev/null \
| grep -v "ui_primitives/" | wc -l || echo 0)
echo "- Files with hardcoded transition strings: $RAW_TRANSITION" >> $GITHUB_STEP_SUMMARY
if [ "$RAW_TRANSITION" -gt 0 ]; then ISSUES=1; fi
# Hardcoded px font sizes.
RAW_FONT=$(grep -rlE "fontSize:\s*['\"][0-9.]+(px|rem)['\"]" web/src --include="*.tsx" --include="*.ts" 2>/dev/null \
| grep -v "ui_primitives/" | wc -l || echo 0)
echo "- Files with hardcoded fontSize values: $RAW_FONT" >> $GITHUB_STEP_SUMMARY
if [ "$RAW_FONT" -gt 0 ]; then ISSUES=1; fi
echo "DS_ISSUES=$ISSUES" >> $GITHUB_ENV
- name: Run quality checks baseline
id: pre-check
continue-on-error: true
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
echo "TYPECHECK_PRE_EXIT=$TYPECHECK_EXIT" >> $GITHUB_ENV
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
if: env.DS_ISSUES == '1'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, useEffect-for-derived-data, raw MUI imports outside ui_primitives/, whole-store Zustand subscriptions, useEffect+fetch instead of useQuery, and prose throat-clearing."
prompt: |
# Migrate to UI Primitives & Design Tokens
Enforce NodeTool's two MANDATORY frontend rules in `web/src`. Read the
policy first — do not guess the mapping:
- `web/src/components/ui_primitives/STRATEGY.md` — decision tree,
migration rules, and the full catalog of 90+ primitives.
- `docs/DESIGN.md` — the token systems (SPACING, TYPOGRAPHY,
BORDER_RADIUS, MOTION, Z_INDEX).
## What to fix
**Raw MUI imports**: Files importing components from `@mui/material`
(`Typography`, `Button`, `IconButton`, `Tooltip`, `CircularProgress`,
`Chip`, `Dialog`, `Alert`, `Divider`, `Paper`, etc.) outside
`ui_primitives/` and `editor_ui/`. Replace each with the matching
primitive from `web/src/components/ui_primitives/`. If a primitive
does not exist for a component you need, leave that file alone and note
it in the PR body — do NOT invent a new primitive in this PR.
**Hardcoded design tokens**: Replace hardcoded border radii (`4`,
`10`, `"18px"`), transition strings (`"all 200ms ease"`), font sizes
(`"14px"`, `"0.85rem"`), and off-grid spacing (`5px`, `10px`, `13px`)
with the named constants (`BORDER_RADIUS`, `MOTION`, `TYPOGRAPHY`,
`SPACING`) from `ui_primitives`.
## Rules
- Verify every mapping against STRATEGY.md and DESIGN.md. When a raw
component has no clean primitive equivalent, skip it.
- Do not change runtime behavior or visual appearance — swap the import
and token, keep the same rendered result.
- Do not touch `ui_primitives/` or `editor_ui/` internals (they are
allowed to use raw MUI).
- Do not touch CI workflow files.
- Keep changes focused — maximum 10 files per PR. Pick a coherent
batch (e.g. one feature directory) so review is easy.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a primitives/design
PR is already open.
- Pick 5-10 files with the most violations or in one area.
## Verification
After changes, all checks must still pass:
```bash
npm run typecheck && npm run lint && npm run test
```
## Submit
Create a PR titled "ui: migrate <area> to primitives/design tokens"
listing each file changed, the raw components/tokens removed, and any
files skipped for lack of a primitive.
- name: Post-change verification
if: always() && env.DS_ISSUES == '1'
run: |
TYPECHECK_EXIT=0; LINT_EXIT=0; TEST_EXIT=0
npm run typecheck 2>&1 || TYPECHECK_EXIT=1
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
if [ "$TYPECHECK_PRE_EXIT" -eq 0 ] && [ $TYPECHECK_EXIT -ne 0 ]; then
echo "New TypeScript errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
.github/workflows/useless-test-pruner.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Useless Test Pruner
# A test that cannot fail is worse than no test: it costs CI time and it reads
# as coverage. This finds the usual shapes — no assertion at all, assertions
# only about mocks the test itself wrote, tautologies, snapshots of a literal —
# and requires proof before anything is deleted.
#
# The proof is a mutation: break the code the test claims to cover, run the test,
# and watch it pass anyway. Only then is it established that the test cannot
# fail. A candidate that turns red under mutation is doing its job and stays.
on:
schedule:
- cron: "30 7 * * *" # Daily at 07:30 UTC
workflow_dispatch:
jobs:
useless-test-pruner:
runs-on: ubuntu-latest
timeout-minutes: 90
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
- name: Install mobile dependencies
run: cd mobile && npm ci
- name: Find tests that cannot fail
id: scan
continue-on-error: true
shell: bash
run: |
node --input-type=module - <<'JS' > useless-tests.log
import { readFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
const files = execFileSync(
"bash",
[
"-c",
// Playwright suites are out of scope: the proof loop below runs
// single cases under Jest and Vitest.
"git ls-files '*.test.ts' '*.test.tsx' '*.spec.ts' '*.spec.tsx' " +
"| grep -vE '^(marketing|reliability)/|^web/(tests|e2e)/'",
],
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024 },
)
.split("\n")
.filter(Boolean);
// Split a test file into cases by `it(`/`test(` at the start of a line,
// which is how every suite in this repo is written. `describe` is not
// a case: counting it as one would report every suite whose header
// sits above its first assertion.
const splitCases = (source) => {
const lines = source.split("\n");
const cases = [];
let current = null;
for (let i = 0; i < lines.length; i += 1) {
const start = /^\s*(it|test)(\.(each|concurrent|failing|only))?\s*[(`]/.test(lines[i]);
if (start) {
if (current) cases.push(current);
current = { line: i + 1, body: [] };
}
if (current) current.body.push(lines[i]);
}
if (current) cases.push(current);
return cases;
};
const findings = [];
for (const file of files) {
const source = readFileSync(file, "utf8");
for (const testCase of splitCases(source)) {
const body = testCase.body.join("\n");
const title = testCase.body[0].trim().slice(0, 90);
const reasons = [];
const assertions = body.match(/\bexpect\s*\(|\bassert[.(]/g) ?? [];
if (assertions.length === 0) {
reasons.push("no assertion");
}
if (/expect\(true\)\.(toBe|toEqual)\(true\)|expect\(1\)\.toBe\(1\)/.test(body)) {
reasons.push("tautological assertion");
}
// Every assertion is `toBeDefined`/`toBeTruthy` on something the
// test just constructed: it asserts that JavaScript works.
if (
assertions.length > 0 &&
/expect\([^)]*\)\.(toBeDefined|toBeTruthy|not\.toBeNull|not\.toBeUndefined)\(\)/.test(body) &&
!/toBe\(|toEqual\(|toMatch|toThrow|toHaveBeenCalledWith|toContain|toStrictEqual/.test(body)
) {
reasons.push("only existence assertions");
}
// Asserting a mock returns what the same test told it to return.
// The subject of every assertion has to be the mock itself —
// asserting the *code under test* while a dependency is mocked is
// just normal testing.
const subjects = [...body.matchAll(/expect\(\s*([A-Za-z0-9_.]+)/g)].map((m) => m[1]);
if (
subjects.length > 0 &&
/mockReturnValue|mockResolvedValue|mockImplementation/.test(body) &&
!/toHaveBeenCalled/.test(body) &&
subjects.every((subject) => /^mock|Mock$|^jest\./.test(subject))
) {
reasons.push("asserts its own mock");
}
if (/expect\([^)]*\)\.toMatchSnapshot\(\)/.test(body) && assertions.length === 1) {
reasons.push("snapshot-only");
}
if (reasons.length) {
findings.push(`${file}:${testCase.line} [${reasons.join(", ")}] ${title}`);
}
}
}
for (const finding of findings.slice(0, 60)) console.log(finding);
console.error(`${findings.length} candidates`);
JS
echo "## Tests That May Not Be Able To Fail" >> $GITHUB_STEP_SUMMARY
CANDIDATES=$(wc -l < useless-tests.log | tr -d ' ')
echo "Candidates: $CANDIDATES" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
head -40 useless-tests.log >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
if [ "$CANDIDATES" -gt 0 ]; then echo "USELESS_FOUND=1" >> $GITHUB_ENV; else echo "USELESS_FOUND=0" >> $GITHUB_ENV; fi
- name: Run quality checks baseline
if: env.USELESS_FOUND == '1'
id: pre-check
continue-on-error: true
run: |
LINT_EXIT=0; TEST_EXIT=0; PKG_TEST_EXIT=0
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
npm run test:packages 2>&1 || PKG_TEST_EXIT=1
echo "LINT_PRE_EXIT=$LINT_EXIT" >> $GITHUB_ENV
echo "TEST_PRE_EXIT=$TEST_EXIT" >> $GITHUB_ENV
echo "PKG_TEST_PRE_EXIT=$PKG_TEST_EXIT" >> $GITHUB_ENV
- name: Run Claude Code
if: env.USELESS_FOUND == '1'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff. Strip speculative abstractions, narrating comments, defensive try/catch on trusted paths, and prose throat-clearing."
prompt: |
# Delete Tests That Cannot Fail
`useless-tests.log` lists test cases whose shape suggests they assert
nothing real: no assertion, a tautology, existence checks only, an
assertion about a mock the test itself configured, a lone snapshot.
The scan is a heuristic. **Nothing gets deleted without proof.**
## The proof
For each candidate, before touching it:
1. Find the code it claims to cover.
2. Break that code — invert a condition, return a wrong constant,
drop a call. One mutation at a time.
3. Run only that test:
```bash
npm test --workspace=web -- -t "<test name>"
npm run test --workspace=packages/<name> -- -t "<test name>"
```
4. If it **still passes**, it cannot fail: it is a candidate for
real. If it **fails**, the test is doing its job — revert the
mutation, drop the candidate, and say so.
5. Revert the mutation before moving on. `git diff` must be clean of
it before you commit anything.
Paste the mutation and the result for every case you act on.
## Then choose: strengthen or delete
- **Strengthen** when the behavior deserves coverage and the test is
merely weak: assert the value, not its existence; assert
`toHaveBeenCalledWith`, not that a mock was configured; assert the
error, not that something threw.
- **Delete** when the behavior is already covered elsewhere, or the
test only exercises the framework, the mock, or the type system.
Name the test that covers it instead, or say plainly that the
behavior is untested and worth someone's attention.
Prefer strengthening. A deleted test is coverage lost; a strengthened
one is coverage gained.
## Rules
- Never delete a test to make CI green. This routine runs against a
green tree.
- Do not touch `test.skip`, `describe.skipIf`, or `test.fixme`
markers — those are tracked debt, not useless tests (AGENTS.md).
- Do not delete a test whose mutation you could not run.
- Snapshot tests over rendered components are not automatically
useless — check whether the snapshot has content.
- Do not touch CI workflow files.
- Maximum 12 test cases per PR.
## Before starting
- Run `gh pr list --state open --limit 20` — skip if a test-pruning
PR is already open.
## Verification
```bash
npm run lint && npm run test && npm run test:packages
```
## Submit
Create a PR titled "test: remove tests that cannot fail in <area>"
with a table: test, mutation applied, result, verdict (strengthened /
deleted), and where the behavior is covered now.
- name: Post-change verification
if: always() && env.USELESS_FOUND == '1'
run: |
LINT_EXIT=0; TEST_EXIT=0; PKG_TEST_EXIT=0
npm run lint 2>&1 || LINT_EXIT=1
npm run test 2>&1 || TEST_EXIT=1
npm run test:packages 2>&1 || PKG_TEST_EXIT=1
if [ "$LINT_PRE_EXIT" -eq 0 ] && [ $LINT_EXIT -ne 0 ]; then
echo "New lint errors introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$TEST_PRE_EXIT" -eq 0 ] && [ $TEST_EXIT -ne 0 ]; then
echo "New test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
if [ "$PKG_TEST_PRE_EXIT" -eq 0 ] && [ $PKG_TEST_EXIT -ne 0 ]; then
echo "New package test failures introduced" >> $GITHUB_STEP_SUMMARY; exit 1
fi
- name: Confirm no mutation was committed
if: always() && env.USELESS_FOUND == '1'
shell: bash
run: |
# A mutation left behind by the proof step would land as a source
# change in a PR that is meant to touch tests only.
BASE="origin/$"
if ! git rev-parse --verify --quiet "$BASE" > /dev/null; then
echo "Base ref $BASE not available — skipping the test-only check" >> $GITHUB_STEP_SUMMARY
exit 0
fi
CHANGED_SRC=$(git diff --name-only "$BASE"...HEAD \
| grep -vE "\.test\.|\.spec\.|__tests__|__snapshots__" || true)
if [ -n "$CHANGED_SRC" ]; then
echo "Non-test files changed by a test-pruning run:" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CHANGED_SRC" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
.github/workflows/workflow-example-validation.yaml
Original GitHub Actions definition, preserved for the external orchestrator.
name: Workflow Example Validation
# Guards the shipped example workflows against registry drift. Nodes get
# renamed, properties change, models get deprecated — and a shipped example that
# references a now-unknown node type or a missing required property is broken for
# every new install. `nodetool validate` catches this statically (unknown node
# types, missing required props, unselected models, dangling/mis-typed edges)
# without running the workflow.
#
# The scan runs `validate` over every example JSON. If any fail, Claude repairs
# them against the current node registry and opens a PR. Never auto-merges.
on:
schedule:
- cron: "0 3 * * 4" # Thursdays at 03:00 UTC
workflow_dispatch:
jobs:
workflow-example-validation:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
issues: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "npm"
- name: Install all workspace dependencies
run: npm ci
env:
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
# validate loads the node registry, which the decorator packages
# (base-nodes, node-sdk, ...) expose from dist/.
- name: Build packages
run: npm run build:packages
- name: Validate shipped examples
id: validate
continue-on-error: true
run: |
echo "## Workflow Example Validation" >> $GITHUB_STEP_SUMMARY
BROKEN=0
: > validation-failures.log
while IFS= read -r -d '' example; do
if npm run dev:nodetool -- validate "$example" --json > /tmp/v.json 2>/tmp/v.err; then
echo "- OK: $example" >> $GITHUB_STEP_SUMMARY
else
BROKEN=1
echo "- FAIL: $example" >> $GITHUB_STEP_SUMMARY
{
echo "### $example"
cat /tmp/v.json 2>/dev/null || cat /tmp/v.err
echo
} >> validation-failures.log
fi
done < <(find packages -path '*examples*' -name '*.json' ! -name '*.app.json' -print0)
echo "EXAMPLES_BROKEN=$BROKEN" >> $GITHUB_ENV
- name: Run Claude Code
if: env.EXAMPLES_BROKEN == '1'
uses: anthropics/claude-code-action@v1.0.210
with:
claude_code_oauth_token: $
additional_permissions: |
actions: read
claude_args: |
--model claude-opus-5
--allowedTools "Bash,Edit,Read,Replace,CreatePullRequest"
--append-system-prompt "Before finalizing any code change or PR, invoke the unslop skill (.claude/skills/unslop/SKILL.md) and apply its checklist to your diff."
prompt: |
# Repair Broken Shipped Workflow Examples
Some shipped example workflows no longer validate against the current
node registry. `validation-failures.log` lists each broken example
with its validation report (unknown node types, missing required
properties, unselected models, dangling or mis-typed edges).
## How to fix
For each broken example JSON:
- Read the validation report to see exactly what's wrong.
- Re-validate a single file with:
`npm run dev:nodetool -- validate "<path>" --json`
- Fix the graph to match the current registry:
- Unknown node type → find the renamed/replacement node type and
update it (search the codebase for the node's `@node`/type).
- Missing required property → add it with a sensible default.
- Unselected model → pick a currently-available model of the right
kind for that provider.
- Dangling/mis-typed edge → repair or remove the edge so source and
target handles exist and their types are compatible.
- Preserve the example's intent. These are user-facing templates —
keep them meaningful, do not gut them to make validation pass.
## Rules
- Only edit example JSON files under `packages/**/examples/`.
- Do not change node source code to fit a broken example — fix the
example to match the code.
- Do not touch CI workflow files.
## Verification
Re-run validate on every file you touched and confirm it passes:
```bash
npm run dev:nodetool -- validate "<path>"
```
## Submit
Create a PR titled "examples: repair workflows broken by registry drift"
listing each example fixed and what changed (node renamed, property
added, model reselected, edge repaired). If every example already
validates, do nothing.