Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,57 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- **Safer installer transactions (HI-10).** `install.sh` backs up a user-owned symlink at a
destination instead of silently replacing it, and stops (`Uninstall INCOMPLETE`, non-zero)
rather than removing assets that settings hooks still reference when settings cleanup fails.
- **Verification surfaces monorepo suites (ME-03).** `detectStack()` now reports `workspaces`
(declared workspace globs) and `packageRoots` (nested package roots found via a bounded,
capped scan), so a root test command can no longer silently claim to cover a whole
npm/pnpm/Turborepo/Maven/Gradle/Python monorepo.
- **Evidence resolution strength gates confidence (ME-05).** `ci:`/`human:`/`file:` refs are
format- and existence-checked, and unresolved pointers like `test:<id>` or a non-existent
`file:` path can no longer lift a claim's confidence into the trusted/serving band; a
resolvable git-ref confirmation still counts exactly as before.
- **Secret-bearing ledger metadata is refused (ME-06).** Secret-shaped evidence refs, authors,
tombstone reasons, and provenance metadata are refused before they can be written to the
ledger — the same detector already applied to claim content — and quarantined records are
stored redacted.
- **Trusted quarantine identity (ME-07).** Quarantine keys rejected records by a trusted content
hash instead of the record's own (attacker-controllable) hash, so distinct forgeries sharing a
fake hash are both retained and malformed lines with no hash are captured instead of dropped.
- **Per-target MCP ownership and atomic add/remove (ME-08, ME-10).** Adoption is tracked per
`{server, target}`, so adopting a same-name server for one tool never authorizes overwriting
another tool's entry (legacy `adopted: [name]` still honored and migrated); `integrations add`
records nothing on a partial emit and `remove` keeps the entry (reporting an incomplete
transaction) if any target cleanup fails, so config and disk never drift silently.
- **Divergent registry-name MCP entries preserved (ME-09).** A user's own MCP server sharing a
built-in name (e.g. `forge-cortex`) is no longer overwritten without explicit `--adopt`.
- **Validated MCP names and safe serialization (ME-11).** Managed server names must match
`^[a-z0-9][a-z0-9_-]{0,63}$`, `foo`/`forge-foo` Continue-file collisions are rejected before
any write, and command strings are quoted safely into YAML and TOML.
- **Non-object JSON is treated as corrupt (ME-12).** Repo config and global settings now reject
valid-but-non-object JSON (`null`, `[]`, `"x"`, `42`) as corrupt instead of silently
overwriting it, preserving the original bytes.
- **Crash-safe config writes (ME-13).** `forge.config.json` writes take a timestamped backup and
go through a temp file + atomic rename.
- **Complete tool list (ME-14).** `roo` is now a selectable primary tool, and
aider/continue/windsurf/roo are auto-detected from their on-disk markers, reconciling
`forge tools` with the MCP emit targets.
- **Functional doctor health probes (ME-15, ME-16, ME-17, ME-18).** `forge doctor` validates the
actual Forge hook wiring against the template (not just the `_forge` marker), verifies
`~/.forge` is a symlink/dir whose required guard assets resolve (catching plain-file shadows
and dangling links), runs a real redactor self-test instead of trusting Node's presence, and
records a partial `--fix` sync (nested `action:"error"` rows) as a failed repair.
- **Explicit PARTIAL sync status (ME-19).** `forge sync` returns and reports an aggregate status;
if any target fails mid-run the result is `PARTIAL` and `sync`/`init` say so and exit non-zero
instead of implying every tool is configured.
- **Fail-safe legacy rules (ME-20).** A corrupt legacy `.forge/rules.json` no longer aborts
`forge sync` — it warns once, falls back to default rules, and leaves the file's bytes intact.
- **Exact gitattributes rule detection (ME-21).** The ledger union-merge rule is detected by its
exact active line, so a comment mentioning `.forge/ledger/` no longer suppresses the real rule.

### Changed

- **Disclose the global settings merge before it happens (ME-22).** `forge init` now prints the
global `~/.claude/settings.json` merge disclosure before the merge mutates the file, not after.

- **Honest positioning (ME-24).** README no longer claims "one brain for every AI coding
agent", "enforced guardrails", or that every task passes a deterministic gate: automatic
hooks are Claude Code-specific (advisory by default, enforcement opt-in via
Expand Down
5 changes: 3 additions & 2 deletions docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -570,9 +570,10 @@ tools` fixes that without changing what `sync` emits.

- `forge tools` — show the detected/primary tool (from `.forge/config.json`, else
auto-detected from which agent folder exists — `CLAUDE.md`, `.cursor/`, `.gemini/`,
`.codex/`, `.zed/`, `.vscode/`) and which targets are currently gitignored.
`.codex/`, `.zed/`, `.vscode/`, `.aider.conf.yml`, `.continue/`, `.windsurf/`, `.roo/`)
and which targets are currently gitignored.
- `forge tools <name>` — record `<name>` (`claude` · `cursor` · `gemini` · `codex` ·
`zed` · `vscode` · `aider` · `continue` · `windsurf`) as this repo's primary tool in
`zed` · `vscode` · `aider` · `continue` · `windsurf` · `roo`) as this repo's primary tool in
`.forge/config.json`, then write a **marked, reversible** block into `.gitignore`
(`# forge:gitignore:begin … # forge:gitignore:end`) that ignores every OTHER tool's
emitted artifacts. Your own `.gitignore` lines are never touched, and the shared
Expand Down
40 changes: 32 additions & 8 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,22 +124,23 @@ async function run(argv) {
// --settings-only: wire hooks + permissions into ~/.claude/settings.json ONLY (the
// idempotent, marker-guarded merge install.sh calls) — no repo emit, no AGENTS.md.
if (argv.includes("--settings-only")) {
// ME-22: heading + disclosure precede the merge — onSettingsNotice fires inside init
// BEFORE mergeSettings touches the file, never after.
heading(`${BRAND.brand} init — settings merge only\n`);
const { settings } = init({
settingsOnly: true,
noSettings,
settingsPath,
onSettingsNotice: consentLine,
});
heading(`${BRAND.brand} init — settings merge only\n`);
if (
(settings?.action === "merged" || settings?.action === "created") &&
"added" in settings
) {
consentLine(settings.path);
const verb = settings.action === "created" ? "created" : "merged";
const what = settings.added.length ? settings.added.join(", ") : "defaults";
console.log(` settings: ${verb} ${what} into ${settings.path}`);
} else if (settings?.action === "unchanged" && "path" in settings) {
consentLine(settings.path);
console.log(` settings: already up to date (${settings.path})`);
} else if (settings?.action === "skipped") {
console.log(" settings: skipped (--no-settings)");
Expand All @@ -153,6 +154,10 @@ async function run(argv) {
}
const profileIdx = argv.indexOf("--profile");
const profile = profileIdx >= 0 ? argv[profileIdx + 1] : undefined;
// ME-22: emit the GLOBAL-settings disclosure BEFORE the merge mutates the file. init
// forwards `onSettingsNotice` to mergeSettings, which fires it right before touching
// ~/.claude/settings.json — so the notice always precedes the mutation it describes.
heading(`${BRAND.brand} init — this repo now speaks every AI tool from one source.\n`);
const {
report,
bytes,
Expand All @@ -165,6 +170,7 @@ async function run(argv) {
noSettings,
profile,
settingsPath,
onSettingsNotice: consentLine,
})
);
if (profileResult?.error) {
Expand All @@ -173,11 +179,21 @@ async function run(argv) {
return;
}
const wrote = report.filter((r) => r.action === "written").map((r) => r.target);
heading(`${BRAND.brand} init — this repo now speaks every AI tool from one source.\n`);
console.log(` emitted: ${wrote.length ? wrote.join(", ") : "(all up to date)"}`);
console.log(
` source: AGENTS.md (${bytes} B) — edit rules in source/, re-run \`${BRAND.cli} sync\``,
);
// ME-19: sync is non-transactional — if a target failed mid-emit, say so instead of
// implying every tool is ready, and fail the command.
const failedTargets = report.filter((r) => r.action === "error");
if (failedTargets.length) {
console.error(
` status: PARTIAL — ${failedTargets.length} target(s) failed: ${failedTargets
.map((r) => r.tool)
.join(", ")} (re-run \`${BRAND.cli} sync\` after fixing)`,
);
process.exitCode = 1;
}
if (profileResult?.profile) {
console.log(` profile: ${profileResult.profile} → .forge/forge.config.json`);
if (profileResult.deprecated) {
Expand All @@ -187,14 +203,12 @@ async function run(argv) {
}
}
if ((settings?.action === "merged" || settings?.action === "created") && "added" in settings) {
consentLine(settings.path);
const verb = settings.action === "created" ? "created" : "merged";
const what = settings.added.length ? settings.added.join(", ") : "defaults";
console.log(` settings: ${verb} ${what} into ${settings.path}`);
if ("backup" in settings && settings.backup)
console.log(` backup: ${settings.backup}`);
} else if (settings?.action === "unchanged" && "path" in settings) {
consentLine(settings.path);
console.log(` settings: already up to date (${settings.path})`);
} else if (settings?.action === "skipped") {
console.log(" settings: skipped (--no-settings)");
Expand Down Expand Up @@ -377,7 +391,9 @@ async function run(argv) {
}
if (cmd === "sync") {
const { sync } = await import("./sync.js");
const { report, warnings, bytes } = sync({ targetRoot: process.cwd() });
const { report, warnings, bytes, partial, status } = sync({
targetRoot: process.cwd(),
});
heading(`${BRAND.brand} sync — one source → every tool\n`);
for (const r of report) {
console.log(
Expand All @@ -386,7 +402,15 @@ async function run(argv) {
}
for (const w of warnings) console.warn(` ! ${w}`);
const written = report.filter((r) => r.action === "written").length;
console.log(`\n${written} file(s) written · canonical ${bytes} B`);
console.log(`\n${written} file(s) written · canonical ${bytes} B · status: ${status}`);
// ME-19: a mid-way target failure must NOT exit 0 as if every tool were configured.
if (partial) {
const failed = report.filter((r) => r.action === "error");
console.error(
` ! PARTIAL sync — ${failed.length} target(s) failed: ${failed.map((r) => r.tool).join(", ")}`,
);
process.exitCode = 1;
}
return;
}
if (cmd === "doctor") {
Expand Down
Loading
Loading