mandrel 1.82.0 → 1.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/README.md +41 -0
- package/.agents/docs/SDLC.md +4 -2
- package/.agents/docs/agentrc-reference.json +10 -2
- package/.agents/docs/execution-reference.md +52 -0
- package/.agents/instructions.md +76 -38
- package/.agents/rules/testing-standards.md +14 -0
- package/.agents/schemas/agentrc.schema.json +31 -3
- package/.agents/schemas/qa-ledger.schema.json +2 -2
- package/.agents/scripts/epic-deliver-prepare.js +41 -1
- package/.agents/scripts/lib/config/explain.js +4 -1
- package/.agents/scripts/lib/config-settings-schema.js +25 -1
- package/.agents/scripts/lib/epic-body-sections.js +88 -0
- package/.agents/scripts/lib/findings/promote-finding.js +3 -3
- package/.agents/scripts/lib/findings/severity.js +5 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +65 -2
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +96 -11
- package/.agents/scripts/lib/orchestration/doc-reader.js +29 -0
- package/.agents/scripts/lib/orchestration/docs-digest.js +134 -0
- package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +15 -1
- package/.agents/scripts/lib/qa/console-allowlist.js +5 -4
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +144 -8
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +7 -5
- package/.agents/skills/core/epic-plan-consolidate/examples.md +51 -0
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +4 -22
- package/.agents/skills/core/epic-plan-decompose-author/examples.md +47 -0
- package/.agents/skills/core/epic-plan-premortem/SKILL.md +9 -8
- package/.agents/skills/core/epic-plan-premortem/examples.md +53 -0
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +21 -81
- package/.agents/skills/core/epic-plan-spec-author/examples.md +91 -0
- package/.agents/skills/skills.index.json +3 -3
- package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +52 -38
- package/.agents/workflows/helpers/code-review.md +70 -5
- package/.agents/workflows/helpers/deliver-epic-reference.md +514 -0
- package/.agents/workflows/helpers/deliver-epic.md +164 -469
- package/.agents/workflows/helpers/epic-deliver-story.md +35 -11
- package/.agents/workflows/helpers/plan-epic-reference.md +136 -0
- package/.agents/workflows/helpers/plan-epic.md +56 -186
- package/.agents/workflows/helpers/plan-story.md +31 -61
- package/.agents/workflows/helpers/qa-run-scenario.md +194 -0
- package/.agents/workflows/helpers/scope-triage-gate.md +97 -0
- package/.agents/workflows/helpers/single-story-deliver-reference.md +423 -0
- package/.agents/workflows/helpers/single-story-deliver.md +128 -392
- package/.agents/workflows/qa-explore.md +63 -32
- package/.agents/workflows/qa-run.md +293 -130
- package/docs/CHANGELOG.md +21 -0
- package/package.json +1 -1
- package/.agents/schemas/qa-finding.schema.json +0 -133
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `qa` contract resolver — Epic #3214, Story #3294
|
|
2
|
+
* `qa` contract resolver — Epic #3214, Story #3294; environment-keyed
|
|
3
|
+
* contract added by Epic #4326, Story #4327.
|
|
3
4
|
*
|
|
4
5
|
* The agent-driven QA harness (`/qa-run`) needs the
|
|
5
6
|
* consumer's `.agentrc.json` `qa` block to know where the `.feature` root
|
|
6
|
-
* lives,
|
|
7
|
+
* lives, which deployment targets (`environments`) exist and how to sign in
|
|
8
|
+
* to each, and which personas the seam accepts. The block is
|
|
7
9
|
* *optional in the schema* (most repos never bind the harness, so config
|
|
8
10
|
* validation must not break them — see Tech Spec #3285 § "qa contract
|
|
9
11
|
* block"), which means presence is enforced at run time by this resolver
|
|
@@ -16,8 +18,14 @@
|
|
|
16
18
|
* exists.
|
|
17
19
|
* - Malformed block → throw an error naming the offending field so the
|
|
18
20
|
* operator can fix `.agentrc.json` without spelunking the schema.
|
|
19
|
-
* - Well-formed block → return the normalized contract object with
|
|
20
|
-
* two optional fields
|
|
21
|
+
* - Well-formed block → return the normalized contract object with
|
|
22
|
+
* `environments` + `defaultEnvironment` and the two optional fields
|
|
23
|
+
* (`consoleAllowlist`, `designTokens`) defaulted.
|
|
24
|
+
*
|
|
25
|
+
* `resolveQaEnvironment(contract, target)` selects one environment per
|
|
26
|
+
* harness invocation — by exact name or by raw-URL origin match against each
|
|
27
|
+
* environment's `baseUrl` — and throws loudly (naming the known environments)
|
|
28
|
+
* on an unknown name or unmatched URL.
|
|
21
29
|
*/
|
|
22
30
|
|
|
23
31
|
import Ajv from 'ajv';
|
|
@@ -36,10 +44,19 @@ import { QA_SCHEMA } from '../config-settings-schema.js';
|
|
|
36
44
|
export const QA_REQUIRED_FIELDS = Object.freeze([
|
|
37
45
|
'featureRoot',
|
|
38
46
|
'fixturesManifest',
|
|
39
|
-
'
|
|
47
|
+
'environments',
|
|
40
48
|
'personas',
|
|
41
49
|
]);
|
|
42
50
|
|
|
51
|
+
/**
|
|
52
|
+
* The environment name whose `allowWrites` defaults to `true` when the
|
|
53
|
+
* consumer omits the flag. Every other environment defaults to read-only
|
|
54
|
+
* (`allowWrites: false`) so an unguarded remote target cannot accept writes
|
|
55
|
+
* by accident — only the conventional `local` environment is write-enabled
|
|
56
|
+
* by default.
|
|
57
|
+
*/
|
|
58
|
+
const WRITE_ENABLED_DEFAULT_ENVIRONMENT = 'local';
|
|
59
|
+
|
|
43
60
|
/** Defaults applied to the optional fields of a well-formed contract. */
|
|
44
61
|
export const QA_CONTRACT_DEFAULTS = Object.freeze({
|
|
45
62
|
consoleAllowlist: Object.freeze([]),
|
|
@@ -48,7 +65,7 @@ export const QA_CONTRACT_DEFAULTS = Object.freeze({
|
|
|
48
65
|
|
|
49
66
|
const ABSENT_MESSAGE =
|
|
50
67
|
'qa: this project has not bound the QA harness — add a `qa` block to ' +
|
|
51
|
-
'.agentrc.json (featureRoot, fixturesManifest,
|
|
68
|
+
'.agentrc.json (featureRoot, fixturesManifest, environments, personas) ' +
|
|
52
69
|
'before invoking the QA harness. See .agents/docs/agentrc-reference.json for the ' +
|
|
53
70
|
'full contract shape.';
|
|
54
71
|
|
|
@@ -127,7 +144,8 @@ function describeError(err) {
|
|
|
127
144
|
* @returns {{
|
|
128
145
|
* featureRoot: string,
|
|
129
146
|
* fixturesManifest: string,
|
|
130
|
-
* signInSeam: object,
|
|
147
|
+
* environments: Record<string, { baseUrl: string, signInSeam: object, allowWrites?: boolean }>,
|
|
148
|
+
* defaultEnvironment: string,
|
|
131
149
|
* personas: Record<string, object>,
|
|
132
150
|
* personaNames: string[],
|
|
133
151
|
* consoleAllowlist: string[],
|
|
@@ -173,10 +191,28 @@ export function resolveQaContract(config) {
|
|
|
173
191
|
|
|
174
192
|
const { personas, personaNames } = normalizePersonas(qa.personas);
|
|
175
193
|
|
|
194
|
+
// Clone each environment so callers cannot mutate the resolver's input.
|
|
195
|
+
const environments = {};
|
|
196
|
+
for (const [name, env] of Object.entries(qa.environments)) {
|
|
197
|
+
environments[name] = { ...env };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// The default environment is the conventional `local` target when present,
|
|
201
|
+
// otherwise the first-declared environment. `resolveQaEnvironment(contract)`
|
|
202
|
+
// (no target) resolves to this one.
|
|
203
|
+
const environmentNames = Object.keys(environments);
|
|
204
|
+
const defaultEnvironment = Object.hasOwn(
|
|
205
|
+
environments,
|
|
206
|
+
WRITE_ENABLED_DEFAULT_ENVIRONMENT,
|
|
207
|
+
)
|
|
208
|
+
? WRITE_ENABLED_DEFAULT_ENVIRONMENT
|
|
209
|
+
: environmentNames[0];
|
|
210
|
+
|
|
176
211
|
return {
|
|
177
212
|
featureRoot: qa.featureRoot,
|
|
178
213
|
fixturesManifest: qa.fixturesManifest,
|
|
179
|
-
|
|
214
|
+
environments,
|
|
215
|
+
defaultEnvironment,
|
|
180
216
|
personas,
|
|
181
217
|
personaNames,
|
|
182
218
|
consoleAllowlist: Array.isArray(qa.consoleAllowlist)
|
|
@@ -188,3 +224,103 @@ export function resolveQaContract(config) {
|
|
|
188
224
|
: qa.designTokens,
|
|
189
225
|
};
|
|
190
226
|
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Normalize a value to its URL origin (`protocol//host:port`), or `null` when
|
|
230
|
+
* it is not a parseable absolute URL. Used to match a raw-URL `target` against
|
|
231
|
+
* each environment's `baseUrl` by origin, so a target carrying a path,
|
|
232
|
+
* query-string, or trailing slash still resolves to the right environment.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} value
|
|
235
|
+
* @returns {string | null}
|
|
236
|
+
*/
|
|
237
|
+
function toOrigin(value) {
|
|
238
|
+
try {
|
|
239
|
+
return new URL(value).origin;
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Resolve a single QA environment for one harness invocation.
|
|
247
|
+
*
|
|
248
|
+
* `target` selects which of the contract's `environments` to run against:
|
|
249
|
+
* - **Omitted / falsy** → the contract's `defaultEnvironment`.
|
|
250
|
+
* - **Exact environment name** → that environment.
|
|
251
|
+
* - **Raw URL** → the environment whose `baseUrl` shares the same origin
|
|
252
|
+
* (`protocol//host:port`), so a target with a path or query still matches.
|
|
253
|
+
*
|
|
254
|
+
* Resolution is name-first: a `target` that exactly names an environment wins
|
|
255
|
+
* even if it also happens to parse as a URL.
|
|
256
|
+
*
|
|
257
|
+
* `allowWrites` is resolved to an explicit boolean on the returned object: the
|
|
258
|
+
* environment's own value when set, otherwise `true` only for the conventional
|
|
259
|
+
* `local` environment and `false` for every other target — an unguarded remote
|
|
260
|
+
* environment is read-only unless the consumer opts in.
|
|
261
|
+
*
|
|
262
|
+
* Fails **loudly**: an unknown name or an unmatched URL throws an error that
|
|
263
|
+
* names the known environments so the operator can correct the invocation.
|
|
264
|
+
*
|
|
265
|
+
* @param {{ environments: Record<string, { baseUrl: string, signInSeam: object, allowWrites?: boolean }>, defaultEnvironment: string }} contract
|
|
266
|
+
* A contract returned by `resolveQaContract`.
|
|
267
|
+
* @param {string} [target] Environment name or raw URL. Omit for the default.
|
|
268
|
+
* @returns {{ name: string, baseUrl: string, signInSeam: object, allowWrites: boolean }}
|
|
269
|
+
* @throws {Error} on an unknown name or unmatched URL.
|
|
270
|
+
*/
|
|
271
|
+
export function resolveQaEnvironment(contract, target) {
|
|
272
|
+
const environments = contract?.environments;
|
|
273
|
+
if (
|
|
274
|
+
environments == null ||
|
|
275
|
+
typeof environments !== 'object' ||
|
|
276
|
+
Object.keys(environments).length === 0
|
|
277
|
+
) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
'qa: cannot resolve an environment — the contract carries no ' +
|
|
280
|
+
'`environments`. Call resolveQaContract first.',
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const known = Object.keys(environments);
|
|
285
|
+
const knownList = known.map((name) => `\`${name}\``).join(', ');
|
|
286
|
+
|
|
287
|
+
// No target → the default environment.
|
|
288
|
+
const name =
|
|
289
|
+
target == null || target === '' ? contract.defaultEnvironment : target;
|
|
290
|
+
|
|
291
|
+
// Exact-name match wins first (a name that also parses as a URL still
|
|
292
|
+
// resolves by name).
|
|
293
|
+
let resolvedName = Object.hasOwn(environments, name) ? name : null;
|
|
294
|
+
|
|
295
|
+
// Otherwise try to match the target as a raw URL against each baseUrl origin.
|
|
296
|
+
if (resolvedName === null) {
|
|
297
|
+
const targetOrigin = toOrigin(name);
|
|
298
|
+
if (targetOrigin !== null) {
|
|
299
|
+
resolvedName =
|
|
300
|
+
known.find(
|
|
301
|
+
(envName) => toOrigin(environments[envName].baseUrl) === targetOrigin,
|
|
302
|
+
) ?? null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (resolvedName === null) {
|
|
307
|
+
throw new Error(
|
|
308
|
+
`qa: unknown environment \`${name}\` — the contract declares ${knownList}. ` +
|
|
309
|
+
'Pass an exact environment name or a URL whose origin matches one of ' +
|
|
310
|
+
'their baseUrl values.',
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const env = environments[resolvedName];
|
|
315
|
+
const allowWrites =
|
|
316
|
+
typeof env.allowWrites === 'boolean'
|
|
317
|
+
? env.allowWrites
|
|
318
|
+
: resolvedName === WRITE_ENABLED_DEFAULT_ENVIRONMENT;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
name: resolvedName,
|
|
322
|
+
baseUrl: env.baseUrl,
|
|
323
|
+
signInSeam: env.signInSeam,
|
|
324
|
+
allowWrites,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
@@ -30,11 +30,13 @@ allowed_tools:
|
|
|
30
30
|
## Role
|
|
31
31
|
|
|
32
32
|
Senior Project Manager + Orchestrator, acting as a **holistic critic** with
|
|
33
|
-
fresh context
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
fresh context — deliberately *separate* from `epic-plan-decompose-author` (the
|
|
34
|
+
generator) so the pass is a fresh-context review, not a same-pass self-critique.
|
|
35
|
+
|
|
36
|
+
> **Read [`examples.md`](./examples.md) on demand** for the extended rationale:
|
|
37
|
+
> why this critic runs with fresh context, why scope conservation is your
|
|
38
|
+
> contract rather than a runtime guarantee, and how the below-ceiling
|
|
39
|
+
> coarsening case is reported.
|
|
38
40
|
|
|
39
41
|
## When to use
|
|
40
42
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# epic-plan-consolidate — examples & extended rationale
|
|
2
|
+
|
|
3
|
+
Read this file on demand. The SKILL.md carries the operating contract (Policy
|
|
4
|
+
Capsule, Inputs / Outputs, Procedure, Constraints); this file carries the
|
|
5
|
+
extended rationale behind the consolidation pass — why it is a separate,
|
|
6
|
+
fresh-context critic and how the scope-conservation invariant is enforced by
|
|
7
|
+
you rather than by the runtime.
|
|
8
|
+
|
|
9
|
+
## Why a separate fresh-context critic
|
|
10
|
+
|
|
11
|
+
This Skill acts as a **holistic critic** with fresh context (a Senior Project
|
|
12
|
+
Manager + Orchestrator lens). It is deliberately *separate* from
|
|
13
|
+
`epic-plan-decompose-author` (the generator): a same-pass self-critique is the
|
|
14
|
+
weak mode this is built to escape. The generator maps Epic capabilities to
|
|
15
|
+
Stories ~1:1; this critic steps back and looks at the *whole* decomposition
|
|
16
|
+
against the Tech Spec's intentional grouping before any GitHub write.
|
|
17
|
+
|
|
18
|
+
## Scope conservation is your contract, not a machine guarantee
|
|
19
|
+
|
|
20
|
+
Scope conservation is the load-bearing invariant: you MUST NOT add scope,
|
|
21
|
+
invent tickets, or drop acceptance criteria. Every acceptance item and every
|
|
22
|
+
`verify` entry present in the draft MUST survive into the consolidated array
|
|
23
|
+
(possibly re-homed onto a merged Story).
|
|
24
|
+
|
|
25
|
+
**This is your contract, not a machine guarantee.** There is **no runtime
|
|
26
|
+
acceptance-union diff** on your output. The only deterministic runtime backstop
|
|
27
|
+
the validator applies after you run is the standard ticket-structure
|
|
28
|
+
validation — it does not re-derive the pre-consolidation acceptance/verify
|
|
29
|
+
union, so a critic that silently dropped an acceptance item would **not** be
|
|
30
|
+
caught downstream. (The repo's unit test exercises a *pure model* of the merge
|
|
31
|
+
over an over-fragmented fixture to document the intended invariant; it does not
|
|
32
|
+
inspect this Skill's actual output.) Conserve scope yourself, deliberately, on
|
|
33
|
+
every merge — and if you cannot preserve an item, you have over-reached, so back
|
|
34
|
+
the operation out.
|
|
35
|
+
|
|
36
|
+
## Ceiling, not target — the below-ceiling coarsening case
|
|
37
|
+
|
|
38
|
+
Consume the Tech Spec **"Delivery Slicing"** section as a **ceiling, not a
|
|
39
|
+
target** when present: the Architect's proposed N shippable Stories is an
|
|
40
|
+
*upper bound*, not a floor. Cluster the draft's Stories toward that grouping,
|
|
41
|
+
and go **below** N when slices form dependent single-consumer chains — a slice
|
|
42
|
+
whose "Independent? No" row carries no justification (parallelism, risk
|
|
43
|
+
isolation, or delivery-envelope pressure) folds into its consumer. You **never**
|
|
44
|
+
split above N.
|
|
45
|
+
|
|
46
|
+
When the consolidated count drops **below** the Delivery-Slicing count, the
|
|
47
|
+
consolidation report MUST surface that fact explicitly — e.g.
|
|
48
|
+
`Below ceiling: N (Delivery Slicing) → M (consolidated)` — and attach a one-line
|
|
49
|
+
rationale to each below-ceiling merge (which dependent single-consumer slice
|
|
50
|
+
folded into which consumer, and why it was unjustified), so the operator sees
|
|
51
|
+
the coarsening at the Phase 8.3 advisory diff.
|
|
@@ -302,28 +302,10 @@ When the Acceptance Table contains **one or more `Disposition: new` rows**, you
|
|
|
302
302
|
|
|
303
303
|
When the Acceptance Table contains **zero `new`-disposition rows** (every row is `updated` or `unchanged`), do NOT emit a scaffold Story — there is nothing to create.
|
|
304
304
|
|
|
305
|
-
**
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
"type": "story",
|
|
310
|
-
"title": "Scaffold @skip-tagged billing feature files",
|
|
311
|
-
"depends_on": [],
|
|
312
|
-
"labels": ["type::story", "persona::qa-engineer"],
|
|
313
|
-
"acceptance": [
|
|
314
|
-
"tests/features/billing/invoice.feature and tests/features/billing/refund.feature both exist on the branch",
|
|
315
|
-
"every Scenario in the two new feature files is preceded by an @skip tag (grep for un-skipped scenarios returns zero matches)",
|
|
316
|
-
"the invoice.feature scenario carries @epic-42-ac-1 and the refund.feature scenario carries @epic-42-ac-2"
|
|
317
|
-
],
|
|
318
|
-
"verify": [
|
|
319
|
-
"test -f tests/features/billing/invoice.feature && test -f tests/features/billing/refund.feature (validate)",
|
|
320
|
-
"test -z \"$(grep -rL '@skip' tests/features/billing/*.feature)\" (validate)",
|
|
321
|
-
"grep -q '@epic-42-ac-1' tests/features/billing/invoice.feature && grep -q '@epic-42-ac-2' tests/features/billing/refund.feature (validate)"
|
|
322
|
-
],
|
|
323
|
-
"body": "## Goal\nbdd-scaffold: create the @skip-tagged, @epic-42-ac-N-tagged feature files the billing-flows implementation Stories verify against, so wave-0 lands them before any implementation Story runs.\n\n## Changes\n- {\"path\": \"tests/features/billing/invoice.feature\", \"assumption\": \"creates\"}\n- {\"path\": \"tests/features/billing/refund.feature\", \"assumption\": \"creates\"}\n\n## Acceptance\n- [ ] tests/features/billing/invoice.feature and tests/features/billing/refund.feature both exist on the branch\n- [ ] every Scenario in the two new feature files is preceded by an @skip tag\n- [ ] the invoice.feature scenario carries @epic-42-ac-1 and the refund.feature scenario carries @epic-42-ac-2\n\n## Verify\n- test -f tests/features/billing/invoice.feature && test -f tests/features/billing/refund.feature (validate)\n- test -z \"$(grep -rL '@skip' tests/features/billing/*.feature)\" (validate)\n- grep -q '@epic-42-ac-1' tests/features/billing/invoice.feature && grep -q '@epic-42-ac-2' tests/features/billing/refund.feature (validate)\n"
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
The implementation Stories that later un-skip and flesh out these scenarios each carry `depends_on: ["scaffold-billing-feature-files"]`, placing them in a later wave than the scaffold. They MUST NOT add the `@epic-42-ac-N` tag themselves — it is already present from the scaffold pass; their job is to remove `@skip` once the scenario passes.
|
|
305
|
+
> **Read [`examples.md`](./examples.md) on demand** for a fully worked wave-0
|
|
306
|
+
> scaffold Story (Epic #42, two `new` rows) showing the serialized string
|
|
307
|
+
> `body`, the top-level `acceptance`/`verify` arrays, the empty `depends_on`,
|
|
308
|
+
> and the paired `@skip` + `@epic-<id>-ac-N` tagging.
|
|
327
309
|
|
|
328
310
|
### SCOPE-OVERLAP FLAGGING (docs/runbook downstream of config work)
|
|
329
311
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# epic-plan-decompose-author — examples & extended rationale
|
|
2
|
+
|
|
3
|
+
Read this file on demand while authoring the ticket array. The SKILL.md carries
|
|
4
|
+
the operating contract (Policy Capsule, Inputs / Outputs, Procedure, the
|
|
5
|
+
authoring guidance that complements the rendered decomposer prompt, and
|
|
6
|
+
Constraints); this file carries the worked WAVE-0 BDD scaffold Story and the
|
|
7
|
+
extended reasoning behind it.
|
|
8
|
+
|
|
9
|
+
## WAVE-0 BDD scaffold Story — worked example
|
|
10
|
+
|
|
11
|
+
The contract lives in SKILL.md under **WAVE-0 BDD SCAFFOLD STORY**: when the
|
|
12
|
+
Epic body's `## Acceptance Table` carries one or more `Disposition: new` rows,
|
|
13
|
+
emit **exactly one** dedicated wave-0 scaffold Story whose sole job is to create
|
|
14
|
+
those `.feature` files with `@skip`-tagged scenarios (each also carrying its
|
|
15
|
+
namespaced `@epic-<id>-ac-N` tag) BEFORE any implementation Story runs.
|
|
16
|
+
|
|
17
|
+
**Worked example.** Epic #42, Acceptance Table with two `new` rows
|
|
18
|
+
(`AC-1` -> `tests/features/billing/invoice.feature`,
|
|
19
|
+
`AC-2` -> `tests/features/billing/refund.feature`). The scaffold Story below
|
|
20
|
+
uses a serialized string `body`, top-level `acceptance`/`verify` arrays, an
|
|
21
|
+
empty `depends_on`, and tags each scenario with both `@skip` and its namespaced
|
|
22
|
+
`@epic-42-ac-N` tag:
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
"slug": "scaffold-billing-feature-files",
|
|
26
|
+
"type": "story",
|
|
27
|
+
"title": "Scaffold @skip-tagged billing feature files",
|
|
28
|
+
"depends_on": [],
|
|
29
|
+
"labels": ["type::story", "persona::qa-engineer"],
|
|
30
|
+
"acceptance": [
|
|
31
|
+
"tests/features/billing/invoice.feature and tests/features/billing/refund.feature both exist on the branch",
|
|
32
|
+
"every Scenario in the two new feature files is preceded by an @skip tag (grep for un-skipped scenarios returns zero matches)",
|
|
33
|
+
"the invoice.feature scenario carries @epic-42-ac-1 and the refund.feature scenario carries @epic-42-ac-2"
|
|
34
|
+
],
|
|
35
|
+
"verify": [
|
|
36
|
+
"test -f tests/features/billing/invoice.feature && test -f tests/features/billing/refund.feature (validate)",
|
|
37
|
+
"test -z \"$(grep -rL '@skip' tests/features/billing/*.feature)\" (validate)",
|
|
38
|
+
"grep -q '@epic-42-ac-1' tests/features/billing/invoice.feature && grep -q '@epic-42-ac-2' tests/features/billing/refund.feature (validate)"
|
|
39
|
+
],
|
|
40
|
+
"body": "## Goal\nbdd-scaffold: create the @skip-tagged, @epic-42-ac-N-tagged feature files the billing-flows implementation Stories verify against, so wave-0 lands them before any implementation Story runs.\n\n## Changes\n- {\"path\": \"tests/features/billing/invoice.feature\", \"assumption\": \"creates\"}\n- {\"path\": \"tests/features/billing/refund.feature\", \"assumption\": \"creates\"}\n\n## Acceptance\n- [ ] tests/features/billing/invoice.feature and tests/features/billing/refund.feature both exist on the branch\n- [ ] every Scenario in the two new feature files is preceded by an @skip tag\n- [ ] the invoice.feature scenario carries @epic-42-ac-1 and the refund.feature scenario carries @epic-42-ac-2\n\n## Verify\n- test -f tests/features/billing/invoice.feature && test -f tests/features/billing/refund.feature (validate)\n- test -z \"$(grep -rL '@skip' tests/features/billing/*.feature)\" (validate)\n- grep -q '@epic-42-ac-1' tests/features/billing/invoice.feature && grep -q '@epic-42-ac-2' tests/features/billing/refund.feature (validate)\n"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
The implementation Stories that later un-skip and flesh out these scenarios each
|
|
44
|
+
carry `depends_on: ["scaffold-billing-feature-files"]`, placing them in a later
|
|
45
|
+
wave than the scaffold. They MUST NOT add the `@epic-42-ac-N` tag themselves —
|
|
46
|
+
it is already present from the scaffold pass; their job is to remove `@skip`
|
|
47
|
+
once the scenario passes.
|
|
@@ -28,14 +28,15 @@ allowed_tools:
|
|
|
28
28
|
|
|
29
29
|
## Role
|
|
30
30
|
|
|
31
|
-
Senior Engineer + Architect, acting as a **fresh-context pre-mortem critic
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
a same-pass self-critique
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
Senior Engineer + Architect, acting as a **fresh-context pre-mortem critic** —
|
|
32
|
+
deliberately *separate* from `epic-plan-decompose-author` (the generator) and
|
|
33
|
+
`epic-plan-consolidate` (the scope-preserving merge critic) so it is a
|
|
34
|
+
fresh-context, code-reading review, not a same-pass self-critique.
|
|
35
|
+
|
|
36
|
+
> **Read [`examples.md`](./examples.md) on demand** for the extended rationale:
|
|
37
|
+
> why this critic opens the actual cited code, why it is additive-recommendation
|
|
38
|
+
> (not scope-preserving) unlike consolidation, and the three predicted-rework
|
|
39
|
+
> finding classes in full.
|
|
39
40
|
|
|
40
41
|
## When to use
|
|
41
42
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# epic-plan-premortem — examples & extended rationale
|
|
2
|
+
|
|
3
|
+
Read this file on demand. The SKILL.md carries the operating contract (Policy
|
|
4
|
+
Capsule, Inputs / Outputs, Procedure, Constraints); this file carries the
|
|
5
|
+
extended rationale behind the pre-mortem critic — why it is a separate,
|
|
6
|
+
fresh-context, code-reading pass and how its three predicted-rework finding
|
|
7
|
+
classes differ from the structural gates.
|
|
8
|
+
|
|
9
|
+
## Why a separate fresh-context code-reading critic
|
|
10
|
+
|
|
11
|
+
This Skill acts as a Senior Engineer + Architect in the role of a
|
|
12
|
+
**fresh-context pre-mortem critic**. It is deliberately *separate* from
|
|
13
|
+
`epic-plan-decompose-author` (the generator) and from `epic-plan-consolidate`
|
|
14
|
+
(the scope-preserving merge critic): a same-pass self-critique is the weak mode
|
|
15
|
+
this is built to escape. The generator maps Epic capabilities to Stories
|
|
16
|
+
against the spec text; this critic opens the **actual cited code** and asks "if
|
|
17
|
+
I tried to deliver this exact backlog, where would it rework?" — before any
|
|
18
|
+
GitHub write makes the rework expensive.
|
|
19
|
+
|
|
20
|
+
**You MUST read the actual cited code surfaces.** This is the load-bearing
|
|
21
|
+
difference between this critic and the structural file-assumption gate: that
|
|
22
|
+
gate proves a path **exists** (or does not); this critic reads what the file
|
|
23
|
+
actually **contains**. A pre-mortem that did not open the cited files has not
|
|
24
|
+
run.
|
|
25
|
+
|
|
26
|
+
## Not scope-preserving (unlike consolidation)
|
|
27
|
+
|
|
28
|
+
Unlike `epic-plan-consolidate` (merge-and-rewire only), this critic MAY
|
|
29
|
+
recommend splitting an under-specified Story, tightening or rewording an
|
|
30
|
+
acceptance criterion, or flagging an over-specified Story — because it only
|
|
31
|
+
*recommends* in a report; it never applies the change itself. The conservation
|
|
32
|
+
invariant belongs to consolidation; this pass is deliberately a separate,
|
|
33
|
+
additive-recommendation lens. It never writes to GitHub, never persists
|
|
34
|
+
`tickets.json`, and never flips a label — re-authoring on its findings is the
|
|
35
|
+
author skill's job (the workflow re-runs `epic-plan-decompose-author` on the
|
|
36
|
+
report before the persist call).
|
|
37
|
+
|
|
38
|
+
## The three predicted-rework finding classes
|
|
39
|
+
|
|
40
|
+
Hunt for the three classes the structural gates cannot catch:
|
|
41
|
+
|
|
42
|
+
1. **Unverifiable acceptance criteria** — an AC no `verify[]` command or
|
|
43
|
+
readable code state can prove. Recommend a concrete verify command or a
|
|
44
|
+
reworded, checkable AC.
|
|
45
|
+
2. **Over- or under-specified Stories** — a Story whose `acceptance[]` is far
|
|
46
|
+
broader or narrower than its `changes[]` footprint and the cited code
|
|
47
|
+
support. Recommend a split (under-specified: one Story doing the work of
|
|
48
|
+
several) or a tightening.
|
|
49
|
+
3. **Semantically-wrong assumptions** — the cited file exists (so the
|
|
50
|
+
file-assumption gate passes) but does not contain the seam / export /
|
|
51
|
+
function / data shape the Story assumes — the file-assumption gate passes,
|
|
52
|
+
the work would still rework. Recommend the corrected target or an explicit
|
|
53
|
+
"create the seam first" Story.
|
|
@@ -181,88 +181,28 @@ before reaching for names that appear only in the documentation. Write to
|
|
|
181
181
|
|
|
182
182
|
#### Delivery Slicing section (authoritative ceiling for Phase 8 consolidation)
|
|
183
183
|
|
|
184
|
-
The Tech Spec MUST carry a `## Delivery Slicing` section
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
the Phase 8 consolidation pass
|
|
184
|
+
The Tech Spec MUST carry a `## Delivery Slicing` section proposing how the
|
|
185
|
+
Epic's enumerated capabilities **cluster into N shippable Stories** — the
|
|
186
|
+
intentional grouping the Phase 8 consolidation pass
|
|
188
187
|
([`epic-plan-consolidate`](../epic-plan-consolidate/SKILL.md)) reconciles the
|
|
189
|
-
decomposer
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
**The
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
other sections.
|
|
208
|
-
|
|
209
|
-
Author the section as a table — one row per proposed slice — naming the
|
|
210
|
-
capability cluster each slice would deliver, what ships in it, and whether it
|
|
211
|
-
can ship independently. Use **noun phrases** for slice names ("Foundation",
|
|
212
|
-
"Transport seam", "Send helper") so they map cleanly onto Feature titles in the
|
|
213
|
-
resulting decomposition — never verb phrases ("Add transport") or file names
|
|
214
|
-
("`sender.ts`"). Do **not** coarsen the Epic's capability enumeration to produce
|
|
215
|
-
the slicing: the granularity lever is *this* grouping recommendation, not a
|
|
216
|
-
dumbed-down Epic enumeration.
|
|
217
|
-
|
|
218
|
-
**What "Independent?" means:** can this slice ship to production and provide
|
|
219
|
-
value *without the next slice landing*? A `Yes` slice is releasable on its own;
|
|
220
|
-
a `No` slice only becomes valuable once a later slice lands on top of it.
|
|
221
|
-
|
|
222
|
-
**"Independent? No" is a smell that must be justified.** A dependent,
|
|
223
|
-
single-consumer slice (one that only feeds the next slice) folds into its
|
|
224
|
-
consumer by default — it is not worth its own delivery session's hydration,
|
|
225
|
-
branch, PR, and CI ceremony. Mark a slice `No` only when you can name a
|
|
226
|
-
one-line reason to keep it separate anyway: **parallelism** (two `No` slices
|
|
227
|
-
that can be delivered concurrently by different sessions), **risk isolation**
|
|
228
|
-
(a blast-radius or reviewability reason to land it as its own reviewable PR),
|
|
229
|
-
or **delivery-envelope pressure** (folding it in would push the consumer past a
|
|
230
|
-
single-session sizing envelope). Absent such a justification, do not author the
|
|
231
|
-
slice as its own row — fold it into its consumer and let the merged slice carry
|
|
232
|
-
the combined capability.
|
|
233
|
-
|
|
234
|
-
Worked example:
|
|
235
|
-
|
|
236
|
-
```text
|
|
237
|
-
## Delivery Slicing
|
|
238
|
-
|
|
239
|
-
Proposed shippable slices (consolidation ceiling for Phase 8):
|
|
240
|
-
|
|
241
|
-
| Slice | What ships | Independent? |
|
|
242
|
-
| -------------- | ------------------------------------------------------ | ------------ |
|
|
243
|
-
| Foundation | Config schema, types, and the no-op default path | Yes |
|
|
244
|
-
| Transport seam | The pluggable transport interface + in-memory adapter | Yes |
|
|
245
|
-
| Send helper | The send() helper + retries, built on the transport | No (justified: risk isolation) |
|
|
246
|
-
|
|
247
|
-
- **Foundation** folds Epic capabilities "config surface" + "type model" — they
|
|
248
|
-
share a reason to exist and ship as one reviewable PR.
|
|
249
|
-
- **Transport seam** is the pluggable boundary; it provides value on its own
|
|
250
|
-
(in-memory adapter is usable for tests) so it is independently shippable.
|
|
251
|
-
- **Send helper** depends on the transport seam landing first, so it is *not*
|
|
252
|
-
independent. It stays its own slice only because the retry/backoff logic is a
|
|
253
|
-
large, high-blast-radius surface worth isolating in its own reviewable PR
|
|
254
|
-
(risk isolation). Absent that justification it would fold into Transport
|
|
255
|
-
seam — a bare "depends on the previous slice" is not a reason to keep it
|
|
256
|
-
separate.
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
The consolidation pass degrades gracefully when this section is absent (it
|
|
260
|
-
falls back to cohesion + single-Story-Feature rules only), so authoring it is
|
|
261
|
-
how the Architect steers the decomposition toward fewer, right-sized Stories.
|
|
262
|
-
Because the count is a **ceiling**, an over-sliced table is coarsened back
|
|
263
|
-
during consolidation — but only where the extra slices are dependent
|
|
264
|
-
single-consumer chains, so an unjustified `No` slice is the one you should fold
|
|
265
|
-
in yourself rather than leaning on the consolidator to catch.
|
|
188
|
+
decomposer draft against before any GitHub write. Author it as a table (one row
|
|
189
|
+
per slice: `Slice | What ships | Independent?`), using **noun-phrase** slice
|
|
190
|
+
names, and **write it before any other section** — it is the primary input to
|
|
191
|
+
consolidation and the section the model most often omits when it drafts it last.
|
|
192
|
+
|
|
193
|
+
- **The count is a ceiling, not a target.** Consolidation may merge below it
|
|
194
|
+
(dependent single-consumer chains) but never splits above it.
|
|
195
|
+
- **"Independent?"** answers: can this slice ship to production and provide
|
|
196
|
+
value without the next slice landing? Mark a slice `No` only with a one-line
|
|
197
|
+
justification (parallelism, risk isolation, or delivery-envelope pressure);
|
|
198
|
+
an unjustified dependent single-consumer slice folds into its consumer.
|
|
199
|
+
- Do **not** coarsen the Epic's capability enumeration to produce the slicing;
|
|
200
|
+
the grouping recommendation is the granularity lever.
|
|
201
|
+
|
|
202
|
+
> **Read [`examples.md`](./examples.md) on demand** for the worked Delivery
|
|
203
|
+
> Slicing table and the extended rationale behind these rules (what
|
|
204
|
+
> "Independent?" means, why an unjustified `No` slice is a smell, and how the
|
|
205
|
+
> consolidation pass degrades gracefully when the section is absent).
|
|
266
206
|
|
|
267
207
|
#### Tech Spec system prompt (authoritative)
|
|
268
208
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# epic-plan-spec-author — examples & extended rationale
|
|
2
|
+
|
|
3
|
+
Read this file on demand while authoring the Tech Spec. The SKILL.md carries
|
|
4
|
+
the operating contract (Policy Capsule, Inputs / Outputs, Procedure, the two
|
|
5
|
+
authoritative system prompts, and Constraints); this file carries the worked
|
|
6
|
+
Delivery Slicing example and the extended rationale behind the slicing rules.
|
|
7
|
+
|
|
8
|
+
## Delivery Slicing — extended rationale
|
|
9
|
+
|
|
10
|
+
The Tech Spec MUST carry a `## Delivery Slicing` section in which the Architect
|
|
11
|
+
— who holds the full design — proposes how the Epic's enumerated capabilities
|
|
12
|
+
**cluster into N shippable Stories**. This section is the intentional grouping
|
|
13
|
+
the Phase 8 consolidation pass
|
|
14
|
+
([`epic-plan-consolidate`](../epic-plan-consolidate/SKILL.md)) reconciles the
|
|
15
|
+
decomposer's draft against before any GitHub write. Without it, the decompose
|
|
16
|
+
phase maps Epic capabilities to Stories ~1:1 and cannot produce a coarser,
|
|
17
|
+
holistic plan; with it, the consolidation critic has a well-defined reference
|
|
18
|
+
instead of a guess.
|
|
19
|
+
|
|
20
|
+
**The proposed count is a ceiling, not a target.** Consolidation reconciles
|
|
21
|
+
the draft *toward* your grouping, but it treats the count as an upper bound: it
|
|
22
|
+
may **merge below** your proposed count when slices form dependent
|
|
23
|
+
single-consumer chains, and it **never splits above** it. Over-slicing here
|
|
24
|
+
therefore locks in fragmentation only when the extra slices are genuinely
|
|
25
|
+
independent — so keep a slice separate only when it earns its own delivery
|
|
26
|
+
session.
|
|
27
|
+
|
|
28
|
+
**Write the Delivery Slicing section before any other section — it is the
|
|
29
|
+
primary input to Phase 8 consolidation.** Author it first so the rest of the
|
|
30
|
+
spec (Core Components, API Changes, Data Models) hangs off a deliberate
|
|
31
|
+
slicing decision rather than being reverse-engineered into one at the end.
|
|
32
|
+
Drafting it last is exactly how the model omits it under the weight of the
|
|
33
|
+
other sections.
|
|
34
|
+
|
|
35
|
+
Author the section as a table — one row per proposed slice — naming the
|
|
36
|
+
capability cluster each slice would deliver, what ships in it, and whether it
|
|
37
|
+
can ship independently. Use **noun phrases** for slice names ("Foundation",
|
|
38
|
+
"Transport seam", "Send helper") so they map cleanly onto Feature titles in the
|
|
39
|
+
resulting decomposition — never verb phrases ("Add transport") or file names
|
|
40
|
+
("`sender.ts`"). Do **not** coarsen the Epic's capability enumeration to produce
|
|
41
|
+
the slicing: the granularity lever is *this* grouping recommendation, not a
|
|
42
|
+
dumbed-down Epic enumeration.
|
|
43
|
+
|
|
44
|
+
**What "Independent?" means:** can this slice ship to production and provide
|
|
45
|
+
value *without the next slice landing*? A `Yes` slice is releasable on its own;
|
|
46
|
+
a `No` slice only becomes valuable once a later slice lands on top of it.
|
|
47
|
+
|
|
48
|
+
**"Independent? No" is a smell that must be justified.** A dependent,
|
|
49
|
+
single-consumer slice (one that only feeds the next slice) folds into its
|
|
50
|
+
consumer by default — it is not worth its own delivery session's hydration,
|
|
51
|
+
branch, PR, and CI ceremony. Mark a slice `No` only when you can name a
|
|
52
|
+
one-line reason to keep it separate anyway: **parallelism** (two `No` slices
|
|
53
|
+
that can be delivered concurrently by different sessions), **risk isolation**
|
|
54
|
+
(a blast-radius or reviewability reason to land it as its own reviewable PR),
|
|
55
|
+
or **delivery-envelope pressure** (folding it in would push the consumer past a
|
|
56
|
+
single-session sizing envelope). Absent such a justification, do not author the
|
|
57
|
+
slice as its own row — fold it into its consumer and let the merged slice carry
|
|
58
|
+
the combined capability.
|
|
59
|
+
|
|
60
|
+
### Worked example
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
## Delivery Slicing
|
|
64
|
+
|
|
65
|
+
Proposed shippable slices (consolidation ceiling for Phase 8):
|
|
66
|
+
|
|
67
|
+
| Slice | What ships | Independent? |
|
|
68
|
+
| -------------- | ------------------------------------------------------ | ------------ |
|
|
69
|
+
| Foundation | Config schema, types, and the no-op default path | Yes |
|
|
70
|
+
| Transport seam | The pluggable transport interface + in-memory adapter | Yes |
|
|
71
|
+
| Send helper | The send() helper + retries, built on the transport | No (justified: risk isolation) |
|
|
72
|
+
|
|
73
|
+
- **Foundation** folds Epic capabilities "config surface" + "type model" — they
|
|
74
|
+
share a reason to exist and ship as one reviewable PR.
|
|
75
|
+
- **Transport seam** is the pluggable boundary; it provides value on its own
|
|
76
|
+
(in-memory adapter is usable for tests) so it is independently shippable.
|
|
77
|
+
- **Send helper** depends on the transport seam landing first, so it is *not*
|
|
78
|
+
independent. It stays its own slice only because the retry/backoff logic is a
|
|
79
|
+
large, high-blast-radius surface worth isolating in its own reviewable PR
|
|
80
|
+
(risk isolation). Absent that justification it would fold into Transport
|
|
81
|
+
seam — a bare "depends on the previous slice" is not a reason to keep it
|
|
82
|
+
separate.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The consolidation pass degrades gracefully when this section is absent (it
|
|
86
|
+
falls back to cohesion + single-Story-Feature rules only), so authoring it is
|
|
87
|
+
how the Architect steers the decomposition toward fewer, right-sized Stories.
|
|
88
|
+
Because the count is a **ceiling**, an over-sliced table is coarsened back
|
|
89
|
+
during consolidation — but only where the extra slices are dependent
|
|
90
|
+
single-consumer chains, so an unjustified `No` slice is the one you should fold
|
|
91
|
+
in yourself rather than leaning on the consolidator to catch.
|