ForgeApps · Repo Roast · 2026-09-09

Top 10 Friction Fixes

Ten things that make working in forgeapps slower or more confusing than it needs to be, each fixable in under 60 minutes. Read-only audit on branch dev at 4c00c4c81, five parallel probes. Security findings were held out on request and are available separately.

10fixes, all under 60 min
~4.5 htotal estimated effort
4%of tests the root runner sees
75dirty files on a "clean" checkout

TLDR

The repo's feedback loops are the friction: local git is broken so the four main dev commands cannot run, the gates that do run are already red, the root test command sees 4% of the tests, and git status is permanently dirty. Nobody, human or agent, can tell "I broke something" from "it was like that."

Top 10 Friction Sources

#FrictionEvidenceWho it slowsConf.TimeFirst fix
1check, lint, typecheck, build all die on this Mac: bare git is blocked by the Xcode licensepnpm repo:check:changed → "You have not agreed to the Xcode license"; package.json:12-15 route through --changed; ~15 scripts call bare gitEvery session on adamobook, every pre-commit hookHigh5 minsudo xcodebuild -license accept (or point xcode-select at CommandLineTools)
2Standing red gates: contract test fails, 9 vitest failures, all stale prose assertionsscripts/__tests__/repo-check.test.ts:195 (sheet-localizer lint); check-kubota-containment.ts:52; 4 skill test files asserting old SKILL.md wordingAnyone trying to prove a change is safeHigh45 minLint exemption for sheet-localizer; fix powerpoint/tests/check-skill.test.ts:127 first
3git status is never clean: 75 dirty files, 1,077 append-only ledgers, two unignored dirs, a routine erroring hourlycrons/**/history/*.jsonl ×1,077 (24 of 75 dirty); .remember/ and tmp/ untracked+unignored; fleet-drift writes ERRORS.md every hour since crons/fleet/ was deleted in 46b348faaEvery land, every "did I change anything?" checkHigh30 minIgnore .remember/, fix the !/tmp/ block at .gitignore:291, restore crons/fleet/
4Root pnpm test runs 57 of ~1,400 tests; crons and skills have no test script at allconfig/vitest/vitest.config.mts:5-70 hand-lists globs; crons/package.json and .forgebot/skills/package.json have no scripts; 200 cron tests + ~470 skill tests never runAgents told "run the tests" get a false greenHigh30 minAdd "test": "vitest run" to both packages; skip-guard the 3 live-network Slack/Fireflies tests first
5Per-app test wiring lies: forgefarm borrows the skills suite, forgescene is "exempt" but ships 7 tests, forgepoll/ara pass on passWithNoTestsapps/forgefarm has a test script and no vitest config, so it resolves the root config; apps/forgescene/package.json exemption says "No automated test suite"; apps/forgepoll/vitest.config.ts:11repo:check --changed on those appsHigh20 minAdd apps/forgefarm/vitest.config.ts with root: __dirname; delete forgescene's stale exemption
6Toolchain nags on every command: Node engine warning, vitest v2 root vs v3 majority, a literal placeholder in the workspace fileNo .nvmrc; engines 24.x, machine 25.8 → [WARN] Unsupported engine per pnpm call; pnpm-workspace.yaml:92 '@google/genai': set this to true or false; root vitest ^2.1.9 vs 57 packages on ^3.2.4; package.json workspaces still lists apps and artifactsEveryone, constantly, in small dosesHigh20 minAdd .nvmrc = 24, set genai to false, bump root vitest to ^3.2.4, drop the two dead workspaces entries
7Registries drift from disk: apps-metadata misses 13 apps and lists a ghost, launch.json misses 2 routines and double-books a portapps/_registry/apps-metadata.json: 70 entries vs 83 folders; forgelessons has no folder; .claude/launch.json lacks fleet-drift and forge-valley report entries; example and example-local both on 4075; a sync script exists and was not run/start, FleetView, anything walking the registryHigh20 minRun crons/routines/routines-maintenance/sync-launch-json.ts; seed the 13 apps from scripts/gen-app-build-inputs.ts
8Slack channel context is split and holey, so ForgeBot answers client channels coldchannels/greg-forgebot + greg_forgebot share one ID; sentry_adamobot + sentry_forgebot share one ID; 5 proposal channels have no CHANNEL.md; 8 registry rows point at missing folders; 394 hardcoded channel IDs have no name anywhereEvery Slack-facing routine and agentHigh40 minMerge the two duplicate pairs, scaffold 5 files from channels/_template, name the top 10 IDs
9"Is this routine scheduled?" cannot be answered from the repo.hermes/cron/jobs.json holds 1 job for 36 routines; 14 routines' last REPORT is dated 2026-07-12, 5 have no date at all; only forgebuild and forgesales are currentAnyone debugging why a watchdog went quietMed30 minAdd a schedule: frontmatter line to every ROUTINE.md; have routines-maintenance assert it
10Template copy-paste debt: 77 byte-identical API handlers, 16 placeholder crons, 11 SecretsPage variantshealth ×12, sentry-issues ×12, github-status ×11, vercel-deployments ×11, doppler-set ×11 (md5-identical); 15 apps schedule a no-op /api/cron/hourly hourly; SecretsPage has 5 distinct hashesEvery fix becomes an N-way editHigh30 minDelete the placeholder crons from 15 vercel.json; extract github-status.ts into a package as the proof

Technical Details

1 The dev loop is dead on this machine High

What's wrong
/usr/bin/git is an Apple shim that refuses to run until the Xcode license is accepted. which -a git returns only that shim; the working binary at /Library/Developer/CommandLineTools/usr/bin/git is not on PATH.
Evidence
pnpm repo:check:changed → Command failed: git diff --name-only … You have not agreed to the Xcode license agreements. package.json:12-15: check, lint, typecheck, build all pass --changed. Bare git callers include scripts/check-kubota-containment.ts:52, scripts/verify-root-guard.mjs, scripts/vercel-ignore-step.mjs, .forgebot/skills/vercel/scripts/check-push-batching.ts.
Why it matters
Four primary commands and the pre-commit hooks cannot run locally. This is a machine fix, but it is the single biggest friction source today.
First fix
sudo xcodebuild -license accept, or sudo xcode-select --switch /Library/Developer/CommandLineTools. Optional repo hardening: let scripts honour a GIT_BINARY env override.
Verification
git --version prints a version; pnpm check runs.

2 The gates that do run are already red High

What's wrong
pnpm repo:contract:test fails on two counts. pnpm test fails 9 tests in 4 files. All nine are string assertions that a SKILL.md contains a sentence that was since reworded.
Evidence
scripts/__tests__/repo-check.test.ts:195 → actual: ['sheet-localizer: lint']; apps/sheet-localizer/package.json has no lint script, no exemption, and is one minified line. scripts/check-kubota-containment.ts:52 shells to bare git (see #1). Vitest: carmen/tests/carmen-protocol.test.ts (4), powerpoint/tests/check-skill.test.ts:127 (2), image-gen/tests/photoreal-target-reference.test.ts (2), skills/tests/static-doc-contracts.test.ts (1).
Why it matters
A red gate that is always red catches nothing. The next real regression hides behind the known failures, and agents learn to ignore the output.
First fix
Add forgeapps.validation.exemptions.lint to sheet-localizer and pretty-print the file. Update the four prose assertions to the current wording. Make the kubota test skip when git is unavailable.
Verification
pnpm repo:contract:test and pnpm test both exit 0.

3 git status is never clean High

What's wrong
Three separate sources of permanent dirt. Routines append to 1,077 tracked history/*.jsonl ledgers on every run. .remember/ and tmp/ are neither tracked nor ignored. The fleet-drift routine writes to ERRORS.md every hour because its probe script was deleted.
Evidence
75 dirty files at audit start, 24 of them ledgers. .gitignore:288-293 un-ignores /tmp/ expecting tmp/.gitkeep and tmp/README.md, neither exists. crons/routines/fleet-drift-maintenance/scripts/fleet-drift.ts:39 points at crons/fleet/fleet-check.sh, deleted in 46b348faa (2026-09-02); ERRORS.md last entry 2026-09-10T02:17Z.
Why it matters
When the tree is always dirty, "did I change anything?" needs a diff every time, and stray files are one git add . from being committed. The fleet-drift error is also the only fleet-drift detector, so its failure reads as "no drift".
First fix
Add /.remember/ to .gitignore; restore tmp/.gitkeep or drop the negation block; git checkout 46b348faa^ -- crons/fleet/. Then decide whether ledgers need full history or a rolling window (a separate, larger call).
Verification
git status --short on a fresh session shows only ledger churn, and the fleet-drift history/ gets a new entry at the next hour.

4 Root test run covers ~4% of tests High

What's wrong
The shared vitest config hand-lists 57 skill test globs. 816 app/package tests and 531 skill/cron/script tests fall outside every include. crons/ and .forgebot/skills/ have no test script, so repo:check never visits them either.
Evidence
config/vitest/vitest.config.mts:5-70; crons/package.json (5 lines, no scripts); 200 cron test files, 12 of which import vitest directly (e.g. crons/routines/forgebuild-maintenance/scripts/check-push-baseline.test.ts); ~180 check-skill.test.ts contract files across skills that fire for almost none.
Why it matters
The cron tests cover the push-batching and attribution logic the pre-push hook depends on. An agent told "run the tests" gets a green that proves almost nothing.
First fix
Wrap the three live-network tests in describe.skipIf(!token) (slack/tests/forgebot-permissions.test.ts:48, slack/tests/legacy/slack-readonly-live.test.ts, fireflies/tests/fireflies-readonly-access.live.test.ts). Then add "test": "vitest run" to crons/package.json and .forgebot/skills/package.json.
Verification
pnpm --filter @forgefx/crons test collects 200 files and passes without secrets.

5 Per-app test wiring lies High

What's wrong
forgefarm has "test": "vitest run" but no vitest or vite config, so vitest walks up and runs the entire root skills suite as forgefarm's tests. forgescene carries an exemption saying "No automated test suite is configured" while shipping 7 test files; repo-check reads the exemption first and skips. forgepoll and ara pass on passWithNoTests with zero tests.
Evidence
apps/forgefarm/package.json, apps/forgefarm/__tests__/ holds only setup.ts; apps/forgescene/package.json exemption vs scripts/repo-check.ts:264-266; apps/forgepoll/vitest.config.ts:11, apps/ara/vitest.config.ts:10. 8 forgeisland scripts/*.test.ts use node:test and sit outside that app's ALL_TESTS glob at apps/forgeisland/vitest.config.ts:92.
Why it matters
A forgefarm edit makes repo:check --changed run unrelated skill tests. Stale exemptions mask real suites.
First fix
Add apps/forgefarm/vitest.config.ts with test: { root: __dirname, passWithNoTests: true }; delete forgescene's exemptions.test key; widen forgeisland's glob to include scripts/**.
Verification
pnpm --filter forgefarm test finishes in seconds with 0 files; pnpm --filter forgescene test collects 7.

6 Toolchain nags and placeholders High

What's wrong
Every pnpm invocation prints an engine warning because the machine runs Node 25 against a 24.x pin and nothing tells nvm otherwise. The workspace file has an unfilled placeholder. The root test runner is a major version behind most packages. The npm-style workspaces field in package.json lists two entries the YAML comments explicitly forbid.
Evidence
[WARN] Unsupported engine: wanted 24.x, current v25.8.1; no .nvmrc/.node-version/.tool-versions. pnpm-workspace.yaml:92 '@google/genai': set this to true or false. Root package.json:98 vitest ^2.1.9 vs ^3.2.4 ×57, ^4.0.16 ×3. package.json:115,127 claim apps and artifacts; pnpm-workspace.yaml:12-20,39-44 document why both are wrong. App engines also disagree: 24.x, >=24, >=20, >=18.
Why it matters
Small, constant noise trains everyone to skim warnings. The v2/v3 split means "passes in the app" and "passes at the root" are different claims.
First fix
Add .nvmrc containing 24; set the genai line to false; bump root vitest to ^3.2.4 and rerun; remove apps and artifacts from workspaces.
Verification
pnpm install prints no engine warning; pnpm test still green after the bump.

7 Registries drift from disk High

What's wrong
The app registry and the launch config are both hand-maintained mirrors of the filesystem, and both have drifted. A sync script exists for launch.json and its output does not match.
Evidence
apps/_registry/apps-metadata.json: 70 entries vs 83 folders. Missing: botstatus, forgehalli, forgelang, forgeplan, forgesweep, roomdesign, tomobot (all deploy to Vercel), plus adamobot-mcp, forgemedia-mcp-server, public, purplewoods, sheet-localizer, unity-blender-cli-curriculum. Ghost entry forgelessons with a production URL and no folder; crons/routines/forgebuild-maintenance/ROUTINE.md references it too. .claude/launch.json: no report entry for fleet-drift-maintenance or forge-valley-maintenance, no opt-out marker either; example and example-local both on port 4075.
Why it matters
Anything that walks the registry (FleetView, /start, the forgebuild routine) silently skips the unregistered apps.
First fix
pnpm exec tsx crons/routines/routines-maintenance/sync-launch-json.ts and commit; delete forgelessons; seed the 13 missing entries from scripts/gen-app-build-inputs.ts, which already enumerates all 83.
Verification
A registry-vs-folders diff returns empty; add it to forgeapps-maintenance so it stays empty.

8 Slack channel context is split and holey High

What's wrong
channels/AGENTS.md tells agents to load channels/<name>/ before answering. Two channels have two folders each (old and new bot name) sharing one Slack ID. Five client-proposal channels have folders with no CHANNEL.md. Eight registry rows point at folders that do not exist. Code hardcodes 445 distinct channel IDs; 394 appear nowhere by name.
Evidence
channels/greg-forgebot + greg_forgebot → C0ADQH04NBZ; sentry_adamobot + sentry_forgebot → C0AN047456D. No CHANNEL.md in applied_materials_proposals, maine_drilling_and_blasting_proposals, mriglobal_proposals, ssab_iowa_inc_proposals, vermeer_proposals. Top unnamed IDs: C053EQZKSGY (415 refs), C0A7UBGD9QE (413), C06SC8X76JU (331).
Why it matters
ForgeBot answers active client channels with no context. Memory writes land in whichever duplicate folder the agent picked, and the other rots into false context. A channel rename is undiagnosable when 2,200 references carry no name.
First fix
Diff and merge each duplicate pair into the underscore name; scaffold the five missing files from channels/_template/CHANNEL.md; resolve the top 10 IDs via conversations.info and add rows.
Verification
A check asserting one folder per slack_id and one CHANNEL.md per folder passes.

9 "Is this routine scheduled?" has no answer in the repo Medium

What's wrong
The only in-repo scheduler file lists one job for a 36-routine fleet. Most routines' last report is nearly two months old, and 14 of them stopped on the same day, which points at one scheduler event rather than 14 coincidences.
Evidence
.hermes/cron/jobs.json: one enabled job, dave-assist-radar-maintenance. Latest REPORT dates: partners-mtg-prep 2026-05-05, lfs-hygiene 06-09, then 14 routines all on 2026-07-12 (airtable, channel-context, dashboard, docs, doppler, forgeapps, forgebase, forgemail, forgemedia, forgemiser, forgewiki, jira, obsidian, sentry). Five have no parseable date. Only forgebuild and forgesales are current.
Why it matters
When a watchdog goes quiet, the first question is "was it supposed to fire?", and the repo cannot answer it.
First fix
Add a schedule: frontmatter line to every ROUTINE.md and have routines-maintenance assert it against whatever scheduler is live. Finding the 07-12 root cause is a separate, longer job.
Verification
grep -L '^schedule:' crons/routines/*/ROUTINE.md returns nothing.

10 Template copy-paste debt High

What's wrong
The app template's API handlers were copied verbatim into dozens of apps and have started to drift. Its placeholder hourly cron was copied with its schedule.
Evidence
Of 418 apps/*/api/**/*.ts, 77 are byte-identical to a file in another app: health.ts ×12, sentry-issues.ts ×12, github-status.ts ×11, github-recent.ts ×11, vercel-deployments.ts ×11, doppler-set.ts ×11 (with 3 drifted variants). 16 apps have the "Placeholder cron hook" handler, 15 schedule it 0 * * * * (~10,800 no-op invocations a month). SecretsPage.tsx ×11 with 5 distinct hashes.
Why it matters
Every fix is an N-way edit and the copies already disagree. The placeholder crons teach everyone that "cron configured" means nothing.
First fix
Delete the crons block from the 15 vercel.json files starting with apps/example. Move github-status.ts (no secrets, lowest risk) into a workspace package and re-export from the 11 apps to prove the pattern.
Verification
The md5 duplicate census drops by 11; Vercel shows no crons on the 15 apps.

Near Misses

Recommended next move

Fix #1 (five minutes, one command), then #2. Together they make the repo's own gates usable again, and every other item on this list gets cheaper to verify once those two are done.

Commands Run

G=/Library/Developer/CommandLineTools/usr/bin/git   # /usr/bin/git is blocked by the Xcode license
$G status --short; $G log --oneline -8; $G ls-files | wc -l
pnpm repo:context; pnpm repo:contract:test; pnpm repo:check:changed; pnpm vitest run --reporter=dot
pnpm --filter {forgeboard,forgesales,forgemedia,forgebase,forgemiser,adamobot} typecheck   # all clean
pnpm ls -r --depth 0
grep -h '"vite"\|"vitest"\|"typescript"\|"node"' apps/*/package.json packages/*/package.json | sort | uniq -c
$G ls-files -z | xargs -0 stat -f '%z %N' | sort -rn | head -40
$G lfs ls-files | wc -l  vs  $G ls-files -z | $G check-attr --stdin -z filter
$G check-ignore -q <each top-level dir>
find apps -path '*/api/*' -name '*.ts' | xargs md5 -q | sort | uniq -c | sort -rn   # duplicate handlers
grep -l 'Placeholder cron hook' apps/*/api/cron/hourly.ts
python3: apps-metadata.json vs apps/ folders; launch.json vs crons/routines; channels/AGENTS.md vs channels/*/CHANNEL.md
grep -rhoE 'C0[A-Z0-9]{8,}' apps crons scripts .forgebot/skills | sort | uniq -c
$G log --diff-filter=D --oneline -3 -- crons/fleet; $G show --stat 46b348faa -- crons/fleet
for d in crons/routines/*/; do grep -oE '202[5-9]-[01][0-9]-[0-3][0-9]' $d/REPORT.md | sort | tail -1; done
find apps packages crons scripts .forgebot/skills -name '*.test.ts' | wc -l; node glob-match vs config/vitest include list
grep -rn -E '\.(skip|only|todo)\(' --include='*.test.ts*' apps packages scripts crons .forgebot/skills
rg -n '"(strict|noImplicitAny|strictNullChecks)"\s*:\s*false' apps/*/tsconfig.json