feat: v0.20 UX & onboarding — per-command help, doctor --fix, forge tools + auto-gitignore, zero-config wiring, dash clarity#69
Merged
CodeWithJuber merged 7 commits intoJul 17, 2026
Conversation
Makes `forge <cmd> --help` real (it was advertised in the banner but silently
dropped by each command's flag filter) by intercepting --help/-h centrally in the
dispatcher before command dispatch. Adds word forms `forge help [cmd]` and
`forge version`, and a "did you mean" suggestion on unknown commands via a
character-bigram suggest() over the existing setOverlap.
COMMANDS entries may now be a summary string OR a rich {summary,usage,flags,
examples,env} object; both coexist and are read through commandSummary/commandHelp
(printHelp, the only value-reader, goes through commandSummary). docs_check reads
only Object.keys(COMMANDS), so the migration is safe. High-traffic commands
(init/sync/doctor/update) migrated to rich help; the rest stay strings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ebv4QDTqzPzgApBW2KUsU
…es/chmod) + node threshold
Each fixable check in src/doctor.js now carries an optional {id,label,run} descriptor.
doctor({fix}) runs run() for every warn/fail result that has one, collects a repairs[]
list, then re-runs all checks so the returned results reflect the repaired state.
Fix map reuses existing idempotent functions only:
- missing settings hooks/permissions -> mergeSettings({settingsPath}) (src/init.js)
- missing ledger union-merge rule -> ensureLedgerGitattributes (src/init.js)
- missing/stale AGENTS.md -> sync({targetRoot}) (src/sync.js)
- guards not +x -> chmodSync mode | 0o111
A new checkSettings surfaces a warn (with a mergeSettings fix) when the user's
~/.claude/settings.json lacks the forge marker or hooks. Unsafe checks (provider
keys, MCP count, pricing, gateway) carry no descriptor and stay report-only.
checkNode reconciled with package.json engines (>=20): fail <18, warn 18-19, ok >=20,
ending the old 18-vs-20 mismatch without hard-breaking node-18 users.
CLI `forge doctor` parses --fix, printing a repairs: block then the re-checked results;
--json includes repairs. doctor already documents --fix in its COMMANDS entry.
Docs: README + docs/GUIDE mention `forge doctor --fix`; CHANGELOG [Unreleased] bullet.
Tests: settings warn->ok via --fix, gitattributes rule added, --fix idempotent (no-op).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ebv4QDTqzPzgApBW2KUsU
Zero-config onboarding (Concern 2b + 2c):
- init({settingsOnly}) runs the idempotent, _forge-marker-guarded mergeSettings
ONLY — no repo emit, no AGENTS.md, no gitattributes — so install.sh can wire
hooks + permissions into ~/.claude/settings.json without touching the repo.
- cli.js `init` handler parses --settings-only and reports the merge outcome.
- install.sh replaces its printed paste-by-hand settings block with an actual
`node "$REPO/src/cli.js" init --settings-only` call, honoring --dry-run (still
prints the call for transparency). mergeSettings is idempotent + marker-guarded,
so re-running install.sh never clobbers existing settings.
- First-run hint in run(): when ~/.claude/settings.json is missing or lacks the
_forge:"forge-managed" marker, a real command is invoked, and FORGE_NO_HINT!=1,
print one tip line to stderr pointing at `forge init` / `forge doctor --fix`.
Stateless, zero writes, self-silences once init runs.
Docs: FORGE_NO_HINT documented in README + docs/GUIDE; CHANGELOG [Unreleased]
bullet. Tests: init({settingsOnly}) merges hooks + permissions but emits no repo
config, and is idempotent on re-run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ebv4QDTqzPzgApBW2KUsU
…dary-tool artifacts
Concern 3: a repo that only uses one agent tool no longer has to track every
other tool's emitted artifacts (.cursor/, .gemini/, .codex/, .zed/, …).
- src/gitignore.js: ensureGitignoreBlock(root, paths) writes/updates ONLY a marked
block (# forge:gitignore:begin … :end), replacing content between the markers or
appending if absent, and never touching a user's own lines. Idempotent
(writeIfChanged contract — identical content is a no-op), handles a missing trailing
newline, and removeGitignoreBlock(root) strips only the block. readGitignoreBlock
reads back the listed paths.
- src/repo_config.js: readRepoConfig(root) → {primaryTool?, tools?} from
.forge/config.json (throw-free), with detectPrimaryTool auto-detecting the primary
tool from which agent folder exists (CLAUDE.md/.cursor/.gemini/.codex/.zed/.vscode),
mirroring autoDetectProvider. nonPrimaryTargets computes the emit targets belonging
to non-primary tools from sync()'s report rows; applyPrimaryTool writes the config
and gitignore block (sync injectable for tests).
- forge tools command (cli.js handler + COMMANDS entry + GROUPS.Core): `forge tools`
shows the detected/primary tool and gitignored targets; `forge tools <name>` sets the
primary tool and gitignores the rest from a real sync report; `forge tools --reset`
clears config + removes the block. Opt-in only — plain `forge sync` never writes
.gitignore. Default emit behaviour is unchanged; config only drives gitignoring.
- Docs: README + docs/GUIDE mention `forge tools`; CHANGELOG [Unreleased] bullet.
- Tests: test/gitignore.test.js (block isolation, user lines preserved, idempotent,
reset removes only the block, no-trailing-newline file) + test/tools.test.js
(readRepoConfig auto-detect, config+block from real emit targets, --reset clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ebv4QDTqzPzgApBW2KUsU
The dashboard was called "bad and confusing" — it showed blank panels on a
fresh repo with no explanation of what each lens meant. This adds the clarity
layer (Concern 1b), display-only, preserving the append-only write discipline.
- dash.js dashData() gains meta:{empty, forgeDir}. empty is true only when
.forge/ holds no ledger claims AND no metrics events; it reuses the already
computed ledger.stats.total and metrics.recent signals (recent is capped but
only empty at zero events) so there is no extra read, and it never throws —
a corrupt store degrades to empty, matching dashData's discipline.
- dash.html gets a "?" legend toggle (a pure display toggle, no fetch, no
writes) that explains every panel in one plain sentence, plus an empty-state
banner keyed off meta.empty: "No data in .forge/ yet — run `forge sync`,
then work a session." Brand tokens reused from the existing inlined palette.
- test/dash.test.js: bare repo -> meta.empty===true (+ forgeDir), populated
fixture and claims-only / metrics-only repos -> false, corrupt store -> empty
without throwing.
- CHANGELOG [Unreleased] bullet.
No new command, env var, or dependency.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ebv4QDTqzPzgApBW2KUsU
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ebv4QDTqzPzgApBW2KUsU
…ssions Adds .claude/hooks/session-start.sh — synchronous, idempotent, gated on CLAUDE_CODE_REMOTE=true (a no-op locally), skips the install when the dev toolchain (biome) is already resolvable. Wired into the repo's SessionStart hooks alongside the existing recall-load + cortex context hooks (both preserved). dogfood.test.js now accepts repo-local .claude/hooks/*.sh scripts in addition to global/guards/*.sh (both must exist and be executable). Completes the /session-start-hook request. Full suite 886 pass / 0 fail / 2 skip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ebv4QDTqzPzgApBW2KUsU
CodeWithJuber
marked this pull request as ready for review
July 17, 2026 14:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Follow-up round addressing three concrete UX/onboarding problems raised after the v0.20.0 feature drop.
1 — Help & discoverability
feat(help)—forge <command> --help/-hnow works for every command (it was advertised in the banner but silently dropped by each command's flag filter — now intercepted centrally before dispatch). Adds word formsforge help [command]andforge version, and "did you mean …?" on unknown commands via a character-bigramsuggest()over the existingsetOverlap.COMMANDSentries may now be a summary string OR a rich{summary, usage, flags, examples, env}object (both coexist; read through newcommandSummary/commandHelp;docs_checkreads only keys, so the migration is safe).2 — Zero-config onboarding (the "too hard to set up" complaint)
feat(doctor)—forge doctor --fix: each safely-fixable finding now carries a repair that reuses an existing idempotent function — missing hooks/permissions →mergeSettings, missing ledger gitattributes →ensureLedgerGitattributes, stale/missingAGENTS.md→sync, non-executable guards →chmod +x. Prints arepairs:block, re-checks, idempotent on a second run; unsafe findings stay report-only. Also reconciles thenodecheck withengines(>=20: fail <18, warn 18–19, ok 20+).feat(init)—forge init --settings-onlyruns only the marker-guarded settings merge (no repo emit), andinstall.shnow calls it instead of printing a block to paste by hand. A stateless first-run hint points atforge init/forge doctor --fixuntil settings are forge-managed (FORGE_NO_HINT=1mutes it).3 — Agentic-folder noise (the ".cursor/.gemini clutter" complaint)
feat(tools)—forge tools:forge tools <name>records the primary tool in.forge/config.jsonand writes a marked, reversible.gitignoreblock (# forge:gitignore:begin … :end,src/gitignore.js) ignoring exactly the NON-primary emit targets, computed fromsync()'s own report rows. User lines,AGENTS.md, and the primary tool's files are never touched;--resetstrips only the block. Opt-in — plainforge syncnever writes.gitignore.Plus
feat(dash)— first-run empty-state banner + a?panel legend explaining each panel in one sentence (the "dashboard is confusing" complaint). Display-only; append-only writes unchanged.chore(hooks)— web session-start install hook (.claude/hooks/session-start.sh, wired alongside the existing recall/cortex hooks): synchronous, idempotent, gated onCLAUDE_CODE_REMOTE=true, so Claude Code on the web can lint and test.Checklist
npm testpasses — 886 pass / 0 fail / 2 pre-existing skipsnpm run checkpasses (Biome — 6 pre-existing warnings, 0 errors)test/help.test.js,test/gitignore.test.js,test/tools.test.js, + extended doctor/init/dash/dogfood)CHANGELOG.mdupdated under## [Unreleased](six bullets)forge toolsin README +docs/GUIDE.md;FORGE_NO_HINTdocumented;docs checkgreenRisk & rollback
doctor --fixonly calls already-idempotent functions, unsafe checks stay report-only; the.gitignorewriter manages only its own marked block and is opt-in + reversible; the first-run hint is stateless with a kill switch; the session-start hook is a no-op outsideCLAUDE_CODE_REMOTE=true.forge tools --resetstrips the gitignore block;FORGE_NO_HINT=1mutes the hint. No ledger schema change.Extra checks (tick if applicable)
npm run typecheckpasses (JSDoc/tsc)doctor --fixand the gitignore writer are fail-safe; hook isset -euo pipefail+ shellcheck-clean🤖 Generated with Claude Code
https://claude.ai/code/session_017Ebv4QDTqzPzgApBW2KUsU
Generated by Claude Code