mandrel 1.74.0 → 1.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -76,6 +76,15 @@ environment variables that override project defaults. The config resolver
76
76
  deep-merges `.agentrc.local.json` over `.agentrc.json` (local wins; absent
77
77
  local file is a no-op). Do not modify these local files unless requested.
78
78
 
79
+ **Durable slash commands.** Any `.md` file placed at
80
+ `.agents/local/workflows/<name>.md` is automatically projected into
81
+ `.claude/commands/<name>.md` by `sync-claude-commands.js`, making it
82
+ invocable as `/<name>`. Because the entire `.agents/local/` subtree is
83
+ exempt from `mandrel sync`'s prune pass, these commands survive
84
+ `npm install`, `mandrel sync`, and `mandrel update` with no manual
85
+ re-sync. Core payload commands of the same basename always win (the
86
+ local copy is ignored with a `shadowed` warning).
87
+
79
88
  ### F. Modular Global Rules
80
89
 
81
90
  Before writing code or documentation, verify if any domain-agnostic rules
@@ -1,10 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Projects .agents/workflows/ into a flat `.claude/commands/` tree so Claude
5
- * Code exposes each workflow as a bare `/<name>` slash command. The workflows
6
- * directory remains the single source of truth; this script is the only writer
7
- * of `.claude/commands/`.
4
+ * Projects .agents/workflows/ (and .agents/local/workflows/ when present) into
5
+ * a flat `.claude/commands/` tree so Claude Code exposes each workflow as a
6
+ * bare `/<name>` slash command. Two source directories are enumerated in order:
7
+ *
8
+ * 1. PAYLOAD_SRC — `.agents/workflows/` (the installed Mandrel payload)
9
+ * 2. LOCAL_SRC — `.agents/local/workflows/` (consumer-authored, prune-exempt)
10
+ *
11
+ * The payload directory wins on basename collision: if both sources supply
12
+ * `foo.md`, the payload copy is projected and the local copy is ignored with a
13
+ * `shadowed` warning. Because both sources are unioned into `sourceSet`, local
14
+ * commands are also protected from the orphan-reap — they survive
15
+ * `npm install`, `mandrel sync`, and `mandrel update` with no manual re-sync.
8
16
  *
9
17
  * Flat projection (reverts the #3576 plugin cutover): the plugin command tree
10
18
  * (`.claude/plugins/mandrel/`) and the repo-local marketplace
@@ -48,9 +56,14 @@ const PROJECT_ROOT = process.cwd();
48
56
  // fixture workflow tree in isolation (regression test for the Epic #1185
49
57
  // frontmatter pass-through contract). When unset, behaviour is unchanged
50
58
  // — the script defaults to the real workflows / commands directories.
51
- const SRC_DIR =
59
+ // SYNC_CLAUDE_COMMANDS_SRC overrides the PAYLOAD source only; the LOCAL_SRC
60
+ // is always derived from the project root so fixture tests can isolate the
61
+ // payload source while still allowing LOCAL_SRC to be present if needed.
62
+ const PAYLOAD_SRC =
52
63
  process.env.SYNC_CLAUDE_COMMANDS_SRC ??
53
64
  path.join(PROJECT_ROOT, '.agents', 'workflows');
65
+ const LOCAL_SRC = path.join(PROJECT_ROOT, '.agents', 'local', 'workflows');
66
+
54
67
  const DEST_DIR =
55
68
  process.env.SYNC_CLAUDE_COMMANDS_DEST ??
56
69
  path.join(PROJECT_ROOT, '.claude', 'commands');
@@ -58,6 +71,23 @@ const DEST_DIR =
58
71
  export const HEADER =
59
72
  '<!-- AUTO-GENERATED — do not edit. Source of truth: .agents/workflows/ -->\n<!-- Re-run: npm run sync:commands -->\n\n';
60
73
 
74
+ export const LOCAL_HEADER =
75
+ '<!-- AUTO-GENERATED from .agents/local/ — do not edit. Source of truth: .agents/local/workflows/ -->\n<!-- Re-run: npm run sync:commands -->\n\n';
76
+
77
+ /**
78
+ * Return true when the given directory path exists and is accessible.
79
+ *
80
+ * @param {string} dir
81
+ * @returns {boolean}
82
+ */
83
+ function dirExists(dir) {
84
+ try {
85
+ return fs.statSync(dir).isDirectory();
86
+ } catch {
87
+ return false;
88
+ }
89
+ }
90
+
61
91
  /**
62
92
  * Reap the generated plugin projection (the #3576 surface) so the namespaced
63
93
  * `/mandrel:<name>` commands and the repo-local marketplace stop shadowing the
@@ -102,12 +132,33 @@ fs.mkdirSync(DEST_DIR, { recursive: true });
102
132
  const isTopLevelWorkflow = (entry) =>
103
133
  entry.isFile() && entry.name.endsWith('.md');
104
134
 
135
+ // Enumerate sources: payload first, then local (if it exists). Payload wins
136
+ // on basename collision — a consumer must not silently shadow a core command.
137
+ const SRC_DIRS = [PAYLOAD_SRC, LOCAL_SRC].filter(dirExists);
138
+
139
+ /** @type {Array<{dir: string, name: string}>} */
140
+ const entries = SRC_DIRS.flatMap((dir) =>
141
+ fs
142
+ .readdirSync(dir, { withFileTypes: true })
143
+ .filter(isTopLevelWorkflow)
144
+ .map((e) => ({ dir, name: e.name })),
145
+ );
146
+
147
+ // Collision policy: payload wins, warn on a shadowed local file.
148
+ const byName = new Map();
149
+ for (const e of entries) {
150
+ if (byName.has(e.name)) {
151
+ Logger.warn(` shadowed ${e.name} (local copy ignored; payload wins)`);
152
+ continue;
153
+ }
154
+ byName.set(e.name, e);
155
+ }
156
+
157
+ // sourceSet drives the orphan-reap: any existing command not in this set is
158
+ // removed. Local-projected commands are included, so they survive the reap.
159
+ const sourceSet = new Set(byName.keys());
160
+
105
161
  const existing = fs.readdirSync(DEST_DIR).filter((f) => f.endsWith('.md'));
106
- const sources = fs
107
- .readdirSync(SRC_DIR, { withFileTypes: true })
108
- .filter(isTopLevelWorkflow)
109
- .map((entry) => entry.name);
110
- const sourceSet = new Set(sources);
111
162
 
112
163
  for (const file of existing) {
113
164
  if (!sourceSet.has(file)) {
@@ -118,18 +169,18 @@ for (const file of existing) {
118
169
 
119
170
  // Copy each workflow, injecting the auto-generated header after any leading
120
171
  // frontmatter (so the `---` block stays on line 1 and Claude Code parses the
121
- // command description). Parallelised so the ~30-file sync doesn't serialise on
122
- // per-file fs latency (noticeable on Windows where each syscall pays a larger
123
- // fixed cost).
172
+ // command description). Use a distinct header comment for local-origin files.
173
+ // Parallelised so the ~30-file sync doesn't serialise on per-file fs latency
174
+ // (noticeable on Windows where each syscall pays a larger fixed cost).
124
175
  let synced = 0;
176
+ const resolvedEntries = Array.from(byName.values());
125
177
  await Promise.all(
126
- sources.map(async (file) => {
127
- const content = await fs.promises.readFile(
128
- path.join(SRC_DIR, file),
129
- 'utf8',
130
- );
131
- const dest = path.join(DEST_DIR, file);
132
- const target = applyHeader(content, HEADER);
178
+ resolvedEntries.map(async ({ dir, name }) => {
179
+ const isLocal = dir === LOCAL_SRC;
180
+ const header = isLocal ? LOCAL_HEADER : HEADER;
181
+ const content = await fs.promises.readFile(path.join(dir, name), 'utf8');
182
+ const dest = path.join(DEST_DIR, name);
183
+ const target = applyHeader(content, header);
133
184
 
134
185
  // Skip write if content is already identical (avoid noisy git diffs).
135
186
  // Use try/catch over existsSync+readFile so we only pay one syscall.
@@ -142,10 +193,10 @@ await Promise.all(
142
193
 
143
194
  await fs.promises.writeFile(dest, target, 'utf8');
144
195
  synced++;
145
- Logger.info(` synced ${file}`);
196
+ Logger.info(` synced ${name}`);
146
197
  }),
147
198
  );
148
199
 
149
200
  Logger.info(
150
- `\n✔ ${synced} file(s) synced, ${sources.length} total commands in .claude/commands/`,
201
+ `\n✔ ${synced} file(s) synced, ${sourceSet.size} total commands in .claude/commands/`,
151
202
  );
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.75.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.74.0...mandrel-v1.75.0) (2026-06-19)
6
+
7
+
8
+ ### Added
9
+
10
+ * **sync:** project .agents/local/workflows/ as prune-exempt slash commands ([#4244](https://github.com/dsj1984/mandrel/issues/4244)) ([9470f6a](https://github.com/dsj1984/mandrel/commit/9470f6a567b656eee508c2ef10910454a084ee43))
11
+
5
12
  ## [1.74.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.73.0...mandrel-v1.74.0) (2026-06-19)
6
13
 
7
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "1.74.0",
3
+ "version": "1.75.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, personas, skills, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",