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.
- package/.agents/README.md +45 -8
- package/.agents/docs/agentrc-reference.json +1 -4
- package/.agents/docs/configuration.md +2 -2
- package/.agents/schemas/agentrc.schema.json +6 -7
- package/.agents/scripts/generate-skills-index.js +158 -75
- package/.agents/scripts/lib/changed-files.js +100 -9
- package/.agents/scripts/lib/config-settings-schema.js +25 -7
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +58 -6
- package/.agents/scripts/lib/skills/skills-index.js +168 -0
- package/.agents/scripts/lib/skills/walk-skill-files.js +133 -9
- package/.agents/scripts/quality-preview.js +50 -9
- package/.agents/scripts/validate-skills.js +53 -66
- package/.agents/workflows/qa-run.md +13 -5
- package/docs/CHANGELOG.md +12 -0
- package/package.json +1 -1
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// .agents/scripts/validate-skills.js
|
|
3
3
|
//
|
|
4
|
-
// Walk
|
|
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
|
|
8
|
-
//
|
|
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 {
|
|
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
|
|
122
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
|
212
|
+
const payloadFiles = collectSkillFiles(root);
|
|
250
213
|
const indexPaths =
|
|
251
214
|
indexInfo.exists && indexInfo.paths !== null ? indexInfo.paths : null;
|
|
252
|
-
for (const file of
|
|
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
|
|
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).
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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