mandrel 2.63.0 → 2.65.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.
Files changed (70) hide show
  1. package/.agents/agents/acceptance-critic.md +3 -2
  2. package/.agents/agents/auditor.md +3 -2
  3. package/.agents/agents/plan-critic.md +3 -2
  4. package/.agents/agents/story-worker.md +2 -2
  5. package/.agents/audit-checklists/quality.md +3 -0
  6. package/.agents/docs/agentrc-reference.json +1 -9
  7. package/.agents/docs/configuration.md +8 -7
  8. package/.agents/schemas/agentrc.schema.json +6 -13
  9. package/.agents/schemas/audit-rules.schema.json +1 -1
  10. package/.agents/schemas/story-deliver-terminal.schema.json +5 -0
  11. package/.agents/scripts/bootstrap.js +8 -2
  12. package/.agents/scripts/check-context-budget.js +1 -1
  13. package/.agents/scripts/lib/ITicketingProvider.js +1 -3
  14. package/.agents/scripts/lib/audit-suite/findings.js +1 -17
  15. package/.agents/scripts/lib/audit-suite/frontmatter.js +0 -28
  16. package/.agents/scripts/lib/audit-suite/index.js +0 -6
  17. package/.agents/scripts/lib/audit-suite/selector.js +0 -31
  18. package/.agents/scripts/lib/bootstrap/agents-md-fold.js +156 -0
  19. package/.agents/scripts/lib/bootstrap/commit-push.js +1 -1
  20. package/.agents/scripts/lib/bootstrap/manifest.js +2 -2
  21. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +13 -29
  22. package/.agents/scripts/lib/config/review-chain-default.js +13 -0
  23. package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
  24. package/.agents/scripts/lib/config-settings-schema-quality.js +11 -13
  25. package/.agents/scripts/lib/doc-tiers.js +25 -6
  26. package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
  27. package/.agents/scripts/lib/observability/metrics-ledger.js +0 -72
  28. package/.agents/scripts/lib/orchestration/ci-red-handling.js +73 -0
  29. package/.agents/scripts/lib/orchestration/code-review.js +11 -6
  30. package/.agents/scripts/lib/orchestration/deliver-recover.js +56 -11
  31. package/.agents/scripts/lib/orchestration/epic-rollup.js +29 -12
  32. package/.agents/scripts/lib/orchestration/merge-block-class.js +20 -4
  33. package/.agents/scripts/lib/orchestration/merge-poll.js +41 -22
  34. package/.agents/scripts/lib/orchestration/required-checks.js +147 -0
  35. package/.agents/scripts/lib/orchestration/review-providers/code-review.js +203 -0
  36. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +6 -4
  37. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -2
  38. package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +1 -0
  39. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +0 -12
  40. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +135 -20
  41. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +112 -82
  42. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +72 -5
  43. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +12 -87
  44. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +3 -0
  45. package/.agents/scripts/lib/templates/decomposer-prompts.js +5 -24
  46. package/.agents/scripts/pr-watch-with-update.js +13 -19
  47. package/.agents/scripts/providers/github/issues.js +14 -23
  48. package/.agents/scripts/sync-claude-agents.js +1 -1
  49. package/.agents/workflows/audit-quality.md +42 -7
  50. package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
  51. package/.agents/workflows/helpers/code-review.md +15 -38
  52. package/.agents/workflows/helpers/deliver-reference.md +4 -2
  53. package/.agents/workflows/helpers/deliver-story.md +3 -0
  54. package/.agents/workflows/helpers/plan-reference.md +9 -8
  55. package/.agents/workflows/mandrel-deliver.md +2 -1
  56. package/.agents/workflows/mandrel-plan.md +10 -7
  57. package/.agents/workflows/mandrel-update.md +5 -3
  58. package/docs/CHANGELOG.md +38 -0
  59. package/lib/cli/claude-code-version.js +73 -0
  60. package/lib/cli/doctor.js +2 -2
  61. package/lib/cli/registry.js +9 -0
  62. package/lib/cli/uninstall.js +37 -9
  63. package/lib/migrations/index.js +2 -0
  64. package/lib/migrations/steps/2.65.0-fold-claude-md-into-agents-md.js +38 -0
  65. package/package.json +2 -1
  66. package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +0 -99
  67. package/.agents/scripts/lib/audit-suite/runner.js +0 -205
  68. package/.agents/scripts/lib/audit-suite/substitutions.js +0 -96
  69. package/.agents/scripts/lib/audit-suite/workflow-loader.js +0 -37
  70. package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +0 -234
@@ -0,0 +1,38 @@
1
+ // lib/migrations/steps/2.65.0-fold-claude-md-into-agents-md.js
2
+ /**
3
+ * Fold a consumer's root CLAUDE.md into AGENTS.md and delete CLAUDE.md.
4
+ * The host reads CLAUDE.md exclusively whenever it exists, so a surviving one
5
+ * would shadow the AGENTS.md wiring. Shares the bootstrap fold contract.
6
+ */
7
+
8
+ import nodeFs from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ import {
12
+ foldClaudeMdIntoAgentsMd,
13
+ LEGACY_ENTRY_DOC,
14
+ } from '../../../.agents/scripts/lib/bootstrap/agents-md-fold.js';
15
+
16
+ export const foldClaudeMdIntoAgentsMdStep = {
17
+ version: '2.65.0',
18
+ description:
19
+ 'fold the root CLAUDE.md into AGENTS.md and delete it — AGENTS.md is ' +
20
+ 'now the entry doc (Story #5410)',
21
+ /**
22
+ * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
23
+ * @param {typeof nodeFs} [fsImpl]
24
+ * @returns {boolean}
25
+ */
26
+ detect(ctx, fsImpl = ctx?.fs ?? nodeFs) {
27
+ const root = ctx?.projectRoot ?? process.cwd();
28
+ return fsImpl.existsSync(path.join(root, LEGACY_ENTRY_DOC));
29
+ },
30
+ /**
31
+ * @param {{ projectRoot?: string, fs?: typeof nodeFs }} [ctx]
32
+ * @param {typeof nodeFs} [fsImpl]
33
+ * @returns {void}
34
+ */
35
+ apply(ctx, fsImpl = ctx?.fs ?? nodeFs) {
36
+ foldClaudeMdIntoAgentsMd(ctx?.projectRoot ?? process.cwd(), fsImpl);
37
+ },
38
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mandrel",
3
- "version": "2.63.0",
3
+ "version": "2.65.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/",
@@ -102,6 +102,7 @@
102
102
  "@cucumber/gherkin": "^42.0.1",
103
103
  "c8": "^11.0.0",
104
104
  "chokidar": "^5.0.0",
105
+ "fast-check": "^4.10.2",
105
106
  "husky": "^9.1.7",
106
107
  "jscpd": "^4.2.5",
107
108
  "knip": "^6.17.1",
@@ -1,99 +0,0 @@
1
- /**
2
- * Close-scope lens diff-floor: a diff under the changed-line floor that
3
- * touches no sensitive path skips lens materialization. Fail-open — every
4
- * degraded input (unknown count, unreadable manifest, floor `0`) means "do
5
- * not skip". Sensitive classes come from the same selector as review depth,
6
- * so the two never disagree. Total; no I/O beyond the injected git spawn.
7
- */
8
-
9
- import { gitSpawn } from '../git-utils.js';
10
- import { readNumstatRows } from '../orchestration/diff-magnitude.js';
11
- import { selectSensitivePathClasses } from './selector.js';
12
-
13
- /** Measured: zero-yield closes clustered well under this size. */
14
- export const DEFAULT_LENS_DIFF_FLOOR = 40;
15
-
16
- /**
17
- * Whole-diff additions + deletions (no companion exemption: the question is
18
- * "is this diff small"). Shares the numstat parse with the light path's
19
- * backstop. `null` on any git failure; binary rows count 0.
20
- *
21
- * @param {{
22
- * baseRef: string,
23
- * headRef: string,
24
- * cwd?: string,
25
- * gitSpawnFn?: typeof gitSpawn,
26
- * }} args
27
- * @returns {number|null}
28
- */
29
- export function countChangedLines({
30
- baseRef,
31
- headRef,
32
- cwd = process.cwd(),
33
- gitSpawnFn = gitSpawn,
34
- } = {}) {
35
- const rows = readNumstatRows({ baseRef, headRef, cwd, gitSpawnFn });
36
- if (rows === null) return null;
37
- return rows.reduce((total, row) => total + row.additions + row.deletions, 0);
38
- }
39
-
40
- /**
41
- * Skip only when the floor is enabled, the count is known and below it, and
42
- * no sensitive-path class matches; every other state names its reason.
43
- *
44
- * @param {{
45
- * changedFiles?: string[]|null,
46
- * changedLineCount?: number|null,
47
- * floor?: number,
48
- * injectedRules?: object,
49
- * selectSensitivePathClassesFn?: typeof selectSensitivePathClasses,
50
- * }} [input]
51
- * @returns {{
52
- * skip: boolean,
53
- * reason: 'floor-disabled'|'line-count-unknown'|'at-or-above-floor'|'sensitive-classes-unknown'|'sensitive-path-hit'|'below-floor',
54
- * floor: number,
55
- * changedLineCount: number|null,
56
- * sensitiveClasses: string[],
57
- * }}
58
- */
59
- export function evaluateLensDiffFloor(input = {}) {
60
- const floorRaw = input.floor;
61
- const floor =
62
- typeof floorRaw === 'number' && Number.isInteger(floorRaw) && floorRaw >= 0
63
- ? floorRaw
64
- : DEFAULT_LENS_DIFF_FLOOR;
65
- const count =
66
- typeof input.changedLineCount === 'number' &&
67
- Number.isFinite(input.changedLineCount) &&
68
- input.changedLineCount >= 0
69
- ? Math.floor(input.changedLineCount)
70
- : null;
71
- const verdict = (skip, reason, sensitiveClasses = []) => ({
72
- skip,
73
- reason,
74
- floor,
75
- changedLineCount: count,
76
- sensitiveClasses,
77
- });
78
-
79
- if (floor <= 0) return verdict(false, 'floor-disabled');
80
- if (count === null) return verdict(false, 'line-count-unknown');
81
- if (count >= floor) return verdict(false, 'at-or-above-floor');
82
-
83
- const select =
84
- input.selectSensitivePathClassesFn ?? selectSensitivePathClasses;
85
- let classes;
86
- try {
87
- classes = select({
88
- changedFiles: Array.isArray(input.changedFiles) ? input.changedFiles : [],
89
- injectedRules: input.injectedRules,
90
- });
91
- } catch {
92
- return verdict(false, 'sensitive-classes-unknown');
93
- }
94
- const matched = Array.isArray(classes) ? classes : [];
95
- if (matched.length > 0) {
96
- return verdict(false, 'sensitive-path-hit', matched);
97
- }
98
- return verdict(true, 'below-floor');
99
- }
@@ -1,205 +0,0 @@
1
- /**
2
- * `runAuditSuite`: owns the audit envelope shape (`metadata`, `findings`,
3
- * `workflows`) and the per-audit fan-out; invoked via the barrel.
4
- */
5
-
6
- import fs from 'node:fs/promises';
7
- import path from 'node:path';
8
- import { getPaths, PROJECT_ROOT, resolveConfig } from '../config-resolver.js';
9
- import { ValidationError } from '../errors/index.js';
10
- import { aggregateSummary } from './findings.js';
11
- import { summarizeWorkflow } from './frontmatter.js';
12
- import { applySubstitutions, computeAllowedKeys } from './substitutions.js';
13
- import { defaultWriteArtifact, loadWorkflow } from './workflow-loader.js';
14
-
15
- async function loadRules(paths) {
16
- const rulesPath = path.join(
17
- PROJECT_ROOT,
18
- paths.schemasRoot,
19
- 'audit-rules.json',
20
- );
21
- const rulesContent = await fs.readFile(rulesPath, 'utf8');
22
- return JSON.parse(rulesContent);
23
- }
24
-
25
- function rejectUnknownKeys(allowedKeys, callerSubstitutions) {
26
- const unknownKeys = Object.keys(callerSubstitutions).filter(
27
- (k) => !allowedKeys.has(k),
28
- );
29
- if (unknownKeys.length === 0) return;
30
- const allowedList = [...allowedKeys].sort().join(', ');
31
- throw new ValidationError(
32
- `Unknown substitution key(s): ${unknownKeys.join(', ')}. Allowed for this call: ${allowedList}.`,
33
- { unknownKeys, allowedKeys: [...allowedKeys] },
34
- );
35
- }
36
-
37
- function emptyEnvelope(auditWorkflows) {
38
- return {
39
- metadata: {
40
- timestamp: new Date().toISOString(),
41
- auditsRequested: auditWorkflows,
42
- auditsRun: [],
43
- summary: { critical: 0, high: 0, medium: 0, low: 0 },
44
- },
45
- findings: [],
46
- workflows: [],
47
- };
48
- }
49
-
50
- function notDefinedFinding(auditName) {
51
- return {
52
- error: true,
53
- finding: {
54
- audit: auditName,
55
- severity: 'low',
56
- message: `Requested audit workflow '${auditName}' is not defined in audit-rules.json.`,
57
- },
58
- };
59
- }
60
-
61
- function notFoundFinding(auditName) {
62
- return {
63
- error: true,
64
- finding: {
65
- audit: auditName,
66
- severity: 'low',
67
- message: `Audit workflow '${auditName}.md' not found in workflows directory.`,
68
- },
69
- };
70
- }
71
-
72
- async function processAudit({
73
- auditName,
74
- validAudits,
75
- loader,
76
- workflowsDir,
77
- effectiveSubstitutions,
78
- }) {
79
- if (!validAudits.includes(auditName)) {
80
- return notDefinedFinding(auditName);
81
- }
82
-
83
- const workflow = await loader(auditName, workflowsDir);
84
- if (!workflow) {
85
- return notFoundFinding(auditName);
86
- }
87
-
88
- const substituted = applySubstitutions(
89
- workflow.content,
90
- effectiveSubstitutions,
91
- );
92
-
93
- return {
94
- success: true,
95
- auditName,
96
- workflowPath: workflow.path ?? null,
97
- workflowContent: substituted,
98
- summary: summarizeWorkflow(workflow.content),
99
- byteSize: Buffer.byteLength(substituted, 'utf8'),
100
- };
101
- }
102
-
103
- async function reduceResults({
104
- results,
105
- envelope,
106
- artifactPrefix,
107
- effectiveArtifactsDir,
108
- writeArtifact,
109
- }) {
110
- for (const result of results) {
111
- if (result.error) {
112
- envelope.findings.push(result.finding);
113
- continue;
114
- }
115
- if (!result.success) continue;
116
-
117
- envelope.metadata.auditsRun.push(result.auditName);
118
- let artifactPath = null;
119
- if (artifactPrefix) {
120
- const fileName = `audit-${artifactPrefix}-${result.auditName}.md`;
121
- artifactPath = await writeArtifact(
122
- effectiveArtifactsDir,
123
- fileName,
124
- result.workflowContent,
125
- );
126
- }
127
- envelope.workflows.push({
128
- audit: result.auditName,
129
- path: result.workflowPath,
130
- summary: result.summary,
131
- byteSize: result.byteSize,
132
- artifactPath,
133
- });
134
- }
135
- }
136
-
137
- /**
138
- * Resolve registered audit workflows into slim descriptors; with
139
- * `artifactPrefix`, full bodies are written to disk instead of travelling in
140
- * the envelope. Substitution keys beyond the built-ins must be declared as
141
- * `substitutionKeys` in audit-rules.json, else ValidationError.
142
- *
143
- * @param {object} opts
144
- * @param {string[]} opts.auditWorkflows
145
- * @param {Record<string,string>} [opts.substitutions]
146
- * @param {string} [opts.artifactPrefix] - writes `audit-<prefix>-<audit>.md`.
147
- * @param {string} [opts.artifactsDir] - defaults to `<auditOutputDir>`.
148
- * @param {Function} [opts.injectedLoadWorkflow]
149
- * @param {object} [opts.injectedRules]
150
- * @param {Function} [opts.injectedWriteArtifact]
151
- * @returns {Promise<object>} Aggregated audit results.
152
- */
153
- export async function runAuditSuite({
154
- auditWorkflows,
155
- substitutions,
156
- artifactPrefix,
157
- artifactsDir,
158
- injectedLoadWorkflow,
159
- injectedRules,
160
- injectedWriteArtifact,
161
- }) {
162
- const config = resolveConfig();
163
- const paths = getPaths(config);
164
- const callerSubstitutions = substitutions ?? {};
165
- const rules = injectedRules ?? (await loadRules(paths));
166
-
167
- const allowedKeys = computeAllowedKeys(rules, auditWorkflows);
168
- rejectUnknownKeys(allowedKeys, callerSubstitutions);
169
-
170
- const effectiveSubstitutions = {
171
- auditOutputDir: paths.auditOutputDir,
172
- ...callerSubstitutions,
173
- };
174
-
175
- const validAudits = Object.keys(rules.audits || {});
176
- const envelope = emptyEnvelope(auditWorkflows);
177
- const workflowsDir = path.join(PROJECT_ROOT, paths.workflowsRoot);
178
- const effectiveArtifactsDir =
179
- artifactsDir ?? path.join(PROJECT_ROOT, paths.auditOutputDir);
180
- const writeArtifact = injectedWriteArtifact ?? defaultWriteArtifact;
181
- const loader = injectedLoadWorkflow ?? loadWorkflow;
182
-
183
- const results = await Promise.all(
184
- auditWorkflows.map((auditName) =>
185
- processAudit({
186
- auditName,
187
- validAudits,
188
- loader,
189
- workflowsDir,
190
- effectiveSubstitutions,
191
- }),
192
- ),
193
- );
194
-
195
- await reduceResults({
196
- results,
197
- envelope,
198
- artifactPrefix,
199
- effectiveArtifactsDir,
200
- writeArtifact,
201
- });
202
-
203
- envelope.metadata.summary = aggregateSummary(envelope.findings);
204
- return envelope;
205
- }
@@ -1,96 +0,0 @@
1
- /**
2
- * `{{key}}` substitution for audit workflow bodies, plus the CLI glue and the
3
- * per-run allowed-key set.
4
- */
5
-
6
- import { ValidationError } from '../errors/index.js';
7
-
8
- export const BUILT_IN_SUBSTITUTION_KEYS = Object.freeze([
9
- 'auditOutputDir',
10
- 'ticketId',
11
- 'baseBranch',
12
- // Newline-joined change set; when unsubstituted, lens templates read the
13
- // literal as "no scope filter".
14
- 'changedFiles',
15
- ]);
16
-
17
- function escapeRegExp(str) {
18
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
19
- }
20
-
21
- /**
22
- * Unrecognised placeholders are left intact; keys are validated upstream.
23
- *
24
- * @param {string} content
25
- * @param {Record<string, string>} substitutions
26
- * @returns {string}
27
- */
28
- export function applySubstitutions(content, substitutions) {
29
- let out = content;
30
- for (const [key, value] of Object.entries(substitutions)) {
31
- out = out.replace(
32
- new RegExp(`\\{\\{${escapeRegExp(key)}\\}\\}`, 'g'),
33
- value,
34
- );
35
- }
36
- return out;
37
- }
38
-
39
- /**
40
- * Throws {@link ValidationError} on a missing `=` or empty key.
41
- *
42
- * @param {string[]} [pairs]
43
- * @returns {Record<string, string>}
44
- */
45
- export function parseSubstitutionPairs(pairs = []) {
46
- const out = {};
47
- for (const entry of pairs) {
48
- const eq = entry.indexOf('=');
49
- if (eq <= 0) {
50
- throw new ValidationError(
51
- `Invalid --substitution "${entry}"; expected key=value.`,
52
- { entry },
53
- );
54
- }
55
- const key = entry.slice(0, eq);
56
- const value = entry.slice(eq + 1);
57
- out[key] = value;
58
- }
59
- return out;
60
- }
61
-
62
- /**
63
- * Fill `ticketId`/`baseBranch` from flags in place, unless an explicit
64
- * `--substitution` already set them.
65
- *
66
- * @param {Record<string, unknown>} values
67
- * @param {Record<string, string|undefined>} substitutions
68
- */
69
- export function applyImplicitSubstitutions(values, substitutions) {
70
- if (values.ticket && substitutions.ticketId === undefined) {
71
- substitutions.ticketId = String(values.ticket);
72
- }
73
- if (values['base-branch'] && substitutions.baseBranch === undefined) {
74
- substitutions.baseBranch = values['base-branch'];
75
- }
76
- }
77
-
78
- /**
79
- * Built-ins plus each requested audit's declared keys; unregistered audits
80
- * are rejected elsewhere.
81
- *
82
- * @param {{ audits?: Record<string, { substitutionKeys?: string[] }> }} rules
83
- * @param {string[]} auditWorkflows
84
- * @returns {Set<string>}
85
- */
86
- export function computeAllowedKeys(rules, auditWorkflows) {
87
- const allowed = new Set(BUILT_IN_SUBSTITUTION_KEYS);
88
- for (const auditName of auditWorkflows) {
89
- const entry = rules.audits?.[auditName];
90
- if (!entry) continue;
91
- for (const k of entry.substitutionKeys ?? []) {
92
- allowed.add(k);
93
- }
94
- }
95
- return allowed;
96
- }
@@ -1,37 +0,0 @@
1
- /**
2
- * Filesystem IO for audit workflows, kept out of the runner so tests can
3
- * inject stubs.
4
- */
5
-
6
- import fs from 'node:fs/promises';
7
- import path from 'node:path';
8
-
9
- /**
10
- * `null` when missing; the runner records a finding rather than failing.
11
- *
12
- * @param {string} auditName
13
- * @param {string} workflowsDir absolute path to the workflows root
14
- * @returns {Promise<{ path: string, content: string } | null>}
15
- */
16
- export async function loadWorkflow(auditName, workflowsDir) {
17
- const workflowPath = path.join(workflowsDir, `${auditName}.md`);
18
- try {
19
- const content = await fs.readFile(workflowPath, 'utf8');
20
- return { path: workflowPath, content };
21
- } catch {
22
- return null;
23
- }
24
- }
25
-
26
- /**
27
- * @param {string} artifactsDir
28
- * @param {string} fileName
29
- * @param {string} content
30
- * @returns {Promise<string>}
31
- */
32
- export async function defaultWriteArtifact(artifactsDir, fileName, content) {
33
- await fs.mkdir(artifactsDir, { recursive: true });
34
- const fullPath = path.join(artifactsDir, fileName);
35
- await fs.writeFile(fullPath, content, 'utf8');
36
- return fullPath;
37
- }