mandrel 2.34.0 → 2.36.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/docs/agentrc-reference.json +3 -1
- package/.agents/docs/configuration.md +2 -0
- package/.agents/docs/quality-gates.md +30 -0
- package/.agents/docs/workflows.md +2 -1
- package/.agents/schemas/agentrc.schema.json +11 -0
- package/.agents/schemas/audit-rules.json +44 -0
- package/.agents/schemas/audit-rules.schema.json +1 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- package/.agents/scripts/check-doc-links.js +23 -2
- package/.agents/scripts/git-cleanup.js +2 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +7 -2
- package/.agents/scripts/lib/close-validation/projections/crap.js +8 -6
- package/.agents/scripts/lib/config/ci.js +18 -0
- package/.agents/scripts/lib/config-settings-schema-delivery.js +13 -0
- package/.agents/scripts/lib/coverage-capture-fullscope.js +5 -2
- package/.agents/scripts/lib/coverage-capture.js +96 -26
- package/.agents/scripts/lib/findings/route-finding.js +98 -35
- package/.agents/scripts/lib/maintainability-utils.js +6 -14
- package/.agents/scripts/lib/observability/source-classifier.js +0 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +22 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -14
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/merged-tip.js +132 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +56 -11
- package/.agents/scripts/lib/orchestration/merge-block-class.js +10 -1
- package/.agents/scripts/lib/orchestration/merge-poll.js +164 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +145 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +96 -5
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +9 -1
- package/.agents/scripts/lib/source-extensions.js +76 -0
- package/.agents/scripts/notify.js +4 -10
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +1 -1
- package/.agents/workflows/audit-adrs.md +270 -0
- package/.agents/workflows/audit-documentation.md +22 -6
- package/docs/CHANGELOG.md +33 -0
- package/package.json +3 -3
- package/.agents/scripts/generate-lifecycle-docs.js +0 -237
|
@@ -7,16 +7,10 @@
|
|
|
7
7
|
* Single dispatch entry point for runtime notifications across two
|
|
8
8
|
* independent channels.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `notify()` payload. Direct inline calls at phase boundaries are no
|
|
15
|
-
* longer the canonical path — listeners on the bus are. See
|
|
16
|
-
* [`docs/LIFECYCLE.md`](../docs/LIFECYCLE.md) for the bus contract,
|
|
17
|
-
* event taxonomy, and the dispatcher's wiring. Direct CLI / library
|
|
18
|
-
* invocations remain supported for one-shot operator commands and the
|
|
19
|
-
* structured-comment back-channel.
|
|
10
|
+
* Direct inline calls at phase boundaries are the only path: `notify()`
|
|
11
|
+
* is invoked from the caller that has something to say. Direct CLI /
|
|
12
|
+
* library invocations are equally supported, for one-shot operator
|
|
13
|
+
* commands and the structured-comment back-channel.
|
|
20
14
|
*
|
|
21
15
|
* Channels:
|
|
22
16
|
*
|
|
@@ -13,7 +13,7 @@ description:
|
|
|
13
13
|
- Document the **why**, not the what. Capture context, constraints, alternatives considered, and trade-offs — code already shows what was built.
|
|
14
14
|
- Write an ADR for any decision that would be expensive to reverse (framework choice, data model, auth strategy, API architecture, hosting platform).
|
|
15
15
|
- Mandrel ships **two first-class decisions-log layouts** — pick one at onboarding (see [Decisions-log layouts](reference.md#decisions-log-layouts)): the **single-file dated-entry** `docs/decisions.md` (default; best for small projects) or the **index + `docs/decisions/` directory** (MADR-style, one file per ADR; best once the log outgrows a single file). Either way, the canonical ADR sections are **Status, Date, Deciders, Context, Decision, (Alternatives Considered), Consequences**.
|
|
16
|
-
- Mark an ADR's status as `Accepted`, `Superseded by ADR-XXX`, or `
|
|
16
|
+
- Mark an ADR's status as `Accepted`, `Superseded by ADR-XXX`, `Deprecated`, or `Reverted (<date>)` — a reverted decision was **undone** rather than replaced, so unlike a superseded one it has no successor to point at. Never silently delete an ADR — supersede it.
|
|
17
17
|
- Do **not** document obvious code; do **not** restate what the code already says. Stale or redundant docs are worse than no docs.
|
|
18
18
|
- Comments explain **non-obvious intent** (the why). If a comment describes what the code does, refactor the code instead.
|
|
19
19
|
- Keep user-facing docs (README, API docs, changelog) updated as part of the change — out-of-date docs are bugs.
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: >-
|
|
3
|
+
Audit the decisions log as a live contract — Accepted ADRs whose claims the
|
|
4
|
+
tree has outgrown, broken supersede chains, structural gaps, and directional
|
|
5
|
+
changes that landed with no decision recorded. A deliberately-global lens over
|
|
6
|
+
whichever decisions-log layout the consumer ships.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Decisions-Log (ADR) Audit
|
|
10
|
+
|
|
11
|
+
You are a Staff Engineer & Decision Historian verifying that the consumer's
|
|
12
|
+
**decisions log still describes the system that exists**. An ADR is not prose
|
|
13
|
+
that merely rots — it is retrieved as *authority*: agents and humans read an
|
|
14
|
+
`Accepted` decision as the settled answer and do not re-litigate it. So an
|
|
15
|
+
Accepted ADR the tree has outgrown is worse than no ADR at all — it actively
|
|
16
|
+
teaches a wrong contract, and it keeps teaching it until someone supersedes it.
|
|
17
|
+
That is this lens's central target; structural tidiness is the cheap part.
|
|
18
|
+
|
|
19
|
+
The shared lens machinery — read-only constraint, scope interpretation, report
|
|
20
|
+
envelope + finding-block skeleton, severity scale, self-cross-check, and
|
|
21
|
+
execution strategy — lives in
|
|
22
|
+
[`helpers/audit-lens-core.md`](helpers/audit-lens-core.md). Write the report to
|
|
23
|
+
`{{auditOutputDir}}/audit-adrs-results.md`. Dimension values:
|
|
24
|
+
`Decision Drift | Supersede-Chain Integrity | Structure & Status Hygiene |
|
|
25
|
+
Missing Decision | Layout Conformance`; the report adds a **Decision Coverage**
|
|
26
|
+
table.
|
|
27
|
+
|
|
28
|
+
## Applicability & layout detection
|
|
29
|
+
|
|
30
|
+
**Mandrel ships two first-class decisions-log layouts** (the
|
|
31
|
+
[`core/documentation-and-adrs`](../skills/core/documentation-and-adrs/SKILL.md)
|
|
32
|
+
Policy Capsule is the SSOT), and this lens reads whichever one the consumer
|
|
33
|
+
adopted. **Both layouts keep the same entry file** — `decisions.md` under the
|
|
34
|
+
configured docs root — so its mere presence never identifies the layout. Detect
|
|
35
|
+
in this order, resolving `<docsRoot>` from `project.paths.docsRoot` in
|
|
36
|
+
`.agentrc.json` (default `docs`):
|
|
37
|
+
|
|
38
|
+
1. **Neither `<docsRoot>/decisions.md` nor `<docsRoot>/decisions/` exists** →
|
|
39
|
+
the project keeps no decisions log. Emit the not-applicable report below and
|
|
40
|
+
stop. Never invent a log, and never infer decisions from commit history.
|
|
41
|
+
2. **`decisions.md` only** → **single-file dated-entry layout** (the default).
|
|
42
|
+
Every ADR body lives in that one file as an append-only entry.
|
|
43
|
+
3. **`decisions.md` + a `decisions/` directory** → read the entry file to tell
|
|
44
|
+
the two apart. When it is predominantly an **index** (one row or link per
|
|
45
|
+
ADR pointing into `decisions/`), this is the **index + `decisions/`
|
|
46
|
+
directory** (MADR-style) layout, and each `decisions/NNNN-*.md` file is an
|
|
47
|
+
ADR body. When it instead carries full ADR bodies *and* a `decisions/`
|
|
48
|
+
directory holds further ADRs, the log is a hybrid — conformant **only** when
|
|
49
|
+
every file under `decisions/` is reachable from the entry file by an
|
|
50
|
+
index row or an in-entry pointer; otherwise it is a Layout Conformance
|
|
51
|
+
finding (an unreferenced ADR body is invisible to every reader who starts,
|
|
52
|
+
as they must, at the entry file).
|
|
53
|
+
4. **`decisions/` only, with no entry file** → a Layout Conformance finding:
|
|
54
|
+
the entry file is the mandatory-read surface both layouts guarantee, and
|
|
55
|
+
without it the directory's ADRs are unreachable from the docs context.
|
|
56
|
+
|
|
57
|
+
**Override.** An operator may pass `--paths <file ...>` (audit specific ADR
|
|
58
|
+
files) or `--dir <path>` (treat that directory as the decisions root) to point
|
|
59
|
+
the lens at a non-conventional location. These flags are the **only** override:
|
|
60
|
+
there is deliberately no `.agentrc.json` key for the decisions-log location, so
|
|
61
|
+
detection stays derived from the skill's two layouts rather than from
|
|
62
|
+
configuration a consumer must maintain.
|
|
63
|
+
|
|
64
|
+
## Whole-log scope (global lens)
|
|
65
|
+
|
|
66
|
+
Unlike the change-set-scoped lenses, this lens **always evaluates the whole
|
|
67
|
+
decisions log**, even when the change that triggered it touched one file.
|
|
68
|
+
Decision integrity is a global property: a supersede chain spans entries the
|
|
69
|
+
change set never names, and — the load-bearing case — a code change *elsewhere*
|
|
70
|
+
is exactly what invalidates an Accepted ADR's claims. Narrowing to the change
|
|
71
|
+
set would blind the lens to its primary finding class.
|
|
72
|
+
|
|
73
|
+
Accordingly this lens declares `"scope": "global"` in
|
|
74
|
+
[`audit-rules.json`](../schemas/audit-rules.json) — the single source of truth
|
|
75
|
+
`resolveLensTier` in
|
|
76
|
+
[`lib/audit-suite/selector.js`](../scripts/lib/audit-suite/selector.js) reads —
|
|
77
|
+
and is **exempt from the cross-epic-leak guard** that narrows every other
|
|
78
|
+
lens's evidence to its `changedFiles`. The exemption is scoped to this lens
|
|
79
|
+
only; the guard is not weakened for any other lens.
|
|
80
|
+
|
|
81
|
+
```text
|
|
82
|
+
{{changedFiles}}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- For this lens, **ignore** the `{{changedFiles}}` block above even when it is
|
|
86
|
+
populated: the decisions log is evaluated whole regardless. The block is
|
|
87
|
+
rendered only for envelope-shape parity with the scoped lenses. Do use it,
|
|
88
|
+
when populated, as a **prioritization hint** — an Accepted ADR whose subject
|
|
89
|
+
the change set touches is the first one to claim-check — never as a filter on
|
|
90
|
+
what is audited or reported.
|
|
91
|
+
|
|
92
|
+
## Execution strategy
|
|
93
|
+
|
|
94
|
+
Run this lens as a single `subagent_type: auditor` dispatch returning the report
|
|
95
|
+
path + Executive Summary; sequential inline execution is the fallback (see the
|
|
96
|
+
core's Execution strategy). On a large log, the Decision Drift claim-check
|
|
97
|
+
(Step 2.1) is the one dimension worth fanning out per batch of entries under
|
|
98
|
+
parallel-tooling Rule 3 — merge under the shared self-cross-check.
|
|
99
|
+
|
|
100
|
+
## Step 1: Deterministic structure sweep first
|
|
101
|
+
|
|
102
|
+
Run the cheap exact checks before reading any ADR for meaning — they
|
|
103
|
+
de-duplicate the easy findings and give Step 2 its inventory. Adjust the paths
|
|
104
|
+
below to the detected layout (or the `--dir` / `--paths` override):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Entry file + any ADR bodies (single-file layout yields just the entry file).
|
|
108
|
+
ls docs/decisions.md docs/decisions/*.md 2>/dev/null
|
|
109
|
+
|
|
110
|
+
# Status lines and their spelling — the vocabulary is Accepted / Superseded
|
|
111
|
+
# by … / Deprecated / Reverted (…); anything else is a Structure & Status
|
|
112
|
+
# Hygiene finding.
|
|
113
|
+
grep -rn '^\*\*Status:\*\*\|^- \*\*Status:\*\*' docs/decisions.md docs/decisions/ 2>/dev/null
|
|
114
|
+
|
|
115
|
+
# Entry headings, for the id/date/uniqueness checks in Step 1's list below.
|
|
116
|
+
grep -n '^## ' docs/decisions.md 2>/dev/null
|
|
117
|
+
|
|
118
|
+
# Link integrity across the docs surface, including every ADR cross-reference.
|
|
119
|
+
node .agents/scripts/check-doc-links.js
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
From that output, resolve deterministically — each of these is a
|
|
123
|
+
**Structure & Status Hygiene** finding except where noted:
|
|
124
|
+
|
|
125
|
+
1. **Canonical sections.** Every ADR carries the canonical set the skill's
|
|
126
|
+
Policy Capsule names — **Status, Date, Deciders, Context, Decision,
|
|
127
|
+
(Alternatives Considered), Consequences**. `Alternatives Considered` is
|
|
128
|
+
optional; a missing `Status`, `Context`, `Decision`, or `Consequences` is a
|
|
129
|
+
finding, and a missing `Status` is the most severe of them because every
|
|
130
|
+
other dimension keys off it.
|
|
131
|
+
2. **Status vocabulary.** Each status reads `Accepted`, `Superseded by <ref>`,
|
|
132
|
+
`Deprecated`, or `Reverted (<date>)` — a reverted decision was **undone**
|
|
133
|
+
rather than replaced, so unlike a superseded one it has no successor to
|
|
134
|
+
point at, and its missing `by <ref>` is correct rather than a defect. A
|
|
135
|
+
free-invented status word — anything outside those four — leaves the entry
|
|
136
|
+
unclassifiable by this lens and by every reader.
|
|
137
|
+
3. **Unique, stable ids.** No two entries share an id/anchor; MADR files use
|
|
138
|
+
zero-padded sequential numbering matching their heading id.
|
|
139
|
+
4. **Parseable dates.** Every `Date` parses, and no entry is dated in the
|
|
140
|
+
future.
|
|
141
|
+
5. **Link integrity.** Report what `check-doc-links.js` finds inside the
|
|
142
|
+
decisions surface; leave findings outside it to `audit-documentation`.
|
|
143
|
+
|
|
144
|
+
This lens orchestrates the existing checker only — it adds no new deterministic
|
|
145
|
+
checker script, and the sweep above is inline shell by design.
|
|
146
|
+
|
|
147
|
+
## Step 2: Evaluation dimensions
|
|
148
|
+
|
|
149
|
+
### 2.1 Decision Drift — **`Accepted` entries only**
|
|
150
|
+
|
|
151
|
+
This is the lens's primary value. For each **Accepted** entry, extract its
|
|
152
|
+
load-bearing claims — the scripts, files, directories, commands, flags, config
|
|
153
|
+
keys, contracts, and mechanisms it names as decided — and verify each against
|
|
154
|
+
the current tree, exactly as a documentation claim-check would. Flag an
|
|
155
|
+
Accepted decision whose subject the code has moved past: a named mechanism that
|
|
156
|
+
no longer exists, a contract the implementation has replaced, a path or command
|
|
157
|
+
that resolves to nothing, a constraint the tree now routinely violates.
|
|
158
|
+
|
|
159
|
+
**Scope this claim-check to Accepted entries and no others.** A `Superseded`
|
|
160
|
+
or `Deprecated` entry is *supposed* to describe a world that no longer exists —
|
|
161
|
+
claim-checking it manufactures findings out of correctly-retired history, and
|
|
162
|
+
on a long log it is also where the cost would go. Superseded and Deprecated
|
|
163
|
+
entries get the chain checks in 2.2 and the structure checks in Step 1, and
|
|
164
|
+
nothing else.
|
|
165
|
+
|
|
166
|
+
**Remediation is always supersede-or-amend, never silent edit or deletion** —
|
|
167
|
+
the skill's lifecycle rule is that an ADR is superseded in place, never pruned
|
|
168
|
+
or archived. Say which in the finding: amend when the decision still holds and
|
|
169
|
+
only its details moved; supersede when the decision itself no longer describes
|
|
170
|
+
what the project does.
|
|
171
|
+
|
|
172
|
+
Severity guidance: **High** — an Accepted ADR whose central decision the code
|
|
173
|
+
contradicts (it will be retrieved and believed); **Medium** — an Accepted ADR
|
|
174
|
+
whose supporting details drifted while its decision still holds; **Low** —
|
|
175
|
+
cosmetic staleness (an old path in an aside, a renamed tool in an example).
|
|
176
|
+
|
|
177
|
+
### 2.2 Supersede-Chain Integrity
|
|
178
|
+
|
|
179
|
+
The chain is what keeps a retired decision honest, so audit it as a graph:
|
|
180
|
+
|
|
181
|
+
- **Every supersede reference resolves** to an ADR that exists (a heading
|
|
182
|
+
anchor in the single-file layout, a file in the directory layout).
|
|
183
|
+
- **No cycles**, and no entry superseding itself.
|
|
184
|
+
- **Index ↔ entry agreement** (directory layout, and any single-file log
|
|
185
|
+
carrying a summary table): the status in the index row matches the status in
|
|
186
|
+
the ADR body. A row saying `Accepted` over a body saying `Superseded` is a
|
|
187
|
+
finding — readers stop at the index.
|
|
188
|
+
- **Partial supersessions name what survives.** An entry recording that *some*
|
|
189
|
+
rows or clauses are superseded while the rest stand MUST say precisely which;
|
|
190
|
+
an unscoped "partly superseded" leaves every clause ambiguous.
|
|
191
|
+
- **No two Accepted entries contradict each other** on the same subject. When
|
|
192
|
+
a later decision silently overrode an earlier one, the earlier is the finding:
|
|
193
|
+
it was never marked superseded.
|
|
194
|
+
|
|
195
|
+
### 2.3 Structure & Status Hygiene
|
|
196
|
+
|
|
197
|
+
Promote the Step 1 sweep's resolved items to findings here. Keep them terse —
|
|
198
|
+
each names the entry and the missing or malformed element.
|
|
199
|
+
|
|
200
|
+
### 2.4 Missing Decision
|
|
201
|
+
|
|
202
|
+
The inverse gap: a directional change that landed with **no** decision
|
|
203
|
+
recorded. **Bound the search by date** — inspect history since the newest
|
|
204
|
+
entry's `Date`, not the whole history, or this dimension dominates the lens's
|
|
205
|
+
cost on any mature repository:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
git log --since=<newest-ADR-date> --pretty='%h %s' -- . | head -50
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Flag only **directional** changes — a mechanism retired, a contract cut over, a
|
|
212
|
+
dependency or platform swapped, an architectural seam moved. Routine features,
|
|
213
|
+
fixes, and refactors are not decisions and must not be reported here. When the
|
|
214
|
+
newest entry is recent and nothing directional landed since, record that as a
|
|
215
|
+
single `Info` observation rather than straining for a finding.
|
|
216
|
+
|
|
217
|
+
### 2.5 Layout Conformance
|
|
218
|
+
|
|
219
|
+
Report the detection outcomes named in **Applicability & layout detection**: an
|
|
220
|
+
unreferenced ADR body under `decisions/`, a `decisions/` directory with no
|
|
221
|
+
entry file, or an index whose rows and directory contents disagree about which
|
|
222
|
+
ADRs exist.
|
|
223
|
+
|
|
224
|
+
## Not-applicable report
|
|
225
|
+
|
|
226
|
+
When the project keeps **no decisions log** (neither the entry file nor the
|
|
227
|
+
directory exists, and no `--paths` / `--dir` override was supplied), emit this
|
|
228
|
+
explicit report instead of empty findings — and stop:
|
|
229
|
+
|
|
230
|
+
```text
|
|
231
|
+
# Decisions-Log (ADR) Audit Report
|
|
232
|
+
|
|
233
|
+
## Executive Summary
|
|
234
|
+
|
|
235
|
+
**Not applicable** — this project keeps no decisions log (no `decisions.md`
|
|
236
|
+
entry file and no `decisions/` directory under the configured docs root), so
|
|
237
|
+
the ADR lens has nothing to inspect and was skipped.
|
|
238
|
+
|
|
239
|
+
## Detailed Findings
|
|
240
|
+
|
|
241
|
+
_None — lens not applicable._
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Constraint (lens-specific carve-out)
|
|
245
|
+
|
|
246
|
+
Read-only over the decisions log and the tree it makes claims about. The single
|
|
247
|
+
write is the report artifact: never edit, supersede, renumber, reformat, or
|
|
248
|
+
delete an ADR — recording that a decision needs superseding is the deliverable,
|
|
249
|
+
and performing the supersession is a separate, human-owned pass. Run
|
|
250
|
+
`check-doc-links.js` in its default read-only mode only. Generic documentation
|
|
251
|
+
staleness outside the decisions surface belongs to
|
|
252
|
+
[`audit-documentation`](audit-documentation.md); architectural boundary
|
|
253
|
+
violations belong to `audit-architecture`. This lens judges whether a *recorded
|
|
254
|
+
decision* still matches the tree — never whether the decision was a good one.
|
|
255
|
+
|
|
256
|
+
## Report additions
|
|
257
|
+
|
|
258
|
+
Beyond the shared skeleton (Executive Summary + Detailed Findings from the
|
|
259
|
+
core), this lens's report carries its own title and a Decision Coverage table,
|
|
260
|
+
so a reader can see what was claim-checked versus what was only chain-checked:
|
|
261
|
+
|
|
262
|
+
```markdown
|
|
263
|
+
# Decisions-Log (ADR) Audit Report
|
|
264
|
+
|
|
265
|
+
## Decision Coverage
|
|
266
|
+
|
|
267
|
+
| ADR | Status | Checked |
|
|
268
|
+
| ----------- | ----------------------------------------------- | ----------------------------- |
|
|
269
|
+
| [id, title] | [Accepted · Superseded · Deprecated · Reverted] | [Claims + chain · Chain only] |
|
|
270
|
+
```
|
|
@@ -38,9 +38,9 @@ union of:
|
|
|
38
38
|
key for it.
|
|
39
39
|
|
|
40
40
|
**Generated docs are excluded from per-doc semantic review.** The output of
|
|
41
|
-
`generate-config-docs.js
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
`generate-config-docs.js` and `generate-workflows-doc.js`, and the synced
|
|
42
|
+
`.claude/commands/` mirrors, are generator-owned: hand-editing them is never
|
|
43
|
+
the remediation. Instead, Step 1
|
|
44
44
|
runs the generators' `--check` mode and emits a **single** "generator output
|
|
45
45
|
dirty" finding when their output is stale — the remediation is "rerun the
|
|
46
46
|
generator", not "edit the doc". Auto-generated changelog files
|
|
@@ -79,7 +79,6 @@ are cheap, exact, and de-duplicate the easy findings:
|
|
|
79
79
|
```bash
|
|
80
80
|
node .agents/scripts/check-doc-links.js
|
|
81
81
|
node .agents/scripts/generate-config-docs.js --check
|
|
82
|
-
node .agents/scripts/generate-lifecycle-docs.js --check
|
|
83
82
|
node .agents/scripts/generate-workflows-doc.js --check
|
|
84
83
|
node .agents/scripts/resolve-doc-tiers.js --json
|
|
85
84
|
```
|
|
@@ -87,8 +86,8 @@ node .agents/scripts/resolve-doc-tiers.js --json
|
|
|
87
86
|
Fold the results in as findings:
|
|
88
87
|
|
|
89
88
|
- **Checker failures** (broken links, generator drift) become individual
|
|
90
|
-
findings with `Category: Link Integrity
|
|
91
|
-
|
|
89
|
+
findings with `Category: Link Integrity`, citing the checker output
|
|
90
|
+
verbatim.
|
|
92
91
|
- **Generator dirtiness** (any `--check` reporting stale output, including
|
|
93
92
|
a stale `.claude/commands/` mirror) becomes **one single finding** with
|
|
94
93
|
`Category: Generator Drift` — never per-line findings — whose
|
|
@@ -211,6 +210,23 @@ findings land as actionable, tracked work rather than a report nobody reads.
|
|
|
211
210
|
Run the deterministic checkers in `--check` mode only; the single write is the
|
|
212
211
|
report artifact. Do not edit any documentation or code.
|
|
213
212
|
|
|
213
|
+
### Boundary with the ADR lens
|
|
214
|
+
|
|
215
|
+
**Decision-log semantics belong to [`audit-adrs`](audit-adrs.md)**, not to this
|
|
216
|
+
lens: whether an `Accepted` ADR's claims still match the tree, whether its
|
|
217
|
+
supersede chain resolves, whether the index and the entry bodies agree on a
|
|
218
|
+
status, and whether a directional change landed with no decision recorded. That
|
|
219
|
+
lens reads the whole decisions log as a graph; this one would only ever see the
|
|
220
|
+
decisions file as one more prose doc.
|
|
221
|
+
|
|
222
|
+
This lens keeps its **generic** coverage of that file — link integrity, command
|
|
223
|
+
and path claims, and the History Bloat / Contradiction / Authority Drift
|
|
224
|
+
categories above — and both lenses may legitimately touch `decisions.md`. When
|
|
225
|
+
a finding turns on an ADR's **status, chain, or decided contract**, leave it to
|
|
226
|
+
`audit-adrs` rather than reporting it here, so the two lenses do not
|
|
227
|
+
double-report the same defect. The History Bloat remediation is unchanged and
|
|
228
|
+
still applies: never prune an ADR by archiving — supersede it in place.
|
|
229
|
+
|
|
214
230
|
## Report additions
|
|
215
231
|
|
|
216
232
|
Beyond the shared skeleton (Executive Summary + Detailed Findings from the
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,39 @@ 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.36.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.35.0...mandrel-v2.36.0) (2026-08-29)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### ⚠ BREAKING CHANGES
|
|
22
|
+
|
|
23
|
+
* `.agents/scripts/generate-lifecycle-docs.js` is removed from the published payload, and the framework no longer requires a consumer `docs/LIFECYCLE.md`. A consumer that created that file only to stop the generator aborting can delete it as residue — nothing else reads it. A consumer invoking the generator by hand, or chaining it in its own `docs:gen` / `docs:check`, should drop the call. The lifecycle schemas under `.agents/schemas/lifecycle/` still ship unchanged and the emit path is untouched.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
* **check-doc-links:** percent-decode relative link targets before the existence check (refs [#5090](https://github.com/dsj1984/mandrel/issues/5090)) ([#5092](https://github.com/dsj1984/mandrel/issues/5092)) ([024afff](https://github.com/dsj1984/mandrel/commit/024afff5aa000ace8e4f8ee2df6bf246acd0ae40))
|
|
28
|
+
* gate auto-merge arming and the merge wait on head-anchored advisory check conclusions ([#5096](https://github.com/dsj1984/mandrel/issues/5096)) ([#5097](https://github.com/dsj1984/mandrel/issues/5097)) ([a6ae194](https://github.com/dsj1984/mandrel/commit/a6ae194349951ab8cc459f9a4d21a46785014ffd))
|
|
29
|
+
* **git-cleanup:** branch the merged-tip skip taxonomy on ancestry instead of SHA inequality ([#5086](https://github.com/dsj1984/mandrel/issues/5086)) ([#5087](https://github.com/dsj1984/mandrel/issues/5087)) ([caf41b0](https://github.com/dsj1984/mandrel/commit/caf41b0a973866ba1035c5c10178eb46e7c1b078))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
* stop requiring consumers to carry docs/LIFECYCLE.md — retire the consumer-side lifecycle doc mirror ([#5089](https://github.com/dsj1984/mandrel/issues/5089)) ([#5091](https://github.com/dsj1984/mandrel/issues/5091)) ([ad16df8](https://github.com/dsj1984/mandrel/commit/ad16df881692fc5dd169bc4f0bcf73191a872345))
|
|
35
|
+
|
|
36
|
+
## [2.35.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.34.0...mandrel-v2.35.0) (2026-08-28)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
* add /audit-adrs — global decisions-log audit lens (decision drift, supersede-chain integrity, structure, missing decisions) ([#5072](https://github.com/dsj1984/mandrel/issues/5072)) ([#5073](https://github.com/dsj1984/mandrel/issues/5073)) ([5757e6a](https://github.com/dsj1984/mandrel/commit/5757e6ae3da471713dcdb4ac8b6d8093ea36d6f4))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
* **adrs:** add Reverted to the ADR status vocabulary and pin it to the skill (refs [#5078](https://github.com/dsj1984/mandrel/issues/5078)) ([#5083](https://github.com/dsj1984/mandrel/issues/5083)) ([d498575](https://github.com/dsj1984/mandrel/commit/d49857573725d8241d7083ad4579583b47af4935))
|
|
47
|
+
* **audit-rules:** register a deploy-runtime sensitive-path class (refs [#5069](https://github.com/dsj1984/mandrel/issues/5069)) ([#5070](https://github.com/dsj1984/mandrel/issues/5070)) ([da7f9f3](https://github.com/dsj1984/mandrel/commit/da7f9f3969aa14afa45a89d1ee4973248268a7cd))
|
|
48
|
+
* **findings:** union the fingerprint lookup with the semantic pass in routeFinding (refs [#5079](https://github.com/dsj1984/mandrel/issues/5079)) ([#5082](https://github.com/dsj1984/mandrel/issues/5082)) ([fd82de3](https://github.com/dsj1984/mandrel/commit/fd82de3496b4fe02cf29bead27957ce82ef7a0b2))
|
|
49
|
+
* fix coverage-capture freshness: derive the scorable-source extension set from the CRAP scanner and fail closed when discovery finds nothing ([#5076](https://github.com/dsj1984/mandrel/issues/5076)) ([#5081](https://github.com/dsj1984/mandrel/issues/5081)) ([09f89c0](https://github.com/dsj1984/mandrel/commit/09f89c0bd39adde9936ebd6c47fb4c2a5c3ee4cd))
|
|
50
|
+
|
|
18
51
|
## [2.34.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.33.0...mandrel-v2.34.0) (2026-08-27)
|
|
19
52
|
|
|
20
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mandrel",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.36.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/",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"baselines:prune": "node .agents/scripts/prune-baseline-orphans.js",
|
|
33
33
|
"lint:md": "markdownlint-cli2 \".agents/**/*.md\" \"*.md\" \"!node_modules/**\" \"!.worktrees/**\"",
|
|
34
34
|
"lint": "node .agents/scripts/run-lint.js && npm run docs:check",
|
|
35
|
-
"docs:gen": "node .agents/scripts/generate-config-docs.js && node .agents/scripts/generate-
|
|
36
|
-
"docs:check": "node .agents/scripts/generate-config-docs.js --check && node .agents/scripts/generate-
|
|
35
|
+
"docs:gen": "node .agents/scripts/generate-config-docs.js && node .agents/scripts/generate-workflows-doc.js && node .agents/scripts/generate-lens-checklists.js",
|
|
36
|
+
"docs:check": "node .agents/scripts/generate-config-docs.js --check && node .agents/scripts/generate-workflows-doc.js --check && node .agents/scripts/generate-lens-checklists.js --check && node .agents/scripts/check-doc-links.js && npm run skills:check",
|
|
37
37
|
"skills:index": "node .agents/scripts/generate-skills-index.js",
|
|
38
38
|
"skills:check": "node .agents/scripts/generate-skills-index.js --check",
|
|
39
39
|
"format": "biome format --write .",
|
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* .agents/scripts/generate-lifecycle-docs.js — Schema-backed lifecycle docs table
|
|
4
|
-
*
|
|
5
|
-
* Renders a bounded region inside `docs/LIFECYCLE.md` from every
|
|
6
|
-
* `.agents/schemas/lifecycle/*.schema.json`. The region is delimited by:
|
|
7
|
-
*
|
|
8
|
-
* <!-- BEGIN GENERATED:lifecycle-events -->
|
|
9
|
-
* ...generated table...
|
|
10
|
-
* <!-- END GENERATED:lifecycle-events -->
|
|
11
|
-
*
|
|
12
|
-
* Columns: | Event | Schema | Description | Required fields |
|
|
13
|
-
*
|
|
14
|
-
* - Event = schema filename minus `.schema.json`
|
|
15
|
-
* - Schema = relative markdown link to the schema file
|
|
16
|
-
* - Description = the schema's top-level `description` property
|
|
17
|
-
* - Required fields = comma-joined list from the schema's `required` array
|
|
18
|
-
* (rendered as inline code; "—" when empty/absent)
|
|
19
|
-
*
|
|
20
|
-
* Skips `README.md`. The `ledger-record.schema.json` file is a record
|
|
21
|
-
* envelope rather than a lifecycle event, but to keep this generator
|
|
22
|
-
* literal-schema driven we still emit a row for it — the surrounding doc
|
|
23
|
-
* already calls out the distinction.
|
|
24
|
-
*
|
|
25
|
-
* Modes:
|
|
26
|
-
* (default) — rewrites the bounded region in place.
|
|
27
|
-
* --check — exits 0 when the on-disk region matches the freshly
|
|
28
|
-
* generated content, exits 1 with a diff hint otherwise.
|
|
29
|
-
*
|
|
30
|
-
* Per `.agents/rules/orchestration-error-handling.md`, unrecoverable
|
|
31
|
-
* failures surface via `throw new Error(...)` so `runAsCli` can map the
|
|
32
|
-
* throw to `process.exit(1)` deterministically (no `Logger.fatal`).
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import fs from 'node:fs';
|
|
36
|
-
import path from 'node:path';
|
|
37
|
-
import { fileURLToPath } from 'node:url';
|
|
38
|
-
import { parseArgs } from 'node:util';
|
|
39
|
-
import { runAsCli } from './lib/cli-utils.js';
|
|
40
|
-
import { Logger } from './lib/Logger.js';
|
|
41
|
-
|
|
42
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
43
|
-
const __dirname = path.dirname(__filename);
|
|
44
|
-
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
|
|
45
|
-
const SCHEMA_DIR = path.join(PROJECT_ROOT, '.agents', 'schemas', 'lifecycle');
|
|
46
|
-
const DOC_PATH = path.join(PROJECT_ROOT, 'docs', 'LIFECYCLE.md');
|
|
47
|
-
const REGION_BEGIN = '<!-- BEGIN GENERATED:lifecycle-events -->';
|
|
48
|
-
const REGION_END = '<!-- END GENERATED:lifecycle-events -->';
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Read and parse every `*.schema.json` under the lifecycle schema dir, in
|
|
52
|
-
* ASCII-sorted filename order. The sort is intentional — it produces a
|
|
53
|
-
* stable diff regardless of the host filesystem's enumeration order, which
|
|
54
|
-
* is what makes `--check` reliable across platforms.
|
|
55
|
-
*
|
|
56
|
-
* @param {string} dir Absolute path to the schema directory.
|
|
57
|
-
* @returns {Array<{event:string, file:string, description:string, required:string[]}>}
|
|
58
|
-
*/
|
|
59
|
-
function readLifecycleSchemas(dir) {
|
|
60
|
-
if (!fs.existsSync(dir)) {
|
|
61
|
-
throw new Error(`Lifecycle schema directory not found: ${dir}`);
|
|
62
|
-
}
|
|
63
|
-
const entries = fs
|
|
64
|
-
.readdirSync(dir)
|
|
65
|
-
.filter((name) => name.endsWith('.schema.json'))
|
|
66
|
-
.sort();
|
|
67
|
-
if (entries.length === 0) {
|
|
68
|
-
throw new Error(`No *.schema.json files found in ${dir}`);
|
|
69
|
-
}
|
|
70
|
-
return entries.map((file) => {
|
|
71
|
-
const abs = path.join(dir, file);
|
|
72
|
-
const raw = fs.readFileSync(abs, 'utf8');
|
|
73
|
-
let json;
|
|
74
|
-
try {
|
|
75
|
-
json = JSON.parse(raw);
|
|
76
|
-
} catch (err) {
|
|
77
|
-
throw new Error(`Failed to parse JSON schema ${file}: ${err.message}`);
|
|
78
|
-
}
|
|
79
|
-
const event = file.replace(/\.schema\.json$/, '');
|
|
80
|
-
const description =
|
|
81
|
-
typeof json.description === 'string' && json.description.trim().length > 0
|
|
82
|
-
? json.description.trim()
|
|
83
|
-
: '';
|
|
84
|
-
const required = Array.isArray(json.required) ? [...json.required] : [];
|
|
85
|
-
return { event, file, description, required };
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Escape pipe characters so they survive Markdown table cell parsing.
|
|
91
|
-
*
|
|
92
|
-
* @param {string} text
|
|
93
|
-
* @returns {string}
|
|
94
|
-
*/
|
|
95
|
-
function escapeCell(text) {
|
|
96
|
-
return text.replace(/\|/g, '\\|');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Render the bounded-region body (the generated content between the two
|
|
101
|
-
* comment markers, exclusive of the markers themselves).
|
|
102
|
-
*
|
|
103
|
-
* @param {ReturnType<typeof readLifecycleSchemas>} rows
|
|
104
|
-
* @returns {string}
|
|
105
|
-
*/
|
|
106
|
-
function renderTable(rows) {
|
|
107
|
-
const header = '| Event | Schema | Description | Required fields |';
|
|
108
|
-
const sep = '| --- | --- | --- | --- |';
|
|
109
|
-
const body = rows.map((row) => {
|
|
110
|
-
const eventCell = `\`${row.event}\``;
|
|
111
|
-
const schemaLink = `[\`${row.file}\`](../.agents/schemas/lifecycle/${row.file})`;
|
|
112
|
-
const description = escapeCell(row.description || '—');
|
|
113
|
-
const requiredCell =
|
|
114
|
-
row.required.length === 0
|
|
115
|
-
? '—'
|
|
116
|
-
: row.required.map((field) => `\`${field}\``).join(', ');
|
|
117
|
-
return `| ${eventCell} | ${schemaLink} | ${description} | ${requiredCell} |`;
|
|
118
|
-
});
|
|
119
|
-
// Surround with blank lines so the markers + table read as a separate block.
|
|
120
|
-
return ['', header, sep, ...body, ''].join('\n');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Substitute the bounded region inside `original`. Throws if either marker
|
|
125
|
-
* is missing, or if `BEGIN` appears after `END`. Idempotent — a re-run on
|
|
126
|
-
* the same input yields byte-identical output.
|
|
127
|
-
*
|
|
128
|
-
* @param {string} original
|
|
129
|
-
* @param {string} body Region body, already including leading/trailing blank
|
|
130
|
-
* lines (as produced by `renderTable`).
|
|
131
|
-
* @returns {string}
|
|
132
|
-
*/
|
|
133
|
-
function spliceRegion(original, body) {
|
|
134
|
-
const beginIdx = original.indexOf(REGION_BEGIN);
|
|
135
|
-
const endIdx = original.indexOf(REGION_END);
|
|
136
|
-
if (beginIdx === -1) {
|
|
137
|
-
throw new Error(
|
|
138
|
-
`Missing region marker "${REGION_BEGIN}" in ${DOC_PATH}. ` +
|
|
139
|
-
'Insert the marker pair before re-running the generator.',
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
if (endIdx === -1) {
|
|
143
|
-
throw new Error(
|
|
144
|
-
`Missing region marker "${REGION_END}" in ${DOC_PATH}. ` +
|
|
145
|
-
'Insert the marker pair before re-running the generator.',
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
if (endIdx < beginIdx) {
|
|
149
|
-
throw new Error(
|
|
150
|
-
`Region markers out of order in ${DOC_PATH}: END appears before BEGIN.`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
const before = original.slice(0, beginIdx + REGION_BEGIN.length);
|
|
154
|
-
const after = original.slice(endIdx);
|
|
155
|
-
return `${before}\n${body}\n${after}`;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Build the canonical post-generation file content for `docs/LIFECYCLE.md`.
|
|
160
|
-
*
|
|
161
|
-
* @param {string} schemaDir
|
|
162
|
-
* @param {string} docPath
|
|
163
|
-
* @returns {{ generated: string, original: string }}
|
|
164
|
-
*/
|
|
165
|
-
function buildExpected(schemaDir, docPath) {
|
|
166
|
-
if (!fs.existsSync(docPath)) {
|
|
167
|
-
throw new Error(`Target doc not found: ${docPath}`);
|
|
168
|
-
}
|
|
169
|
-
const original = fs.readFileSync(docPath, 'utf8');
|
|
170
|
-
const rows = readLifecycleSchemas(schemaDir);
|
|
171
|
-
const body = renderTable(rows);
|
|
172
|
-
const generated = spliceRegion(original, body);
|
|
173
|
-
return { generated, original };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* @param {string[]} argv
|
|
178
|
-
*/
|
|
179
|
-
async function main(argv = process.argv.slice(2)) {
|
|
180
|
-
const { values } = parseArgs({
|
|
181
|
-
args: argv,
|
|
182
|
-
options: {
|
|
183
|
-
check: { type: 'boolean', default: false },
|
|
184
|
-
},
|
|
185
|
-
allowPositionals: false,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
const { generated, original } = buildExpected(SCHEMA_DIR, DOC_PATH);
|
|
189
|
-
|
|
190
|
-
if (values.check) {
|
|
191
|
-
if (generated === original) {
|
|
192
|
-
Logger.info(
|
|
193
|
-
`generate-lifecycle-docs: ${path.relative(PROJECT_ROOT, DOC_PATH)} is up to date.`,
|
|
194
|
-
);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
const hint =
|
|
198
|
-
`${path.relative(PROJECT_ROOT, DOC_PATH)} is out of date. ` +
|
|
199
|
-
'Run `node .agents/scripts/generate-lifecycle-docs.js` to regenerate the bounded region.';
|
|
200
|
-
throw new Error(hint);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (generated === original) {
|
|
204
|
-
Logger.info(
|
|
205
|
-
`generate-lifecycle-docs: ${path.relative(PROJECT_ROOT, DOC_PATH)} already current — no write.`,
|
|
206
|
-
);
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
fs.writeFileSync(DOC_PATH, generated, 'utf8');
|
|
210
|
-
Logger.info(
|
|
211
|
-
`generate-lifecycle-docs: wrote bounded region into ${path.relative(PROJECT_ROOT, DOC_PATH)}.`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export {
|
|
216
|
-
buildExpected,
|
|
217
|
-
REGION_BEGIN,
|
|
218
|
-
REGION_END,
|
|
219
|
-
readLifecycleSchemas,
|
|
220
|
-
renderTable,
|
|
221
|
-
spliceRegion,
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
runAsCli(import.meta.url, main, {
|
|
225
|
-
source: 'generate-lifecycle-docs',
|
|
226
|
-
usage: {
|
|
227
|
-
invocation: 'node .agents/scripts/generate-lifecycle-docs.js [--check]',
|
|
228
|
-
summary:
|
|
229
|
-
'Regenerate the lifecycle-event table in docs/LIFECYCLE.md from the event schemas. Writes only when the generated content differs.',
|
|
230
|
-
flags: [
|
|
231
|
-
[
|
|
232
|
-
'--check',
|
|
233
|
-
'Verify the doc is current and fail if stale; write nothing.',
|
|
234
|
-
],
|
|
235
|
-
],
|
|
236
|
-
},
|
|
237
|
-
});
|