Repository navigation
release: 0.42.0 - #141
Merged
Merged
release: 0.42.0#141
Conversation
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 Report✅ All modified and coverable lines are covered by tests.
🚀 New features to boost your workflow:
|
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.
Cuts 0.42.0. Seventeen changesets are pending: one minor (upgrade notes), sixteen patch. The bump is minor because
initwithout--yesnow stops when it finds existing tool config,check()gainsstaleTargets, and several generated formats change.What ships
Upgrade notes (
upgrade-notes-0-42). What to check after upgrading: rungenerateonce for the new Claude Codepaths:, Windsurfglobs:and nestedAGENTS.mdformats; delete a duplicatesrc.md-style rule an earlier Codex round trip created;init --yes;checkandstale_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 symlinkedSKILL.mdis 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
init --yeskeeps both versions of a same-name rule, command or agent (init --yeskeeps only one of two same-name rules or commands from different tools #132).initwithout--yesstops instead of enabling a tool whose config it did not import (init --global+generate --globaloverwrite unmanaged home config (CLAUDE.md, MCP servers) without a backup #130).paths:scoping survives import and generate (Claude Codepaths:rule scoping is dropped on import, so the rule applies everywhere #137).globs:, the field Windsurf reads; import reads every form, including the documented unquoted one that used to fail.journal.md, and never runs from the hook (Legacy lessons migration drops rules, never reads journal.md, then deletes the old files #138).Correct output and gates
checkfails while agenerate --targetsrun left an enabled target stale (checkpasses aftergenerate --targetswhile other targets' outputs are stale #136).generateno longer fails when a root rule links to a rule and two targets shareAGENTS.md(generatefails when the root rule links to a rule and two AGENTS.md targets are enabled #139).MCP server
installanduninstallnotices no longer corrupt the JSON-RPC stream (MCP: install and uninstall print warnings to stdout and corrupt the JSON-RPC stream #133).create_*stores a body that starts with---as body text (MCP:create_*with empty frontmatter turns a body that starts with---into frontmatter #135).Verification
generate --checkon this repochore(generate): refresh target artifacts.d.ts, strict TS)npm pack --dry-runNotes
The repo's own
generate --checkcaught a regression in the #139 fix before release: in a sharedAGENTS.md, a backticked folder path such as.agentsmesh/skills/post-feature-qa/becameskills/post-feature-qa/.6abd853elimits that rewrite to Markdown link destinations.src/core/reference/link-rebaser.tswas already over the 200-line limit (215) and is 219 now; worth splitting separately.🤖 Generated with Claude Code