mandrel 2.38.0 → 2.39.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.
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  // .agents/scripts/validate-skills.js
3
3
  //
4
- // Walk `.agents/skills/{core,stack}/**/SKILL.md` via the shared parser
4
+ // Walk `SKILL.md` under both skills roots — the package payload
5
+ // (`.agents/skills/{core,stack}/`) and the consumer-writable local zone
6
+ // (`.agents/local/skills/{core,stack}/`, Story #5135) — via the shared parser
5
7
  // helper, validate each frontmatter block against
6
8
  // `.agents/schemas/skill.schema.json`, enforce Policy Capsule presence
7
- // (5–12 bullets), and verify membership in `.agents/skills/skills.index.json`
8
- // when that manifest exists. All findings are batched into a single
9
+ // (5–12 bullets), and verify membership in each root's own manifest when it
10
+ // exists. A consumer-authored skill is held to exactly the same bar as a
11
+ // shipped one; the roots are validated separately because each carries its
12
+ // own index (the shipped manifest is a payload file and must stay
13
+ // payload-only — see generate-skills-index.js). All findings are batched into a single
9
14
  // human-readable report; the process exits non-zero when any finding is
10
15
  // surfaced.
11
16
  //
@@ -30,7 +35,17 @@ import { parseStandardCliArgs } from './lib/cli/standard-args.js';
30
35
  import { runAsCli } from './lib/cli-utils.js';
31
36
  import { Logger } from './lib/Logger.js';
32
37
  import { parseSkill } from './lib/skills/parse-skill.js';
33
- import { collectSkillFiles } from './lib/skills/walk-skill-files.js';
38
+ import {
39
+ auditIndex,
40
+ indexPathFor,
41
+ readIndexPaths,
42
+ } from './lib/skills/skills-index.js';
43
+ import {
44
+ collectLocalSkillFiles,
45
+ collectSkillFiles,
46
+ LOCAL_SKILLS_SEGMENTS,
47
+ PAYLOAD_SKILLS_SEGMENTS,
48
+ } from './lib/skills/walk-skill-files.js';
34
49
 
35
50
  const MIN_CAPSULE_BULLETS = 5;
36
51
  const MAX_CAPSULE_BULLETS = 12;
@@ -118,56 +133,11 @@ function buildManifestValidator(repoRoot) {
118
133
  }
119
134
 
120
135
  /**
121
- * Read the on-disk skills.index.json manifest, returning `{ exists, paths,
122
- * manifest }` where `paths` is the Set of `entry.path` values when the file
123
- * is present and parseable, or null otherwise.
136
+ * Read one root's manifest into the `{ exists, paths, manifest, indexPath }`
137
+ * shape the findings pass consumes.
124
138
  */
125
- function readIndex(repoRoot) {
126
- const indexPath = path.join(
127
- repoRoot,
128
- '.agents',
129
- 'skills',
130
- 'skills.index.json',
131
- );
132
- if (!fs.existsSync(indexPath)) {
133
- return { exists: false, paths: null, manifest: null, indexPath };
134
- }
135
- try {
136
- const manifest = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
137
- const paths = new Set(
138
- Array.isArray(manifest.skills)
139
- ? manifest.skills
140
- .map((s) => s.path)
141
- .filter((p) => typeof p === 'string')
142
- : [],
143
- );
144
- return { exists: true, paths, manifest, indexPath };
145
- } catch (err) {
146
- return {
147
- exists: true,
148
- paths: null,
149
- manifest: null,
150
- indexPath,
151
- parseError: err.message,
152
- };
153
- }
154
- }
155
-
156
- /**
157
- * Validate a parsed manifest against skills-index.schema.json. Returns
158
- * finding strings tagged with the `manifest-schema` pillar.
159
- */
160
- function validateManifestSchema(manifest, indexRelPath, validateManifest) {
161
- const findings = [];
162
- if (!validateManifest(manifest)) {
163
- for (const err of validateManifest.errors ?? []) {
164
- const where = err.instancePath || '(root)';
165
- findings.push(
166
- `${indexRelPath}: manifest-schema: schema violation at ${where}: ${err.message}`,
167
- );
168
- }
169
- }
170
- return findings;
139
+ function readIndex(repoRoot, rootSegments = PAYLOAD_SKILLS_SEGMENTS) {
140
+ return readIndexPaths(indexPathFor(repoRoot, rootSegments));
171
141
  }
172
142
 
173
143
  /**
@@ -216,6 +186,7 @@ function rel(absPath, repoRoot) {
216
186
  /**
217
187
  * Pure entry point used by tests. Returns `{ status, output, findings }`.
218
188
  */
189
+
219
190
  export function run({ argv = [], repoRoot } = {}) {
220
191
  const parsed = parseArgs(argv);
221
192
  if (parsed.help) {
@@ -234,25 +205,41 @@ export function run({ argv = [], repoRoot } = {}) {
234
205
  const indexRel = rel(indexInfo.indexPath, root);
235
206
 
236
207
  const findings = [];
237
- if (!indexInfo.exists) {
238
- findings.push(
239
- `index missing: ${indexRel} not found — run 'node .agents/scripts/generate-skills-index.js'`,
240
- );
241
- } else if (indexInfo.paths === null) {
242
- findings.push(`index unparseable: ${indexRel} — ${indexInfo.parseError}`);
243
- } else if (indexInfo.manifest !== null) {
244
- findings.push(
245
- ...validateManifestSchema(indexInfo.manifest, indexRel, validateManifest),
246
- );
247
- }
208
+ findings.push(
209
+ ...auditIndex(indexInfo, indexRel, validateManifest, { required: true }),
210
+ );
248
211
 
249
- const skillFiles = collectSkillFiles(root);
212
+ const payloadFiles = collectSkillFiles(root);
250
213
  const indexPaths =
251
214
  indexInfo.exists && indexInfo.paths !== null ? indexInfo.paths : null;
252
- for (const file of skillFiles) {
215
+ for (const file of payloadFiles) {
253
216
  findings.push(...validateOne(file, root, validateFrontmatter, indexPaths));
254
217
  }
255
218
 
219
+ // The local zone is optional: a repo with no consumer-authored skills has
220
+ // no local root and no local index, and that is a clean run, not a finding.
221
+ const localFiles = collectLocalSkillFiles(root);
222
+ if (localFiles.length > 0) {
223
+ const localIndexInfo = readIndex(root, LOCAL_SKILLS_SEGMENTS);
224
+ const localIndexRel = rel(localIndexInfo.indexPath, root);
225
+ findings.push(
226
+ ...auditIndex(localIndexInfo, localIndexRel, validateManifest, {
227
+ required: true,
228
+ }),
229
+ );
230
+ const localIndexPaths =
231
+ localIndexInfo.exists && localIndexInfo.paths !== null
232
+ ? localIndexInfo.paths
233
+ : null;
234
+ for (const file of localFiles) {
235
+ findings.push(
236
+ ...validateOne(file, root, validateFrontmatter, localIndexPaths),
237
+ );
238
+ }
239
+ }
240
+
241
+ const skillFiles = [...payloadFiles, ...localFiles];
242
+
256
243
  if (findings.length === 0) {
257
244
  Logger.info(`validate-skills: ${skillFiles.length} skill(s) passed`);
258
245
  return { status: 0, output: '', findings };
@@ -106,7 +106,11 @@ browser surface") and stop. Never attempt a headless fallback.
106
106
  runs against via
107
107
  [`resolveQaEnvironment`](../scripts/lib/qa/resolve-qa-contract.js), yielding
108
108
  `{ name, baseUrl, signInSeam, allowWrites }` (`allowWrites` defaults to an
109
- explicit boolean — `true` only for the conventional `local` environment). When
109
+ explicit boolean — `true` only for the conventional `local` environment;
110
+ `signInSeam` is `null` when the environment declares none). A `{ skill }` seam
111
+ is resolved here too: the resolver throws when the id resolves to no
112
+ `SKILL.md`, so a dangling seam fails before the browser is driven, never
113
+ mid-sweep. When
110
114
  `<env>` is supplied, pass it straight through (an exact name wins; a raw URL
111
115
  matches by origin). When it is omitted (bare `/qa-run`), **prompt** the
112
116
  operator, enumerating every environment as `name → baseUrl` (marking
@@ -152,10 +156,14 @@ resolved environment's discriminated-union seam (anchored on `baseUrl`):
152
156
  input; under a `urlTemplate` seam the contract is authored as a plain name
153
157
  array and no per-persona auth material is read.
154
158
  - **`kind: 'skill'`** — invoke the named consumer sign-in skill (procedural /
155
- non-URL sign-in). Real auth uses **only `credentialRef`-indirected material**
156
- the skill dereferences; raw passwords, tokens, or API keys are never inlined
157
- into the contract, the workflow, or chat, and captured evidence is redacted
158
- per [`helpers/qa-core.md`](helpers/qa-core.md) before persistence.
159
+ non-URL sign-in), reading the `SKILL.md` at the seam's resolved `skillPath`.
160
+ Real auth uses **only `credentialRef`-indirected material** the skill
161
+ dereferences; raw passwords, tokens, or API keys are never inlined into the
162
+ contract, the workflow, or chat, and captured evidence is redacted per
163
+ [`helpers/qa-core.md`](helpers/qa-core.md) before persistence.
164
+ - **No seam (`signInSeam` absent → `null`)** — a declarable state, not a
165
+ defect. Drive only the unauthenticated surface and record the gap in the
166
+ envelope; never fabricate a session or enter real credentials by hand.
159
167
 
160
168
  **Verification (the envelope's proof).** After sign-in, confirm the
161
169
  authenticated state with a `take_snapshot` showing the persona badge (the user
package/docs/CHANGELOG.md CHANGED
@@ -15,6 +15,18 @@ All notable changes to this project will be documented in this file.
15
15
  -->
16
16
  <!-- markdownlint-disable-file MD004 MD012 MD037 -->
17
17
 
18
+ ## [2.39.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.38.0...mandrel-v2.39.0) (2026-09-05)
19
+
20
+
21
+ ### Added
22
+
23
+ * qa: make the `signInSeam` `{ skill }` arm authorable via a consumer-writable `.agents/local/skills/` root, and declarable-absent ([#5135](https://github.com/dsj1984/mandrel/issues/5135)) ([#5136](https://github.com/dsj1984/mandrel/issues/5136)) ([4a7c89e](https://github.com/dsj1984/mandrel/commit/4a7c89e55b83c20637c518950e3cdee5620a4442))
24
+
25
+
26
+ ### Fixed
27
+
28
+ * quality-preview --staged: re-base the staged scope to MERGE_HEAD so a base-sync merge commit is not scored for the base branch's work ([#5131](https://github.com/dsj1984/mandrel/issues/5131)) ([#5132](https://github.com/dsj1984/mandrel/issues/5132)) ([d6fccc1](https://github.com/dsj1984/mandrel/commit/d6fccc15738fe22965ff8e9c819a55fe69b39815))
29
+
18
30
  ## [2.38.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.37.0...mandrel-v2.38.0) (2026-09-04)
19
31
 
20
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.38.0",
3
+ "version": "2.39.0",
4
4
  "description": "Claude Code-first opinionated workflow framework: instructions, skills, rules, and SDLC workflows that govern AI coding assistants.",
5
5
  "files": [
6
6
  ".agents/",