mandrel 1.71.0 → 1.73.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/configuration.md +13 -0
- package/.agents/instructions.md +14 -6
- package/.agents/personas/devops-engineer.md +4 -2
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +430 -0
- package/.agents/scripts/lib/bootstrap/manifest.js +8 -0
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +36 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +35 -5
- package/.agents/scripts/lib/story-body/story-body.js +5 -2
- package/.agents/scripts/lib/story-plan.js +41 -4
- package/.agents/scripts/lint-issue-body.js +261 -0
- package/.agents/skills/core/documentation-and-adrs/SKILL.md +38 -2
- package/.agents/templates/docs/architecture.md +4 -1
- package/.agents/templates/docs/decisions/_template.md +35 -0
- package/.agents/templates/docs/decisions.index.md +49 -0
- package/.agents/templates/docs/decisions.md +11 -0
- package/.agents/workflows/helpers/plan-epic.md +51 -8
- package/.agents/workflows/helpers/plan-story.md +19 -3
- package/.agents/workflows/plan.md +71 -3
- package/docs/CHANGELOG.md +20 -0
- package/package.json +1 -1
|
@@ -227,13 +227,51 @@ export function buildContextEnvelope({
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
230
|
+
* Extract the "Tech Stack" `##` section from a markdown document.
|
|
231
|
+
*
|
|
232
|
+
* Tolerates a numbered / decorated heading (`## 1. Tech Stack`,
|
|
233
|
+
* `## Tech Stack`, etc.) and a section that is the final `##` in the
|
|
234
|
+
* file (the terminator matches the next `##` heading **or** end-of-file).
|
|
235
|
+
* Returns the matched section text (re-headed to a clean `## Tech Stack`)
|
|
236
|
+
* or `null` when no Tech Stack heading is present.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} content
|
|
239
|
+
* @returns {string|null}
|
|
240
|
+
*/
|
|
241
|
+
function extractTechStackSection(content) {
|
|
242
|
+
const match = content.match(
|
|
243
|
+
/^##\s+(?:\d+[.)]\s+)?Tech Stack\s*$([\s\S]*?)(?=^##\s+|(?![\s\S]))/m,
|
|
244
|
+
);
|
|
245
|
+
return match ? `## Tech Stack${match[1]}`.trim() : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Resolve the project's Tech Stack inventory for the host LLM, in order:
|
|
250
|
+
*
|
|
251
|
+
* 1. A dedicated `docs/tech-stack.md` when present (the emerging
|
|
252
|
+
* single-ownership convention — WHAT in tech-stack.md, HOW in
|
|
253
|
+
* architecture.md, WHY in the ADRs). Its full body is returned.
|
|
254
|
+
* 2. Otherwise, the `## Tech Stack` section of `docs/architecture.md`,
|
|
255
|
+
* tolerating a numbered/decorated heading and a final-section
|
|
256
|
+
* heading (no following `##` required).
|
|
257
|
+
*
|
|
258
|
+
* Returns `null` when neither source yields an inventory.
|
|
232
259
|
*
|
|
233
260
|
* @param {string} projectRoot
|
|
234
261
|
* @returns {Promise<string|null>}
|
|
235
262
|
*/
|
|
236
263
|
export async function readTechStackSummary(projectRoot) {
|
|
264
|
+
const dedicatedPath = path.join(projectRoot, 'docs', 'tech-stack.md');
|
|
265
|
+
try {
|
|
266
|
+
const dedicated = await readFile(dedicatedPath, 'utf8');
|
|
267
|
+
const trimmed = dedicated.trim();
|
|
268
|
+
if (trimmed) {
|
|
269
|
+
return trimmed;
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
// No dedicated tech-stack.md — fall through to architecture.md.
|
|
273
|
+
}
|
|
274
|
+
|
|
237
275
|
const archPath = path.join(projectRoot, 'docs', 'architecture.md');
|
|
238
276
|
let content;
|
|
239
277
|
try {
|
|
@@ -241,6 +279,5 @@ export async function readTechStackSummary(projectRoot) {
|
|
|
241
279
|
} catch {
|
|
242
280
|
return null;
|
|
243
281
|
}
|
|
244
|
-
|
|
245
|
-
return match ? `## Tech Stack${match[1]}`.trim() : null;
|
|
282
|
+
return extractTechStackSection(content);
|
|
246
283
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// .agents/scripts/lint-issue-body.js
|
|
3
|
+
/**
|
|
4
|
+
* Issue-body conformance lint (Story #4227).
|
|
5
|
+
*
|
|
6
|
+
* Runs the canonical `story-body.parse()` against a human-opened
|
|
7
|
+
* `type::story` / `type::epic` issue body and reports whether the body
|
|
8
|
+
* round-trips. This is the drift guard between the generated GitHub Issue
|
|
9
|
+
* Forms (`lib/bootstrap/issue-forms-template.js`) and the parser: if a
|
|
10
|
+
* human files a ticket whose body `parse()` rejects (or which lacks the
|
|
11
|
+
* binding `goal` / `acceptance` / `verify` sections), the lint surfaces a
|
|
12
|
+
* **comment** on the issue rather than failing silently — the supported
|
|
13
|
+
* human entry points (`/plan` from an existing Epic ID, the qa-assist →
|
|
14
|
+
* `/plan` handoff) depend on a parseable body.
|
|
15
|
+
*
|
|
16
|
+
* ## Design
|
|
17
|
+
*
|
|
18
|
+
* - `evaluateIssueBody(body)` is **pure** (no I/O): it parses the body and
|
|
19
|
+
* returns a structured conformance verdict. This is the unit-tested core.
|
|
20
|
+
* - The CLI wrapper reads the issue body + labels (via `gh` or env), runs
|
|
21
|
+
* the evaluator, and posts/updates a single marker comment when the body
|
|
22
|
+
* is non-conformant. Network-touching, exercised in CI.
|
|
23
|
+
*
|
|
24
|
+
* GitHub Issue Forms render a skipped optional field as the literal
|
|
25
|
+
* `_No response_`; the evaluator strips that sentinel so an empty optional
|
|
26
|
+
* section does not masquerade as content.
|
|
27
|
+
*
|
|
28
|
+
* @module lint-issue-body
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { spawnSync } from 'node:child_process';
|
|
32
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
33
|
+
import { parse, StoryBodyParseError } from './lib/story-body/story-body.js';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Marker that identifies the lint's own comment so re-runs update rather
|
|
37
|
+
* than duplicate it.
|
|
38
|
+
*/
|
|
39
|
+
export const LINT_COMMENT_MARKER = '<!-- mandrel:issue-body-conformance -->';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The binding sections every conformant ticket body MUST carry. `changes`
|
|
43
|
+
* and `references` are advisory (per the Engineer persona's implementation
|
|
44
|
+
* latitude), so they are not required here.
|
|
45
|
+
*/
|
|
46
|
+
const REQUIRED_SECTIONS = [
|
|
47
|
+
{ field: 'goal', label: 'Goal' },
|
|
48
|
+
{ field: 'acceptance', label: 'Acceptance' },
|
|
49
|
+
{ field: 'verify', label: 'Verify' },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Strip the GitHub Issue Form empty-field sentinel so a skipped optional
|
|
54
|
+
* field is treated as absent rather than literal content.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} body
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
function stripNoResponseSentinel(body) {
|
|
60
|
+
return body
|
|
61
|
+
.split('\n')
|
|
62
|
+
.filter((line) => line.trim() !== '_No response_')
|
|
63
|
+
.join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {object} ConformanceVerdict
|
|
68
|
+
* @property {boolean} conformant - True when the body parses AND carries
|
|
69
|
+
* every required section with non-empty content.
|
|
70
|
+
* @property {string[]} problems - Human-readable problem statements
|
|
71
|
+
* (empty when conformant).
|
|
72
|
+
* @property {string[]} warnings - Non-fatal parser warnings surfaced for
|
|
73
|
+
* transparency (e.g. legacy-path-entry).
|
|
74
|
+
* @property {boolean} parseFailed - True when `parse()` threw (fail-closed).
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Evaluate an issue body for conformance with the canonical Story-body
|
|
79
|
+
* schema. Pure — no I/O. Fail-closed parse errors are caught and reported
|
|
80
|
+
* as a non-conformant verdict (never thrown), because the caller's job is
|
|
81
|
+
* to *comment*, not to crash CI.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} body - Raw issue-body markdown.
|
|
84
|
+
* @returns {ConformanceVerdict}
|
|
85
|
+
*/
|
|
86
|
+
export function evaluateIssueBody(body) {
|
|
87
|
+
if (typeof body !== 'string' || body.trim().length === 0) {
|
|
88
|
+
return {
|
|
89
|
+
conformant: false,
|
|
90
|
+
problems: ['The issue body is empty.'],
|
|
91
|
+
warnings: [],
|
|
92
|
+
parseFailed: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const cleaned = stripNoResponseSentinel(body);
|
|
97
|
+
|
|
98
|
+
let result;
|
|
99
|
+
try {
|
|
100
|
+
result = parse(cleaned);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err instanceof StoryBodyParseError) {
|
|
103
|
+
return {
|
|
104
|
+
conformant: false,
|
|
105
|
+
problems: [
|
|
106
|
+
`The body could not be parsed into the canonical schema: ${err.message}`,
|
|
107
|
+
],
|
|
108
|
+
warnings: [],
|
|
109
|
+
parseFailed: true,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const problems = [];
|
|
116
|
+
|
|
117
|
+
// A legacy string body parses but carries no structured sections — that
|
|
118
|
+
// is exactly the human-filed shape this lint exists to catch.
|
|
119
|
+
if (result.info.isLegacyStringBody) {
|
|
120
|
+
problems.push(
|
|
121
|
+
'The body has no recognised `## Goal` / `## Acceptance` / `## Verify` sections. ' +
|
|
122
|
+
'File via the Story/Epic issue form so it round-trips through the parser.',
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const { field, label } of REQUIRED_SECTIONS) {
|
|
127
|
+
const value = result.body[field];
|
|
128
|
+
const empty =
|
|
129
|
+
value == null ||
|
|
130
|
+
(typeof value === 'string' && value.trim().length === 0) ||
|
|
131
|
+
(Array.isArray(value) && value.length === 0);
|
|
132
|
+
if (empty) {
|
|
133
|
+
problems.push(
|
|
134
|
+
`The required \`## ${label}\` section is missing or empty.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
conformant: problems.length === 0,
|
|
141
|
+
problems,
|
|
142
|
+
warnings: result.warnings,
|
|
143
|
+
parseFailed: false,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Render the markdown comment body the lint posts on a non-conformant
|
|
149
|
+
* issue. Carries {@link LINT_COMMENT_MARKER} so re-runs update in place.
|
|
150
|
+
*
|
|
151
|
+
* @param {ConformanceVerdict} verdict
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
export function renderConformanceComment(verdict) {
|
|
155
|
+
const lines = [
|
|
156
|
+
LINT_COMMENT_MARKER,
|
|
157
|
+
'### ⚠️ Ticket body does not round-trip through the Mandrel parser',
|
|
158
|
+
'',
|
|
159
|
+
'Agents build ticket bodies from a canonical schema that this body does ' +
|
|
160
|
+
'not match, so the supported human entry points (e.g. `/plan` from an ' +
|
|
161
|
+
'existing Epic ID) will reject it. Please fix the following:',
|
|
162
|
+
'',
|
|
163
|
+
...verdict.problems.map((p) => `- ${p}`),
|
|
164
|
+
'',
|
|
165
|
+
'The quickest fix is to refile using the **Story** or **Epic** issue form ' +
|
|
166
|
+
'(New issue → pick the template), which lays out the required sections.',
|
|
167
|
+
];
|
|
168
|
+
if (verdict.warnings.length > 0) {
|
|
169
|
+
lines.push(
|
|
170
|
+
'',
|
|
171
|
+
'<details><summary>Parser warnings (non-blocking)</summary>',
|
|
172
|
+
'',
|
|
173
|
+
...verdict.warnings.map((w) => `- ${w}`),
|
|
174
|
+
'',
|
|
175
|
+
'</details>',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return lines.join('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Thin `gh` wrapper. Returns the trimmed stdout, throwing on a non-zero
|
|
183
|
+
* exit so the CLI surfaces the failure (orchestration-error-handling rule).
|
|
184
|
+
*
|
|
185
|
+
* @param {string[]} args
|
|
186
|
+
* @returns {string}
|
|
187
|
+
*/
|
|
188
|
+
function gh(args) {
|
|
189
|
+
const res = spawnSync('gh', args, { encoding: 'utf8' });
|
|
190
|
+
if (res.status !== 0) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`gh ${args.join(' ')} failed (exit ${res.status}): ${res.stderr?.trim() ?? ''}`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return (res.stdout ?? '').trim();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* CLI entry. Reads the target issue (number from `--issue` or the
|
|
200
|
+
* `ISSUE_NUMBER` env), fetches its body + labels, and — when the body is
|
|
201
|
+
* non-conformant — upserts a single marker comment. Always exits 0 (the
|
|
202
|
+
* lint *informs*, it does not block), unless an unexpected I/O error occurs.
|
|
203
|
+
*
|
|
204
|
+
* Flags:
|
|
205
|
+
* --issue <n> Issue number (defaults to env ISSUE_NUMBER).
|
|
206
|
+
* --repo <o/r> owner/repo (defaults to env GITHUB_REPOSITORY).
|
|
207
|
+
* --dry-run Evaluate + print the verdict, never touch GitHub.
|
|
208
|
+
*/
|
|
209
|
+
async function main() {
|
|
210
|
+
const args = process.argv.slice(2);
|
|
211
|
+
const get = (flag) => {
|
|
212
|
+
const i = args.indexOf(flag);
|
|
213
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
214
|
+
};
|
|
215
|
+
const dryRun = args.includes('--dry-run');
|
|
216
|
+
const issue = get('--issue') ?? process.env.ISSUE_NUMBER;
|
|
217
|
+
const repo = get('--repo') ?? process.env.GITHUB_REPOSITORY;
|
|
218
|
+
|
|
219
|
+
if (!issue) {
|
|
220
|
+
throw new Error('lint-issue-body: --issue <n> or ISSUE_NUMBER is required');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const repoArgs = repo ? ['--repo', repo] : [];
|
|
224
|
+
const raw = gh([
|
|
225
|
+
'issue',
|
|
226
|
+
'view',
|
|
227
|
+
String(issue),
|
|
228
|
+
...repoArgs,
|
|
229
|
+
'--json',
|
|
230
|
+
'body,labels',
|
|
231
|
+
]);
|
|
232
|
+
const { body, labels } = JSON.parse(raw);
|
|
233
|
+
const labelNames = (labels ?? []).map((l) => l.name);
|
|
234
|
+
const isTicket =
|
|
235
|
+
labelNames.includes('type::story') || labelNames.includes('type::epic');
|
|
236
|
+
|
|
237
|
+
if (!isTicket) {
|
|
238
|
+
// Machine-parsable JSON envelope → process.stdout.write (not console.log),
|
|
239
|
+
// per the .agents/scripts logging contract (tests/enforcement/no-console).
|
|
240
|
+
process.stdout.write(
|
|
241
|
+
`${JSON.stringify({ issue: Number(issue), skipped: 'not-a-ticket' })}\n`,
|
|
242
|
+
);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const verdict = evaluateIssueBody(body ?? '');
|
|
247
|
+
process.stdout.write(
|
|
248
|
+
`${JSON.stringify({
|
|
249
|
+
issue: Number(issue),
|
|
250
|
+
conformant: verdict.conformant,
|
|
251
|
+
problems: verdict.problems,
|
|
252
|
+
})}\n`,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (verdict.conformant || dryRun) return;
|
|
256
|
+
|
|
257
|
+
const comment = renderConformanceComment(verdict);
|
|
258
|
+
gh(['issue', 'comment', String(issue), ...repoArgs, '--body', comment]);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
runAsCli(import.meta.url, main, { source: 'lint-issue-body' });
|
|
@@ -12,7 +12,7 @@ description:
|
|
|
12
12
|
|
|
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](#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
16
|
- Mark an ADR's status as `Accepted`, `Superseded by ADR-XXX`, or `Deprecated`. 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.
|
|
@@ -54,9 +54,41 @@ highest-value documentation you can write.
|
|
|
54
54
|
- Choosing between build tools, hosting platforms, or infrastructure
|
|
55
55
|
- Any decision that would be expensive to reverse
|
|
56
56
|
|
|
57
|
+
### Decisions-log layouts
|
|
58
|
+
|
|
59
|
+
Mandrel ships **two supported layouts** for the decisions log. Both keep the
|
|
60
|
+
mandatory-read file named `docs/decisions.md` (the `project.docsContextFiles`
|
|
61
|
+
default), so `config-resolver.js` and every `.agents/` reference resolve the
|
|
62
|
+
same regardless of which you pick — only the **shape** differs. Choose one at
|
|
63
|
+
onboarding:
|
|
64
|
+
|
|
65
|
+
| Layout | Shape | Template(s) | When to use |
|
|
66
|
+
| ----------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
67
|
+
| **Single-file dated entries** (default) | One `decisions.md` of append-only `## YYYY-MM-DD — title` entries | [`templates/docs/decisions.md`](../../../templates/docs/decisions.md) | Small projects; a handful of decisions; you want everything in one scannable file. |
|
|
68
|
+
| **Index + `decisions/` directory** | `decisions.md` is a one-row-per-ADR **index**; each ADR is `decisions/NNNN-*.md` | [`templates/docs/decisions.index.md`](../../../templates/docs/decisions.index.md) + [`templates/docs/decisions/_template.md`](../../../templates/docs/decisions/_template.md) | The log has outgrown a single file (dozens of ADRs); you want per-decision history and `git blame` per ADR. |
|
|
69
|
+
|
|
70
|
+
To adopt the directory layout, replace `decisions.md` with the index variant,
|
|
71
|
+
create a `decisions/` directory beside it, and scaffold each ADR from
|
|
72
|
+
`decisions/_template.md` using zero-padded sequential numbering
|
|
73
|
+
(`0001-*.md`, `0002-*.md`, …).
|
|
74
|
+
|
|
75
|
+
> **Loading model (resolved design question).** The decisions **index** is the
|
|
76
|
+
> only artifact loaded into mandatory task context — individual ADR bodies
|
|
77
|
+
> under `decisions/` are **lazy / link-followed**, not auto-loaded. This is
|
|
78
|
+
> **index-only by default**: auto-loading every ADR body into each task's
|
|
79
|
+
> context would reintroduce exactly the bloat the split exists to remove.
|
|
80
|
+
> `project.docsContextFiles` entries are plain filenames resolved against the
|
|
81
|
+
> docs root (no glob expansion in the loader), so the index ships as a normal
|
|
82
|
+
> mandatory-read with no loader change. A project that genuinely wants the full
|
|
83
|
+
> ADR set in mandatory context can add explicit per-file entries (or a
|
|
84
|
+
> `decisions/*.md`-style entry if it maintains its own globbing) as a
|
|
85
|
+
> deliberate opt-in, but that is the exception, not the default.
|
|
86
|
+
|
|
57
87
|
### ADR Template
|
|
58
88
|
|
|
59
|
-
|
|
89
|
+
In the **single-file** layout, append a short dated entry per the
|
|
90
|
+
`templates/docs/decisions.md` format. In the **directory** layout, store ADRs
|
|
91
|
+
in `docs/decisions/` with sequential numbering:
|
|
60
92
|
|
|
61
93
|
```markdown
|
|
62
94
|
# ADR-001: Use PostgreSQL for primary database
|
|
@@ -69,6 +101,10 @@ Accepted | Superseded by ADR-XXX | Deprecated
|
|
|
69
101
|
|
|
70
102
|
2025-01-15
|
|
71
103
|
|
|
104
|
+
## Deciders
|
|
105
|
+
|
|
106
|
+
The platform team (architect + two senior engineers).
|
|
107
|
+
|
|
72
108
|
## Context
|
|
73
109
|
|
|
74
110
|
We need a primary database for the task management application. Key
|
|
@@ -27,4 +27,7 @@ Describe the top-level directories and their responsibilities.
|
|
|
27
27
|
|
|
28
28
|
## Key Decisions
|
|
29
29
|
|
|
30
|
-
Link to `decisions.md` for the architectural decision log.
|
|
30
|
+
Link to `decisions.md` for the architectural decision log. Mandrel supports two
|
|
31
|
+
first-class layouts for it: a single-file dated-entry `decisions.md` (default)
|
|
32
|
+
or an index + `decisions/` ADR directory — see
|
|
33
|
+
[`.agents/skills/core/documentation-and-adrs/SKILL.md`](../../skills/core/documentation-and-adrs/SKILL.md).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# ADR-NNNN: <short decision title>
|
|
2
|
+
|
|
3
|
+
> Copy this file to `decisions/NNNN-<kebab-title>.md` (zero-padded, sequential
|
|
4
|
+
> — `0001`, `0002`, …) and add a matching row to the `decisions.md` index.
|
|
5
|
+
> ADRs are **append-only**: never rewrite or delete an accepted ADR — supersede
|
|
6
|
+
> it with a new one and flip this one's **Status** to `Superseded by ADR-NNNN`.
|
|
7
|
+
|
|
8
|
+
## Status
|
|
9
|
+
|
|
10
|
+
Accepted
|
|
11
|
+
|
|
12
|
+
<!-- One of: Proposed | Accepted | Superseded by ADR-NNNN | Deprecated -->
|
|
13
|
+
|
|
14
|
+
## Date
|
|
15
|
+
|
|
16
|
+
YYYY-MM-DD
|
|
17
|
+
|
|
18
|
+
## Deciders
|
|
19
|
+
|
|
20
|
+
<!-- Who made the call (names / roles / "the team"). -->
|
|
21
|
+
|
|
22
|
+
## Context
|
|
23
|
+
|
|
24
|
+
<!-- What forced the decision: the constraint, problem, or trade-off. What
|
|
25
|
+
were the requirements and the forces in tension? -->
|
|
26
|
+
|
|
27
|
+
## Decision
|
|
28
|
+
|
|
29
|
+
<!-- What was chosen, stated plainly. -->
|
|
30
|
+
|
|
31
|
+
## Consequences
|
|
32
|
+
|
|
33
|
+
<!-- What this enables and what it costs going forward — positive and negative.
|
|
34
|
+
Include follow-on work, new constraints, and anything a future reader must
|
|
35
|
+
know before reversing this. -->
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Architectural Decisions Log (Index)
|
|
2
|
+
|
|
3
|
+
> **Directory-layout variant.** This is the MADR-style **index + `decisions/`
|
|
4
|
+
> directory** alternative to the default single-file dated-entry
|
|
5
|
+
> [`decisions.md`](decisions.md). To adopt it, replace your `decisions.md` with
|
|
6
|
+
> this index, create a `decisions/` directory next to it, and scaffold ADRs
|
|
7
|
+
> from [`decisions/_template.md`](decisions/_template.md). This file stays named
|
|
8
|
+
> `decisions.md` so it remains the `project.docsContextFiles` mandatory-read
|
|
9
|
+
> every `.agents/` reference and `config-resolver.js` already point at — only
|
|
10
|
+
> its **shape** changes from dated entries to an index.
|
|
11
|
+
>
|
|
12
|
+
> Prefer this layout once the single-file log grows past a few dozen entries
|
|
13
|
+
> (athportal hit ~1060 lines / 32 ADRs before splitting). For small projects,
|
|
14
|
+
> keep the default single-file template instead.
|
|
15
|
+
|
|
16
|
+
## How this layout works
|
|
17
|
+
|
|
18
|
+
- **This file is the index** — one row per ADR, newest at the top. It is the
|
|
19
|
+
mandatory-read; agents scan the index and follow the link into a specific
|
|
20
|
+
ADR only when the detail is relevant (index-only by default — see
|
|
21
|
+
[Loading model](#loading-model)).
|
|
22
|
+
- **Each ADR is its own file** under `decisions/`, named
|
|
23
|
+
`NNNN-<kebab-title>.md` with a zero-padded sequential number.
|
|
24
|
+
- **ADRs are append-only.** Never rewrite or delete an accepted ADR — write a
|
|
25
|
+
new one and flip the old one's status to `Superseded by ADR-NNNN`.
|
|
26
|
+
- **Scaffold new ADRs** from [`decisions/_template.md`](decisions/_template.md)
|
|
27
|
+
(Status / Date / Deciders / Context / Decision / Consequences).
|
|
28
|
+
|
|
29
|
+
## Loading model
|
|
30
|
+
|
|
31
|
+
This index is the only decisions artifact loaded into mandatory task context
|
|
32
|
+
(`project.docsContextFiles`). Individual ADR bodies under `decisions/` are
|
|
33
|
+
**lazy / link-followed**, not auto-loaded — that is the whole point of the
|
|
34
|
+
split: keep the per-task context lean while preserving the full decision
|
|
35
|
+
history on disk. If a project genuinely wants the entire ADR set in mandatory
|
|
36
|
+
context, it can add an explicit `decisions/*.md`-style entry to
|
|
37
|
+
`project.docsContextFiles` as an opt-in (see the configuration reference), but
|
|
38
|
+
index-only is the intended default.
|
|
39
|
+
|
|
40
|
+
## Index
|
|
41
|
+
|
|
42
|
+
| ADR | Title | Status | Date |
|
|
43
|
+
| -------- | ---------------------------------------- | -------- | ---------- |
|
|
44
|
+
| ADR-0001 | _Example — replace with your first ADR_ | Proposed | YYYY-MM-DD |
|
|
45
|
+
|
|
46
|
+
_Add new rows above this line, newest first. Once you scaffold a real ADR from
|
|
47
|
+
[`decisions/_template.md`](decisions/_template.md) into
|
|
48
|
+
`decisions/0001-<title>.md`, link the ADR id to that file (e.g.
|
|
49
|
+
`[ADR-0001](decisions/0001-<title>.md)`) and delete this example row._
|
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
> decisions here as dated entries. This file is one of the
|
|
5
5
|
> `project.docsContextFiles` mandatory-reads — agents consult it before every
|
|
6
6
|
> task to avoid re-litigating settled choices.
|
|
7
|
+
>
|
|
8
|
+
> **Two supported layouts — pick one at onboarding.** This single-file
|
|
9
|
+
> dated-entry layout is the **default**, ideal for small projects. Once the
|
|
10
|
+
> log grows past a few dozen entries it becomes a context-bloat liability;
|
|
11
|
+
> at that point switch to the first-class **index + `decisions/` directory**
|
|
12
|
+
> variant ([`decisions.index.md`](decisions.index.md) + the ADR scaffold at
|
|
13
|
+
> [`decisions/_template.md`](decisions/_template.md)). Both layouts keep the
|
|
14
|
+
> file named `decisions.md` so the `project.docsContextFiles` mandatory-read
|
|
15
|
+
> resolves the same; only the shape differs. See
|
|
16
|
+
> [`.agents/skills/core/documentation-and-adrs/SKILL.md`](../../skills/core/documentation-and-adrs/SKILL.md)
|
|
17
|
+
> for when to choose which.
|
|
7
18
|
|
|
8
19
|
## Format
|
|
9
20
|
|
|
@@ -74,14 +74,22 @@ Phase 5 (Re-Plan Detection).
|
|
|
74
74
|
one-pager feeds the scope-triage gate below, whose verdict folds into
|
|
75
75
|
the **same** Phase 1 HITL confirmation. Do not stop twice.
|
|
76
76
|
|
|
77
|
-
3. **HITL stop — confirm the sharpened one-pager
|
|
78
|
-
to the operator and **STOP**. Do not proceed to Phase 2
|
|
79
|
-
user explicitly confirms the direction. This is the same gate
|
|
80
|
-
skill's own Phase 3 enforces; surfacing it here makes the wait
|
|
77
|
+
3. **HITL stop — confirm the sharpened one-pager** (**gate #1**): Display
|
|
78
|
+
the one-pager to the operator and **STOP**. Do not proceed to Phase 2
|
|
79
|
+
until the user explicitly confirms the direction. This is the same gate
|
|
80
|
+
the skill's own Phase 3 enforces; surfacing it here makes the wait
|
|
81
81
|
contract visible to `/plan` callers. When the Phase 1.5 verdict is
|
|
82
82
|
`story` or `borderline`, this stop carries the three-way choice the
|
|
83
83
|
triage gate defines (below) instead of a plain confirm.
|
|
84
84
|
|
|
85
|
+
> **`--yes` (headless) auto-proceed.** When `/plan` was invoked with
|
|
86
|
+
> `--yes`, this gate does **not** STOP: the one-pager confirm resolves as
|
|
87
|
+
> **approved** and the run continues to Phase 2. A `story` / `borderline`
|
|
88
|
+
> triage verdict resolves to its **Recommended** branch (below) rather
|
|
89
|
+
> than prompting the three-way choice. Display the one-pager and the
|
|
90
|
+
> verdict line for the record, then proceed without waiting. See
|
|
91
|
+
> [`plan.md` § Headless / non-interactive mode](../plan.md#headless--non-interactive-mode---yes).
|
|
92
|
+
|
|
85
93
|
## Phase 1.5: Scope Triage (ideation path only)
|
|
86
94
|
|
|
87
95
|
This phase runs **only** on the ideation path, immediately after Phase 1
|
|
@@ -124,6 +132,15 @@ skill states once).
|
|
|
124
132
|
avoid the ceremony tax of pushing a story-sized scope through the full Epic
|
|
125
133
|
pipeline.
|
|
126
134
|
|
|
135
|
+
> **`--yes` (headless) exception.** "Never auto-route" is the interactive
|
|
136
|
+
> contract. Under `--yes` the operator has *pre-authorized* the
|
|
137
|
+
> recommendation: the three-way choice resolves to its **Recommended**
|
|
138
|
+
> branch deterministically — `single Story` hands off to
|
|
139
|
+
> `/plan --from-notes <path>` (carrying `--yes` so the receiving story
|
|
140
|
+
> path also auto-proceeds), and an `epic` verdict simply continues to
|
|
141
|
+
> Phase 2. No operator wait. This is the only sanctioned auto-route, and it
|
|
142
|
+
> exists solely to make `/plan` driveable headlessly.
|
|
143
|
+
|
|
127
144
|
## Phase 2: Cross-Epic Duplicate Search
|
|
128
145
|
|
|
129
146
|
Runs immediately after Phase 1 (and only on the s-plan-ideation path).
|
|
@@ -396,6 +413,19 @@ for the scoring logic.
|
|
|
396
413
|
permission") is honored — no `gh issue edit` call until the
|
|
397
414
|
operator confirms.
|
|
398
415
|
|
|
416
|
+
> **`--yes` (headless) auto-proceed.** This refinement-diff confirm is the
|
|
417
|
+
> clarity-gate face of `/plan`'s **gate #1** on the existing-Epic
|
|
418
|
+
> (`/plan <epicId>`) path — it is an operator *wait*, not a deterministic
|
|
419
|
+
> validator (the deterministic half is the section-presence *scoring* in
|
|
420
|
+
> step 1, which always runs). When `/plan` was invoked with `--yes`, this
|
|
421
|
+
> confirm does **not** STOP: the sharpened body is auto-**approved** and the
|
|
422
|
+
> run proceeds to step 6 (persist). The blast-radius note is still displayed
|
|
423
|
+
> for the record; only the operator wait is suppressed. This keeps
|
|
424
|
+
> `/plan <epicId> --yes` driveable headlessly even when the Epic body needs
|
|
425
|
+
> refinement (`gh issue edit` still runs only via the step 6 persist call,
|
|
426
|
+
> which the auto-approval authorizes). See
|
|
427
|
+
> [`plan.md` § Headless / non-interactive mode](../plan.md#headless--non-interactive-mode---yes).
|
|
428
|
+
|
|
399
429
|
6. **Persist**: On approval, run the persist mode:
|
|
400
430
|
|
|
401
431
|
```bash
|
|
@@ -556,18 +586,31 @@ for the scoring logic.
|
|
|
556
586
|
`planningRisk.requiresReview` unless the operator passed
|
|
557
587
|
`--force-review`:
|
|
558
588
|
- **High risk** (`requiresReview === true`) or **operator override**
|
|
559
|
-
(`--force-review`)
|
|
560
|
-
PRD, Tech Spec, and Acceptance Spec on GitHub. Approval is
|
|
561
|
-
user's verbal OK in this session — the three context tickets stay
|
|
589
|
+
(`--force-review`) — **gate #2**: **STOP**. Ask the USER to review the
|
|
590
|
+
generated PRD, Tech Spec, and Acceptance Spec on GitHub. Approval is
|
|
591
|
+
the user's verbal OK in this session — the three context tickets stay
|
|
562
592
|
**open** through delivery and are closed automatically by
|
|
563
593
|
`/deliver` when the Epic PR opens. Do NOT proceed
|
|
564
594
|
to decomposition until the user confirms the plan is accurate.
|
|
595
|
+
|
|
596
|
+
> **`--yes` (headless) auto-proceed.** When `/plan` was invoked with
|
|
597
|
+
> `--yes`, this review gate does **not** STOP, even when
|
|
598
|
+
> `requiresReview === true` or `--force-review` was also passed: the
|
|
599
|
+
> review resolves as **approved** and the run **continues directly to
|
|
600
|
+
> Phase 8**, exactly as the low-risk auto-proceed branch below. The
|
|
601
|
+
> three context tickets stay **open** through delivery as usual; only
|
|
602
|
+
> the operator *wait* is suppressed. This is `/plan`'s **gate #2** —
|
|
603
|
+
> the second and last HITL STOP `--yes` suppresses. `--yes` does
|
|
604
|
+
> **not** alter risk routing or the review criteria themselves; it
|
|
605
|
+
> only forces a proceed where this gate would otherwise STOP. See
|
|
606
|
+
> [`plan.md` § Headless / non-interactive mode](../plan.md#headless--non-interactive-mode---yes).
|
|
565
607
|
- **Low risk** (`requiresReview === false` and no `--force-review`):
|
|
566
608
|
Emit the auto-proceed message from the persist stdout
|
|
567
609
|
(`reviewRouting.operatorMessage`) and **continue directly to Phase 8**
|
|
568
610
|
without an extra review stop. The Epic still carries
|
|
569
611
|
`agent::review-spec` until decomposition completes; the routing
|
|
570
|
-
decision is recorded in the `epic-plan-state` checkpoint.
|
|
612
|
+
decision is recorded in the `epic-plan-state` checkpoint. (`--yes` is
|
|
613
|
+
a no-op on this branch — there is no STOP to suppress.)
|
|
571
614
|
|
|
572
615
|
5. **Tech Spec freshness check (advisory)**: After the Tech Spec issue
|
|
573
616
|
is created, `epic-plan-spec.js` runs
|
|
@@ -68,6 +68,9 @@ to authoring the standalone Story body from the handed-off one-pager.
|
|
|
68
68
|
|
|
69
69
|
# Inspect the draft body without creating an Issue:
|
|
70
70
|
/plan --dry-run --body temp/single-story-draft.md
|
|
71
|
+
|
|
72
|
+
# Headless / non-interactive (auto-proceeds the draft-confirm gate):
|
|
73
|
+
/plan --idea "rip out the unused TaskBodyMigrator export" --yes
|
|
71
74
|
```
|
|
72
75
|
|
|
73
76
|
## Phase 1 — Emit Context
|
|
@@ -94,7 +97,7 @@ Envelope fields (`kind: "story-plan-context"`, `version: 1`):
|
|
|
94
97
|
| `bodyTemplate` | Contents of `.agents/templates/single-story-body.md`. |
|
|
95
98
|
| `requiredSections` | `["Context", "Acceptance Criteria", "Out of Scope", "Notes"]`. |
|
|
96
99
|
| `duplicateCandidates` | Ranked open Stories whose titles fuzzy-match the seed. |
|
|
97
|
-
| `techStack` | The `## Tech Stack` section of `docs/architecture.md
|
|
100
|
+
| `techStack` | The project's Tech Stack inventory, resolved in order: `docs/tech-stack.md` (full body) when present, else the `## Tech Stack` section of `docs/architecture.md` (numbered/decorated and final-section headings tolerated). |
|
|
98
101
|
| `deliverContract` | Workflow path + required/forbidden labels and references. |
|
|
99
102
|
|
|
100
103
|
### Refine heuristic
|
|
@@ -153,8 +156,18 @@ add a second stop.
|
|
|
153
156
|
|
|
154
157
|
Display the draft to the operator and **STOP**. Do not call the persist phase
|
|
155
158
|
until the operator explicitly confirms the draft. This mirrors the HITL gate
|
|
156
|
-
`/plan` Phase 3 enforces before opening the Epic Issue.
|
|
157
|
-
|
|
159
|
+
`/plan` Phase 3 enforces before opening the Epic Issue. This is the
|
|
160
|
+
story-path face of `/plan`'s **gate #1** (the ideation one-pager /
|
|
161
|
+
scope-triage confirm). The scope-triage verdict folds into this same stop:
|
|
162
|
+
|
|
163
|
+
> **`--yes` (headless) auto-proceed.** When `/plan` was invoked with `--yes`,
|
|
164
|
+
> this gate does **not** STOP: the draft confirm resolves as **approved** and
|
|
165
|
+
> the run proceeds to Phase 3 (persist). An `epic` verdict resolves to its
|
|
166
|
+
> **Recommended** branch — escalate to `/plan --idea` (carrying `--yes`),
|
|
167
|
+
> abandoning the draft — rather than prompting the three-way choice. Display
|
|
168
|
+
> the draft and the verdict line for the record, then proceed without
|
|
169
|
+
> waiting. See
|
|
170
|
+
> [`plan.md` § Headless / non-interactive mode](../plan.md#headless--non-interactive-mode---yes).
|
|
158
171
|
|
|
159
172
|
- **`story` verdict (or gate skipped via handoff)** → no extra prompt. The
|
|
160
173
|
operator confirms the draft as usual and the run proceeds to Phase 3
|
|
@@ -177,6 +190,9 @@ verdict folds into this same stop:
|
|
|
177
190
|
|
|
178
191
|
**Never auto-route.** The verdict is advisory; the operator always decides,
|
|
179
192
|
and no `agent::*` / label transition happens on either side of the choice.
|
|
193
|
+
(**`--yes` exception:** headless mode pre-authorizes the **Recommended**
|
|
194
|
+
branch deterministically — see the `--yes` note above — the only sanctioned
|
|
195
|
+
auto-route, present solely to make `/plan` driveable without an operator.)
|
|
180
196
|
|
|
181
197
|
## Phase 3 — Persist (`gh issue create`)
|
|
182
198
|
|