Skip to content

release: 0.42.0 - #141

Merged
sampleXbro merged 38 commits into
masterfrom
develop
Sep 24, 2026
Merged

sampleXbro merged 38 commits into
masterfrom
develop

Conversation

@sampleXbro

Copy link
Copy Markdown
Owner

Cuts 0.42.0. Seventeen changesets are pending: one minor (upgrade notes), sixteen patch. The bump is minor because init without --yes now stops when it finds existing tool config, check() gains staleTargets, and several generated formats change.

What ships

Upgrade notes (upgrade-notes-0-42). What to check after upgrading: run generate once for the new Claude Code paths:, Windsurf globs: and nested AGENTS.md formats; delete a duplicate src.md-style rule an earlier Codex round trip created; init --yes; check and stale_targets; imports that add instead of replace; pack and extend root rules; MCP call order; old lessons stores.

Security fixes (pack-folder-containment, skill-md-symlink, root-rule-precedence). Pack and lessons folders must resolve inside the project, a symlinked SKILL.md is never followed, and an installed pack or extend can no longer replace the project's own root rule. Advisories will be published after the release.

Import and adoption no longer lose data

Correct output and gates

MCP server

Verification

Check Result
Unit and integration (coverage run) 14,036 passed, 1 skipped
Coverage 98.5% statements, 95.6% branches; per-file floor OK
End to end 658 passed
Types (src and tests), lint, dead code clean
generate --check on this repo clean after chore(generate): refresh target artifacts
Consumer smoke (packed .d.ts, strict TS) OK
npm pack --dry-run 6 top-level entries, nothing unexpected
Website build, support-matrix verify OK

Notes

The repo's own generate --check caught a regression in the #139 fix before release: in a shared AGENTS.md, a backticked folder path such as .agentsmesh/skills/post-feature-qa/ became skills/post-feature-qa/. 6abd853e limits that rewrite to Markdown link destinations.

src/core/reference/link-rebaser.ts was already over the 200-line limit (215) and is 219 now; worth splitting separately.

🤖 Generated with Claude Code

git keeps symlinks, so a repository could point .agentsmesh/packs, or
one pack folder, outside the project; install, install --sync, refresh
and uninstall then followed the link and wrote, replaced or deleted
there, through the CLI and the MCP tools.

- New assertPackPathInsideProject: the real path of a packs or pack
  folder must stay inside the project root (the home folder in global
  mode), checked with the existing assertPathInsideRoot.
- installAsPack checks the packs folder before any read and the target
  pack folder before any write, also under --dry-run.
- runUninstall checks the packs folder and every pack it will remove
  before any prompt or delete, also under --dry-run (a pre-flight, like
  generate's boundary check, so it is not swallowed by per-pack
  failure isolation).
- parseNames and defaultAdapter move to uninstall-io.ts to keep
  run-uninstall.ts under 200 lines.
The second half of the pack-folder fix: a repository could link
.agentsmesh/lessons (or .agentsmesh) outside the project, and lessons
add, the MCP lessons tools, init --lessons and the recall hook's logs
then wrote lessons.json, its lock and its logs there.

- resolvesInsideRootSync (path-containment.ts): a sync containment
  check that treats a dangling link as outside, since a missing target
  must not count as inside.
- lessons-dir-guard.ts: lessonsDirInsideProject and
  assertLessonsDirInsideProject, anchored at the project root.
- saveLessonsGraph, acquireLessonsLock and scaffoldLessons refuse with
  LessonsDirOutsideProjectError. Every graph writer (add, deprecate,
  prune, resolve, legacy migration, MCP) goes through the lock or the
  save, so all of them are covered, and migration never reaches its
  delete step.
- The capture, recall and outcome logs skip such a folder, so the hook
  still exits 0. Reading lessons through the link keeps working.
In a script or CI shell without --yes, init found existing tool config,
printed only a hint to import it, and still enabled that tool. The next
generate then replaced the config; in --global mode that meant
~/.claude/CLAUDE.md and the MCP servers in ~/.claude.json, which git
cannot restore.

Now non-interactive init stops with exit 1 and writes nothing when a
detected tool would be enabled without --yes. The message says to run
init --yes to import it first, or pass --targets to leave the tool out.
Explicit targets that leave the detected tool out, --yes, and the
interactive wizard work as before.

Tests that asserted the old scaffold-anyway behaviour now use --targets
or --yes for the same purpose. Docs (init.mdx, README) and the init help
text describe the stop.

Fixes #130
parseSkills and parseSkillDirectory read SKILL.md with readFileSafe,
which follows symlinks, while every other canonical reader skips them
(readDirRecursiveNoSymlinks, copyDir). A skills source with
skills/<name>/SKILL.md (or a root SKILL.md) linked to a local file made
install copy that file into .agentsmesh/packs and the generated
.claude/.cursor skills; generate did the same for a project's own
canonical skills.

Both parsers now read SKILL.md through readSkillFile, which lstats it
and skips a symlink. Every path goes through them: install sources,
installed packs, the canonical loader for generate, and skill packs.
The native import pipeline already enumerates with
readDirRecursiveNoSymlinks, and a symlinked --path file already fails.
Twelve targets pick the root with rules.find((r) => r.root). The merge
(extends -> packs -> local) appends new rule names, so a pack or extend
rule with root: true under any name but _root came before the user's
_root.md and became the root: CLAUDE.md, AGENTS.md and Cursor's root
rule showed only its text, check passed, and it worked even for remote
packs whose elevated artifacts are stripped.

settleRootRule (canonical/load/root-precedence.ts) now keeps exactly
one root after the merge: the project's own, else an installed pack's,
else an extend's. Every other root: true rule becomes a normal rule,
and loadCanonicalWithExtends warns with both paths. Docs (extends.mdx,
install.mdx) describe the rule.
Importing a second tool replaced .agentsmesh/permissions.yaml and
.agentsmesh/ignore, and four importers (gemini-cli, cline, windsurf,
crush) replaced .agentsmesh/mcp.json. Claude Code deny rules that
protect secrets were dropped without a word, and the next generate
removed them from .claude/settings.json.

runTargetImport now wraps every importFrom call (import, init --yes,
the public importFrom API and so the MCP import tool). It reads the
three files before the import and adds back any entries the import
dropped: permission lists and ignore lines are unioned, MCP servers
merge by name with the new import winning. Being one step around the
importer, it covers every builtin importer and plugin targets alike.

Fixes #131
Supersede the "last-import-wins except _root.md and mcp.json" lesson
with the runTargetImport rule, and record that init detects Cursor by
its rules, not by cli.json.
init --yes imports every detected tool in one run, but a canonical name
comes from the file name, so two tools' typescript.md rules (or deploy.md
commands) landed on one file: the later tool replaced the earlier text and
mixed in its frontmatter, and the next generate wrote that over the first
tool's own file.

importKeepingSameName now wraps each tool's import in init. The rule,
command and agent files earlier tools wrote in this run are set aside
while the next tool imports, so it writes a clean file. The earlier file
is then put back; a different text from the later tool is saved as
<name>-<tool>.md and listed in the new InitData.sameNameCopies, which the
renderer prints as a warning. The same text keeps one file. Separate
import --from runs stay last-import-wins.

Fixes #132
Supersede the import merge lessons with one that also names
importKeepingSameName, and record two bits of friction: a str.replace
whose shorter-indent pattern matched twice, and renderer test fixtures
that omit required InitData fields.
The install and uninstall prompt adapters wrote the broken-link and
locally-modified notices with process.stdout.write, so under
agentsmesh mcp they landed in the JSON-RPC stream, and under --json in
the JSON output. They now use the new logger writeOutput, which follows
the MCP stderr redirect and the --json mute.

Fixes #133
serializeMd returned the body as-is when frontmatter was empty, so a
body starting with a --- block became the file's frontmatter: get_*
returned different metadata and body text could set root: true,
allowed-tools or name for generate. It now writes an empty --- block
before such a body, and parseMd uses the shared splitFrontmatter so MCP
reads and the canonical loaders agree on where frontmatter ends.

Fixes #135
…change

JSON-RPC clients may send several tools/call requests without waiting.
Each write tool reads a file, changes it and writes it back, so parallel
add_mcp_server, update_permissions/update_ignore (append) and
update_config calls kept only the last writer's change while every call
reported success. The server now runs tool calls through one queue, in
arrival order; a failed call does not block the next one.

The stdio test client moved to tests/helpers/mcp-stdio.ts so the #133
and #134 integration tests share it.

Fixes #134
A filtered generate wrote the new canonical checksums to the lock even
though the targets it skipped were not regenerated, so check reported
"in sync" while their outputs were stale. writeLockFile now takes the
skipped targets: when there are any, it keeps the previous lock's
checksums, extends and packs and only merges the new outputs, and with
no lock yet it writes none. check then fails until a full generate. A
--targets list that names every enabled target is still a full run.

Fixes #136
Claude Code scopes a .claude/rules/*.md rule with `paths:`, the only
field it reads from a rule. Import left `paths` as an extra field and
gave the canonical rule `globs: []`, and generate wrote canonical globs
as `globs:`, which Claude Code ignores, so scoped rules applied to every
file. The rule mapper now reads `paths` (list or comma-separated string)
into globs, still reads an older generated `globs:`, and the generator
writes `paths:`.

Fixes #137
…lding the lock back

The first #136 fix kept the lock's old checksums after a --targets run
that skipped an enabled target, and wrote no lock at all on a first
filtered run. That lost the outputs provenance the stale-file sweep
needs (the target contract matrix caught it), made a full run after a
filtered --force run need --force again under strategy: lock, and kept
check red after two filtered runs that together covered every target.

The lock now always records the current sources and gains an optional
stale_targets list: a filtered run adds the enabled targets it skipped
when the sources changed, keeps only already-stale skipped targets when
they did not, and a full run clears it. check reports them as
staleTargets (CLI, --json, MCP check, check() API) and fails while any
enabled target is listed. Locks of projects that never use --targets do
not change.

Refs #136
A root instruction file claimed by two targets (AGENTS.md for codex-cli
and cursor, codex-cli and gemini-cli, cursor and windsurf) skipped
reference rewriting entirely, so its copies stayed identical. A relative
link in the root rule, like ./typescript.md, then pointed nowhere from
the project root and the broken-link check failed the whole run.

Shared root files are now rewritten with an identity path translation:
Markdown links are rebased onto the canonical file they name, which is
the same for every target, so the copies still converge; bare path
tokens stay untouched.

Fixes #139
…es on import

codex-cli (and codebuff, which writes the same paths) wrote a scoped
rule into a nested <dir>/AGENTS.md as plain text, so import read it as a
new <dir>.md rule and every import/generate cycle repeated the text in
src/AGENTS.md and copied it to other targets.

Each rule in a nested file is now a bare agentsmesh:embedded-rule entry
naming its canonical source, description, globs and targets. Bare
entries are not a protected block, so their links are still rewritten
for the nested file, and codex's entries stay a prefix of codebuff's so
the shared path still resolves by containment. The codex-cli, codebuff
and windsurf nested importers split the entries back to their canonical
files via splitNestedAgentsFile (keeping the Codex override variant) and
import only the remaining hand-written text as the directory rule.

The per-rule entry code moved from managed-blocks.ts to
embedded-rule-entries.ts, and the windsurf nested import to its own file,
to stay under 200 lines.

Fixes #140
…te from the hook

The legacy lessons migration kept only single-line numbered items under
a "## Rules" heading, so wrapped rules lost their tail, bullet rules and
rules under "## Lessons" were dropped, and rules with the same number in
two sections overwrote each other. It never read journal.md, then
deleted it with the rest, and the recall hook ran it silently.

The rules parser (now import-legacy-rules.ts) reads numbered and bullet
items under "## Rules" or "## Lessons", joins the lines that wrap onto
an item, and numbers rules by position so ids stay unique. A list item
under any other heading stops the migration with LegacyStrayRuleError,
naming the file and line, before anything is written or deleted.
journal.md and journal.legacy.md are kept, since their notes are not
rules. The lessons hook no longer migrates, and recall takes an
autoMigrate option the hook paths turn off.

Fixes #138
The #133 fix swapped the adapter's write arrow for writeOutput, which
left the untested ask arrow as a quarter of the file's functions and
dropped uninstall-io.ts below the per-file coverage floor. The new test
drives ask through stdin and checks both writes go to the logger stream.
For a rule with globs the windsurf generator wrote three outputs: the
glob rule .windsurf/rules/<rule>.md, an identical .windsurf/rules/<dir>.md
and a plain <dir>/AGENTS.md. Windsurf reads a trigger: glob rule for the
files its globs match and also reads a subdirectory AGENTS.md as a rule
for that folder (docs.devin.ai/desktop/cascade/memories), so it loaded
the rule several times, the nested file widened src/**/*.ts to src/**,
and import --from windsurf added a duplicate <dir>.md canonical rule.

Only .windsurf/rules/<rule>.md is written now, and the layout no longer
declares <dir>/AGENTS.md as a rule output. The stale .windsurf/rules/<dir>.md
is evicted by the next generate (managed dir + lock provenance). A nested
<dir>/AGENTS.md is not a managed output, so an old copy stays on disk;
the windsurf nested importer skips one whose text equals a
.windsurf/rules/*.md body, so the round trip is a no-op, while a
hand-written nested AGENTS.md still imports as the folder rule.
Windsurf scopes a trigger: glob rule with `globs:`, one string with
patterns joined by commas: its docs (docs.devin.ai/desktop/cascade/memories,
where docs.windsurf.com redirects) name only `globs`, real .windsurf/rules
files use `globs: a,b`, and its language server parses rules in
parseRuleFrontmatter and turns the string into a list with
globStringToGlobList. The generator wrote one pattern as `glob:`, which
Windsurf ignores, so single-glob rules never activated, and several as
a YAML list.

The generator now writes `globs:` as a comma-joined string. The rules
importer reads `globs` as a string or a list and the older `glob:`,
splits only on commas outside braces so *.{ts,tsx} stays whole, quotes
an unquoted globs value first (the documented `globs: **/*.test.ts` is
a YAML alias and failed the whole import), and skips a rule it still
cannot parse with a warning instead of aborting.
…hind

- generate-lock: inline currentSources, a one-caller helper left from the
  first #136 design.
- mcp-merge: inline readExistingServers, now a one-line wrapper.
- uninstall-io: parseUninstallNames uses flatMap (same order, keeps
  duplicates).
- import-legacy-read: plain Error instead of LegacyStrayRuleError, whose
  class and code nothing read.
- windsurf rule-globs: drop formatWindsurfGlobs (globs.join(',') inline in
  the generator) and read a globs list with the shared toToolsArray.
- tests: nine new test files shared one copied temp-project block
  (mkdtemp, write, read, cleanup); they now use tests/helpers/temp-project.ts.

No behaviour change.
…share

The #139 fix ran shared root files (AGENTS.md written by two targets)
through the link rebaser with an identity translation. That rebased
every path token, and a backticked canonical folder such as
.agentsmesh/skills/qa/ came out in the mesh-relative form skills/qa/,
which does not exist from the project root. The broken-link check skips
inline code, so tests passed; the repo's own generate --check caught it.

rewriteFileLinks gains markdownLinksOnly, and shared root files use it:
Markdown link destinations (inline and reference-style) are rebased onto
the canonical file, and every other path token stays as written.

Refs #139
@codecov

codecov Bot commented Sep 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/canonical/extends/extends.ts 100.00% <100.00%> (ø)
src/canonical/features/permissions.ts 100.00% <100.00%> (ø)
src/canonical/features/skills.ts 100.00% <100.00%> (ø)
src/canonical/load/root-precedence.ts 100.00% <100.00%> (ø)
src/cli/commands/check.ts 100.00% <ø> (ø)
src/cli/commands/generate-empty-run.ts 100.00% <100.00%> (ø)
src/cli/commands/generate-handlers.ts 100.00% <ø> (ø)
src/cli/commands/generate-lock.ts 100.00% <100.00%> (ø)
src/cli/commands/generate.ts 100.00% <ø> (ø)
src/cli/commands/import.ts 100.00% <100.00%> (ø)
... and 56 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sampleXbro
sampleXbro merged commit 2cc715a into master Sep 24, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants