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.
package/.agents/instructions.md
CHANGED
|
@@ -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/
|
|
5
|
-
* Code exposes each workflow as a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
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).
|
|
122
|
-
//
|
|
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
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
131
|
-
const
|
|
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 ${
|
|
196
|
+
Logger.info(` synced ${name}`);
|
|
146
197
|
}),
|
|
147
198
|
);
|
|
148
199
|
|
|
149
200
|
Logger.info(
|
|
150
|
-
`\n✔ ${synced} file(s) synced, ${
|
|
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