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
package/.agents/README.md
CHANGED
|
@@ -216,6 +216,47 @@ If your AI tool is not Claude Code, load
|
|
|
216
216
|
[`instructions.md`](instructions.md) verbatim through that tool's own
|
|
217
217
|
system-prompt mechanism (`.cursorrules`, Custom Instructions, etc.).
|
|
218
218
|
|
|
219
|
+
### What to always-load vs read on-demand
|
|
220
|
+
|
|
221
|
+
The always-loaded context is re-paid on every session **and every subagent
|
|
222
|
+
spawn**, so the shipped set is kept deliberately lean. Load this core into your
|
|
223
|
+
system prompt; read everything else only when the task engages it (the same
|
|
224
|
+
read-when-relevant pattern skills use).
|
|
225
|
+
|
|
226
|
+
**Always-load (the recommended core):**
|
|
227
|
+
|
|
228
|
+
- [`instructions.md`](instructions.md) — the core agent protocol.
|
|
229
|
+
- [`personas/engineer.md`](personas/engineer.md) — the default persona (swap in
|
|
230
|
+
another persona when the task calls for it).
|
|
231
|
+
- [`rules/security-baseline.md`](rules/security-baseline.md) — inviolable
|
|
232
|
+
security MUSTs, relevant to every change.
|
|
233
|
+
- [`rules/git-conventions.md`](rules/git-conventions.md) — every commit,
|
|
234
|
+
branch, and PR touches it.
|
|
235
|
+
|
|
236
|
+
**Read on-demand (do the read before the matching work):**
|
|
237
|
+
|
|
238
|
+
- [`rules/shell-conventions.md`](rules/shell-conventions.md) — before chaining
|
|
239
|
+
shell commands or writing cross-platform command strings.
|
|
240
|
+
- [`rules/testing-standards.md`](rules/testing-standards.md) — before authoring
|
|
241
|
+
or restructuring tests.
|
|
242
|
+
- [`rules/orchestration-error-handling.md`](rules/orchestration-error-handling.md)
|
|
243
|
+
— before writing or modifying orchestration scripts under
|
|
244
|
+
`.agents/scripts/**`.
|
|
245
|
+
- The remaining domain rules
|
|
246
|
+
([`rules/api-conventions.md`](rules/api-conventions.md),
|
|
247
|
+
[`rules/gherkin-standards.md`](rules/gherkin-standards.md),
|
|
248
|
+
[`rules/changelog-style.md`](rules/changelog-style.md),
|
|
249
|
+
[`rules/test-seams.md`](rules/test-seams.md)) — when the task is in that
|
|
250
|
+
domain.
|
|
251
|
+
- Every `SKILL.md` under [`skills/`](skills/) — when the task hits its trigger.
|
|
252
|
+
- [`docs/execution-reference.md`](docs/execution-reference.md) — log-level and
|
|
253
|
+
token-budget reference detail lifted out of `instructions.md`.
|
|
254
|
+
|
|
255
|
+
Each on-demand rule opens with a one-line "this rule applies when…" scope
|
|
256
|
+
header, so a quick skim of its first paragraph tells you whether it governs the
|
|
257
|
+
task at hand. `instructions.md` § 1.F is the canonical in-prompt statement of
|
|
258
|
+
this split.
|
|
259
|
+
|
|
219
260
|
---
|
|
220
261
|
|
|
221
262
|
## Interactive repo / project pickers
|
package/.agents/docs/SDLC.md
CHANGED
|
@@ -1172,8 +1172,10 @@ non-QA consumer) but enforced at run time: the resolver fails **loudly** with
|
|
|
1172
1172
|
"this project has not bound the QA harness" when no `qa` block is present —
|
|
1173
1173
|
there is no silent fallback. The contract's four required keys are
|
|
1174
1174
|
`qa.featureRoot` (the `.feature` discovery root), `qa.fixturesManifest`
|
|
1175
|
-
(persona → seed-data binding), `qa.
|
|
1176
|
-
|
|
1175
|
+
(persona → seed-data binding), `qa.environments` (the environment-keyed map —
|
|
1176
|
+
each entry `{ baseUrl, signInSeam, allowWrites? }`, selected per invocation by
|
|
1177
|
+
`resolveQaEnvironment`; the per-environment `signInSeam` is either
|
|
1178
|
+
`{ urlTemplate }` or `{ skill }`), and `qa.personas` (the persona set,
|
|
1177
1179
|
authored as a name-only array under a url-template seam or as a per-persona
|
|
1178
1180
|
credential/skill map under a skill seam); the two optional keys
|
|
1179
1181
|
`qa.consoleAllowlist` and `qa.designTokens` default to `[]` and `null`.
|
|
@@ -264,8 +264,16 @@
|
|
|
264
264
|
"qa": {
|
|
265
265
|
"featureRoot": "tests/features",
|
|
266
266
|
"fixturesManifest": "tests/fixtures/personas.json",
|
|
267
|
-
"
|
|
268
|
-
"
|
|
267
|
+
"environments": {
|
|
268
|
+
"local": {
|
|
269
|
+
"baseUrl": "http://localhost:3000",
|
|
270
|
+
"signInSeam": { "urlTemplate": "/dev/sign-in-as/{persona}" }
|
|
271
|
+
},
|
|
272
|
+
"staging": {
|
|
273
|
+
"baseUrl": "https://staging.example.test",
|
|
274
|
+
"signInSeam": { "skill": "stack/qa/sign-in" },
|
|
275
|
+
"allowWrites": false
|
|
276
|
+
}
|
|
269
277
|
},
|
|
270
278
|
"personas": {
|
|
271
279
|
"admin": { "credentialRef": "QA_ADMIN_CREDENTIAL" },
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Execution Reference (on-demand)
|
|
2
|
+
|
|
3
|
+
Reference-only material extracted from
|
|
4
|
+
[`.agents/instructions.md`](../instructions.md) so the always-loaded system
|
|
5
|
+
prompt stays lean (Story #4332). Nothing here is a per-task MUST — it is
|
|
6
|
+
detail an agent consults **only when the relevant lever is in play** (tuning
|
|
7
|
+
log verbosity, reasoning about the token budget). The always-loaded protocol
|
|
8
|
+
links here from the sections that used to inline this content.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Log-level control
|
|
13
|
+
|
|
14
|
+
The orchestrator logger (`lib/Logger.js`) emits progress/trace output based on
|
|
15
|
+
the `AGENT_LOG_LEVEL` environment variable:
|
|
16
|
+
|
|
17
|
+
- `silent` — only `fatal` emits; useful for script embedding where the caller
|
|
18
|
+
owns presentation.
|
|
19
|
+
- `info` — default. Emits `info` / `warn` / `error` / `fatal`.
|
|
20
|
+
- `verbose` — adds `debug` trace output on top of the `info` set. `debug` is
|
|
21
|
+
accepted as a backward-compatible alias.
|
|
22
|
+
|
|
23
|
+
This is a diagnostic knob: set it when you need quieter script embedding
|
|
24
|
+
(`silent`) or a deeper trace (`verbose`). The friction-telemetry MUST it sits
|
|
25
|
+
under — post friction to the relevant ticket via `diagnose-friction.js` — stays
|
|
26
|
+
in [`instructions.md` § 1.H](../instructions.md).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## FinOps & token budgeting (economic guardrails)
|
|
31
|
+
|
|
32
|
+
Mandrel does **not** enforce live LLM spend from response metadata. The
|
|
33
|
+
framework limits **hydrated prompt size** and optional **pre-dispatch
|
|
34
|
+
estimates**; your host runtime (editor / CLI) owns session quota and hard
|
|
35
|
+
stops. Consult this section when reasoning about why a task prompt was elided
|
|
36
|
+
or why `/deliver` refused a fan-out on budget grounds.
|
|
37
|
+
|
|
38
|
+
### Token budget (hydration + pre-dispatch estimates)
|
|
39
|
+
|
|
40
|
+
- **`delivery.maxTokenBudget`** (`.agentrc.json`, resolved via
|
|
41
|
+
`lib/config/limits.js`): caps the task prompt built by
|
|
42
|
+
`hydrate-context` / `hydrateContext`. The pipeline uses a rough token
|
|
43
|
+
estimate (≈4 characters per token) and applies section-aware elision
|
|
44
|
+
(`elideEnvelope`) so oversized envelopes drop or summarize lower-priority
|
|
45
|
+
sections before you receive the prompt.
|
|
46
|
+
- **`delivery.preflight.*`** (optional): before `/deliver` fan-out,
|
|
47
|
+
`epic-deliver-preflight.js` compares **estimated** story count, waves,
|
|
48
|
+
install time, GitHub API volume, and Claude quota tokens against configured
|
|
49
|
+
ceilings (`maxClaudeQuotaTokens`, etc.). A breach surfaces via
|
|
50
|
+
`agent::blocked`; there is no per-tool-call metering.
|
|
51
|
+
- **Host runtime**: session billing, quota exhaustion, and operator overrides
|
|
52
|
+
are enforced by your provider (e.g. Claude Code), not by Mandrel scripts.
|
package/.agents/instructions.md
CHANGED
|
@@ -87,10 +87,39 @@ local copy is ignored with a `shadowed` warning).
|
|
|
87
87
|
|
|
88
88
|
### F. Modular Global Rules
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
The `.agents/rules/` directory is split into an **always-on core** and an
|
|
91
|
+
**on-demand set** — the same read-when-relevant pattern skills use (§ 1.B).
|
|
92
|
+
The core loads into every session; the on-demand rules are read only when the
|
|
93
|
+
task actually engages them, so a generic task (and every subagent it spawns)
|
|
94
|
+
does not re-pay their bytes on every turn.
|
|
95
|
+
|
|
96
|
+
- **Always-on core** (loaded alongside this file):
|
|
97
|
+
- [`rules/security-baseline.md`](rules/security-baseline.md) — inviolable
|
|
98
|
+
security MUSTs; applies to every piece of code generated.
|
|
99
|
+
- [`rules/git-conventions.md`](rules/git-conventions.md) — every commit,
|
|
100
|
+
branch, and PR touches it.
|
|
101
|
+
|
|
102
|
+
- **On-demand** — read the file **before** doing the matching work; each opens
|
|
103
|
+
with a one-line "this rule applies when…" scope header, so skimming its first
|
|
104
|
+
paragraph confirms whether it governs the task at hand:
|
|
105
|
+
- [`rules/shell-conventions.md`](rules/shell-conventions.md) — before
|
|
106
|
+
chaining shell commands or writing cross-platform command strings.
|
|
107
|
+
- [`rules/testing-standards.md`](rules/testing-standards.md) — before
|
|
108
|
+
authoring or restructuring tests (the three-tier pyramid, assertion
|
|
109
|
+
placement, mocking/isolation MUSTs).
|
|
110
|
+
- [`rules/orchestration-error-handling.md`](rules/orchestration-error-handling.md)
|
|
111
|
+
— before writing or modifying orchestration scripts under
|
|
112
|
+
`.agents/scripts/**`.
|
|
113
|
+
- [`rules/api-conventions.md`](rules/api-conventions.md),
|
|
114
|
+
[`rules/gherkin-standards.md`](rules/gherkin-standards.md),
|
|
115
|
+
[`rules/changelog-style.md`](rules/changelog-style.md),
|
|
116
|
+
[`rules/test-seams.md`](rules/test-seams.md) — when the task is in that
|
|
117
|
+
domain (API surface, Gherkin scenarios, changelog prose, test seams).
|
|
118
|
+
|
|
119
|
+
When in doubt, read the rule — the read is cheap relative to shipping a
|
|
120
|
+
MUST-violating change. Precedence between a rule and any other governance
|
|
121
|
+
document is unchanged (§ 1.K): loading a rule on demand does not lower its
|
|
122
|
+
authority.
|
|
94
123
|
|
|
95
124
|
### G. Structured Configuration
|
|
96
125
|
|
|
@@ -123,14 +152,9 @@ GitHub Story (or Epic) ticket:
|
|
|
123
152
|
|
|
124
153
|
#### Log Level Control
|
|
125
154
|
|
|
126
|
-
The orchestrator logger (`
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- `silent` — only `fatal` emits; useful for script embedding where the
|
|
130
|
-
caller owns presentation.
|
|
131
|
-
- `info` — default. Emits `info` / `warn` / `error` / `fatal`.
|
|
132
|
-
- `verbose` — adds `debug` trace output on top of the `info` set. `debug` is
|
|
133
|
-
accepted as a backward-compatible alias.
|
|
155
|
+
The orchestrator logger honors `AGENT_LOG_LEVEL` (`silent` / `info` /
|
|
156
|
+
`verbose`). The per-level emission table is reference detail — see
|
|
157
|
+
[`docs/execution-reference.md` § Log-level control](docs/execution-reference.md#log-level-control).
|
|
134
158
|
|
|
135
159
|
### I. Anti-Thrashing Protocol
|
|
136
160
|
|
|
@@ -217,27 +241,14 @@ Two carve-outs refine the ordering:
|
|
|
217
241
|
|
|
218
242
|
## 2. FinOps & Token Budgeting (Economic Guardrails)
|
|
219
243
|
|
|
220
|
-
Mandrel does **not** enforce live LLM spend from response metadata.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
stops.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
`lib/config/limits.js`): caps the task prompt built by
|
|
229
|
-
`hydrate-context` / `hydrateContext`. The pipeline uses a rough token
|
|
230
|
-
estimate (≈4 characters per token) and applies section-aware elision
|
|
231
|
-
(`elideEnvelope`) so oversized envelopes drop or summarize
|
|
232
|
-
lower-priority sections before you receive the prompt.
|
|
233
|
-
- **`delivery.preflight.*`** (optional): before `/deliver` fan-out,
|
|
234
|
-
`epic-deliver-preflight.js` compares **estimated** story count, waves,
|
|
235
|
-
install time, GitHub API volume, and Claude quota tokens against
|
|
236
|
-
configured ceilings (`maxClaudeQuotaTokens`, etc.). A breach surfaces
|
|
237
|
-
via `agent::blocked`; there is no per-tool-call metering.
|
|
238
|
-
- **Host runtime**: session billing, quota exhaustion, and operator
|
|
239
|
-
overrides are enforced by your provider (e.g. Claude Code), not by
|
|
240
|
-
Mandrel scripts.
|
|
244
|
+
Mandrel does **not** enforce live LLM spend from response metadata. It caps
|
|
245
|
+
**hydrated prompt size** (`delivery.maxTokenBudget`, section-aware elision) and
|
|
246
|
+
runs optional **pre-dispatch estimates** (`delivery.preflight.*`); your host
|
|
247
|
+
runtime owns session quota and hard stops. The config keys, the ≈4-char/token
|
|
248
|
+
estimate, and the elision behaviour are reference detail — see
|
|
249
|
+
[`docs/execution-reference.md` § FinOps & token budgeting](docs/execution-reference.md#finops--token-budgeting-economic-guardrails).
|
|
250
|
+
Consult it when a task prompt was elided or `/deliver` refused a fan-out on
|
|
251
|
+
budget grounds.
|
|
241
252
|
|
|
242
253
|
---
|
|
243
254
|
|
|
@@ -245,8 +256,9 @@ stops.
|
|
|
245
256
|
|
|
246
257
|
1. **Context First:** Before proposing any solution, understand the
|
|
247
258
|
repository's tech stack, historical context, and structure.
|
|
248
|
-
- **Mandatory Reading
|
|
249
|
-
|
|
259
|
+
- **Mandatory Reading (planning & interactive tasks)**: For planning
|
|
260
|
+
(`/plan`) and interactive tasks, before starting ANY work you MUST read
|
|
261
|
+
every file listed in `project.docsContextFiles` in `.agentrc.json`.
|
|
250
262
|
This list is the project's authoritative reference set (architecture,
|
|
251
263
|
data dictionary, decisions log, patterns, etc.) and replaces any
|
|
252
264
|
hardcoded filename list. Resolve each entry against
|
|
@@ -258,6 +270,23 @@ stops.
|
|
|
258
270
|
When it is an index, only the index is the mandatory-read; the
|
|
259
271
|
per-ADR bodies under `decisions/` are link-followed on demand
|
|
260
272
|
(index-only by default), not auto-loaded into every task's context.
|
|
273
|
+
- **Digest-first Reading (`/deliver` story sub-agents)**: A `/deliver`
|
|
274
|
+
Story delivery sub-agent (dispatched via `helpers/epic-deliver-story` or
|
|
275
|
+
`helpers/single-story-deliver`) does **not** re-read the full
|
|
276
|
+
`project.docsContextFiles` set per Story. Instead it reads the **per-Epic
|
|
277
|
+
docs digest** — a single compact outline (path, byte size, heading
|
|
278
|
+
outline with line numbers, and the first paragraph under each `##`) that
|
|
279
|
+
`epic-deliver-prepare.js` writes to
|
|
280
|
+
`temp/epic-<epicId>/docs-digest.md` and the parent threads into the
|
|
281
|
+
child prompt as `docsDigestPath`. Use the digest to decide which docs are
|
|
282
|
+
relevant to the Story at hand, then **pull the full file on demand**
|
|
283
|
+
(reading the section at the line number the digest names) when a section
|
|
284
|
+
bears on the change. When `docsDigestPath` is null (the project has no
|
|
285
|
+
`project.docsContextFiles` configured) there is no digest to read and no
|
|
286
|
+
per-Story docs mandate — read a full doc only if the Story's own context
|
|
287
|
+
points you at one. This is the hard cutover from the former
|
|
288
|
+
read-every-file-per-Story rule: delivery children no longer ingest the
|
|
289
|
+
whole docs set up front.
|
|
261
290
|
- **Conditional Reads**: When the task touches UI copy, layout, or
|
|
262
291
|
routing and the corresponding file is present in the project, also
|
|
263
292
|
read `docs/style-guide.md` and `docs/web-routes.md`. Skip both when
|
|
@@ -289,9 +318,18 @@ stops.
|
|
|
289
318
|
|
|
290
319
|
- **Re-Plan on Failure:** If a strategy fails, **STOP** and re-plan
|
|
291
320
|
immediately. Do not repeat a broken approach.
|
|
292
|
-
- **Subagent Strategy:**
|
|
293
|
-
|
|
294
|
-
|
|
321
|
+
- **Subagent Strategy:** Spawning a subagent is not free — each spawn
|
|
322
|
+
re-pays the full always-loaded context, so treat it as a cost decision,
|
|
323
|
+
not a reflex. Prefer an **inline search** (grep, a targeted read) for
|
|
324
|
+
small or localized lookups where you already know roughly where to look;
|
|
325
|
+
reach for a subagent **only when the work is large enough to justify
|
|
326
|
+
replicating context** — a broad multi-file investigation, a parallel
|
|
327
|
+
exploration front, or an isolated task that would otherwise crowd the main
|
|
328
|
+
context window. One objective per subagent. When the host exposes a
|
|
329
|
+
cheaper or faster capability, prefer it for **mechanical or read-only**
|
|
330
|
+
spawns (search, doc regeneration, lint, log triage) and keep
|
|
331
|
+
**implementation and design** work on the default capability; name no
|
|
332
|
+
specific model — let the host and operator own the concrete mapping.
|
|
295
333
|
- **Anti-Laziness:** NEVER use placeholder comments like
|
|
296
334
|
`// ... existing code ...`, `/* rest of file */`, or
|
|
297
335
|
`// implementation here`. You MUST output the ENTIRE file or the ENTIRE
|
|
@@ -161,6 +161,20 @@ assertions into a single "kitchen sink" test — split them.
|
|
|
161
161
|
- Coverage targets apply to production code. Test helpers, fixtures, and
|
|
162
162
|
generated code are excluded per the project's coverage config.
|
|
163
163
|
|
|
164
|
+
## Anti-Gaming (review-side complement)
|
|
165
|
+
|
|
166
|
+
These standards define what a *correct* test looks like; they cannot, on
|
|
167
|
+
their own, catch a change that reaches green by **weakening the check rather
|
|
168
|
+
than fixing the code** — a relaxed assertion, a skipped or deleted test, a
|
|
169
|
+
swallowed error, a stub return, a fake rename, or a warning silenced by
|
|
170
|
+
comment deletion. That shortcut taxonomy is enumerated, and the reviewer-facing
|
|
171
|
+
detection lens for it lives, in the **Anti-Gaming / Shortcut Detection** pillar
|
|
172
|
+
(Pillar 4) of
|
|
173
|
+
[`../workflows/helpers/code-review.md`](../workflows/helpers/code-review.md#pillar-4-anti-gaming--shortcut-detection).
|
|
174
|
+
When you loosen a matcher, quarantine a test, or remove coverage, record the
|
|
175
|
+
spec-sanctioned rationale in the commit body or Story comment so that pillar
|
|
176
|
+
reads it as a deliberate decision rather than gaming.
|
|
177
|
+
|
|
164
178
|
## Property-Based Testing (a technique, not a tier)
|
|
165
179
|
|
|
166
180
|
Property-based testing is a **technique** — generating a domain of inputs and
|
|
@@ -1512,6 +1512,34 @@
|
|
|
1512
1512
|
}
|
|
1513
1513
|
]
|
|
1514
1514
|
},
|
|
1515
|
+
"qaEnvironments": {
|
|
1516
|
+
"description": "Deployment targets the QA harness can run against (Epic #4326). A map keyed by environment name (e.g. `local`, `staging`), each carrying its own `baseUrl`, its own per-environment sign-in seam (the same url-template/skill union as `qaSignInSeam`), and an optional `allowWrites` gate. resolveQaEnvironment selects one environment per invocation by name or by raw-URL origin match against `baseUrl`; `allowWrites` defaults to true only for the `local` environment. Replaces the retired top-level single `signInSeam`.",
|
|
1517
|
+
"type": "object",
|
|
1518
|
+
"minProperties": 1,
|
|
1519
|
+
"additionalProperties": {
|
|
1520
|
+
"type": "object",
|
|
1521
|
+
"required": ["baseUrl", "signInSeam"],
|
|
1522
|
+
"additionalProperties": false,
|
|
1523
|
+
"properties": {
|
|
1524
|
+
"baseUrl": {
|
|
1525
|
+
"allOf": [
|
|
1526
|
+
{
|
|
1527
|
+
"$ref": "#/$defs/safeString"
|
|
1528
|
+
},
|
|
1529
|
+
{
|
|
1530
|
+
"minLength": 1
|
|
1531
|
+
}
|
|
1532
|
+
]
|
|
1533
|
+
},
|
|
1534
|
+
"signInSeam": {
|
|
1535
|
+
"$ref": "#/$defs/qaSignInSeam"
|
|
1536
|
+
},
|
|
1537
|
+
"allowWrites": {
|
|
1538
|
+
"type": "boolean"
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
},
|
|
1515
1543
|
"qaPersonas": {
|
|
1516
1544
|
"description": "Personas the QA-harness sign-in seam accepts. Two accepted shapes: (1) a plain array of persona names \u2014 the honest shape for a `urlTemplate` dev-impersonation seam, where the persona name is the sole input the workflow consumes; (2) the object-map form keyed by persona name, where each entry carries per-persona auth material (`credentialRef` or `signInSkill`) consulted only under a `skill`/credential seam.",
|
|
1517
1545
|
"oneOf": [
|
|
@@ -1575,7 +1603,7 @@
|
|
|
1575
1603
|
},
|
|
1576
1604
|
"qa": {
|
|
1577
1605
|
"type": "object",
|
|
1578
|
-
"description": "Agent-driven QA harness contract (Epic #3214). Optional top-level block. All filesystem-pointer fields (featureRoot, fixturesManifest, designTokens) carry safeString guards rejecting shell-injection metacharacters.
|
|
1606
|
+
"description": "Agent-driven QA harness contract (Epic #3214; environment-keyed by Epic #4326). Optional top-level block. All filesystem-pointer fields (featureRoot, fixturesManifest, designTokens) carry safeString guards rejecting shell-injection metacharacters. environments is a map of named deployment targets (each with a baseUrl, a per-environment url-template/skill sign-in seam, and an optional allowWrites gate); personas resolve to a stored credential reference or a sign-in skill, never an inline secret.",
|
|
1579
1607
|
"additionalProperties": false,
|
|
1580
1608
|
"properties": {
|
|
1581
1609
|
"featureRoot": {
|
|
@@ -1598,8 +1626,8 @@
|
|
|
1598
1626
|
}
|
|
1599
1627
|
]
|
|
1600
1628
|
},
|
|
1601
|
-
"
|
|
1602
|
-
"$ref": "#/$defs/
|
|
1629
|
+
"environments": {
|
|
1630
|
+
"$ref": "#/$defs/qaEnvironments"
|
|
1603
1631
|
},
|
|
1604
1632
|
"personas": {
|
|
1605
1633
|
"$ref": "#/$defs/qaPersonas"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"$id": "https://mandrel.dev/schemas/qa-ledger.schema.json",
|
|
4
4
|
"title": "QaLedgerItem",
|
|
5
|
-
"description": "Structured exploratory-QA ledger item (`f3-qa-explore-core`, Feature #3710, Epic #3686). One object per observation captured while exploring a consumer surface —
|
|
5
|
+
"description": "Structured exploratory-QA ledger item (`f3-qa-explore-core`, Feature #3710, Epic #3686). One object per observation captured while exploring a consumer surface, and — since Story #4330 unified `/qa-run` onto this ledger — one object per browser-sweep `F#` finding as well: a ledger item classifies an exploratory or sweep signal (product bug, environment-setup friction, tooling/DX gap, test-coverage gap, or enhancement idea), records the evidence and coverage context, names any missing test, and carries the operator disposition so Triage can parse a whole session. A ledger item has a two-phase lifecycle: the read-only **Capture** phase appends an item with the Capture-phase fields populated (`id`, `class`, `severity`, `evidence`, `coverage`, `missingTest`) but no resolved `disposition`; the **Triage** phase later assigns the `disposition`. The schema therefore validates BOTH the captured-but-untriaged shape (`disposition` absent, `null`, or a `pending`/`untriaged` sentinel — the rolling backlog `.agents/scripts/lib/qa/qa-session.js` reads back on resume) AND the fully-triaged shape (`disposition` resolved to `file`/`defer`/`dismiss`), while still rejecting genuinely malformed items. Evidence MUST already be scrubbed of secrets, session tokens, and PII per `.agents/rules/security-baseline.md` before it is rendered here.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"required": [
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
"routedTo": {
|
|
66
66
|
"type": "object",
|
|
67
|
-
"description": "Optional finding-to-issue link recording where Triage routed this item. Populated only after the item is promoted to a follow-up ticket (`disposition: file`); omit while the item is still in Capture or was deferred/dismissed.
|
|
67
|
+
"description": "Optional finding-to-issue link recording where Triage routed this item. Populated only after the item is promoted to a follow-up ticket (`disposition: file`); omit while the item is still in Capture or was deferred/dismissed.",
|
|
68
68
|
"additionalProperties": false,
|
|
69
69
|
"required": ["issue", "url", "kind"],
|
|
70
70
|
"properties": {
|
|
@@ -32,13 +32,16 @@
|
|
|
32
32
|
* node .agents/scripts/epic-deliver-prepare.js --epic <epicId>
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
+
import fs from 'node:fs';
|
|
36
|
+
import path from 'node:path';
|
|
35
37
|
import { parseArgs } from 'node:util';
|
|
36
38
|
|
|
37
39
|
import { runAsCli } from './lib/cli-utils.js';
|
|
38
|
-
import { getRunners, resolveConfig } from './lib/config-resolver.js';
|
|
40
|
+
import { getPaths, getRunners, resolveConfig } from './lib/config-resolver.js';
|
|
39
41
|
import { currentBranch as gitCurrentBranch } from './lib/git-branch-lifecycle.js';
|
|
40
42
|
import { getEpicBranch, gitSpawn } from './lib/git-utils.js';
|
|
41
43
|
import { Logger } from './lib/Logger.js';
|
|
44
|
+
import { buildDocsDigest } from './lib/orchestration/docs-digest.js';
|
|
42
45
|
import {
|
|
43
46
|
resolveOperator,
|
|
44
47
|
runPrepareGuards,
|
|
@@ -286,6 +289,40 @@ function evaluatePrepareConcurrencyGate({
|
|
|
286
289
|
return gate;
|
|
287
290
|
}
|
|
288
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Build the per-Epic docs digest and write it to
|
|
294
|
+
* `<tempRoot>/epic-<id>/docs-digest.md`, returning its repo-relative path.
|
|
295
|
+
* Story #4338 — the parent threads this path into every child prompt so
|
|
296
|
+
* delivery sub-agents read one compact outline instead of re-ingesting the
|
|
297
|
+
* full `project.docsContextFiles` set per Story.
|
|
298
|
+
*
|
|
299
|
+
* Keyed off the **un-defaulted** config (`config.raw`): when the operator has
|
|
300
|
+
* not configured `project.docsContextFiles`, this returns `null` (no file
|
|
301
|
+
* written) rather than digesting the resolver's built-in default set — the
|
|
302
|
+
* digest is an opt-in surface for projects that curate their docs context.
|
|
303
|
+
*
|
|
304
|
+
* @param {{ epicId: number, cwd?: string, config: object }} args
|
|
305
|
+
* @returns {Promise<string|null>} repo-relative digest path, or null when
|
|
306
|
+
* `project.docsContextFiles` is empty/unset (or every file is missing).
|
|
307
|
+
*/
|
|
308
|
+
async function writeDocsDigest({ epicId, cwd, config }) {
|
|
309
|
+
const rawFiles = config?.raw?.project?.docsContextFiles;
|
|
310
|
+
const docsContextFiles = Array.isArray(rawFiles) ? rawFiles : [];
|
|
311
|
+
if (docsContextFiles.length === 0) return null;
|
|
312
|
+
|
|
313
|
+
const paths = getPaths(config);
|
|
314
|
+
const root = path.resolve(cwd ?? process.cwd());
|
|
315
|
+
const docsRoot = path.resolve(root, paths.docsRoot);
|
|
316
|
+
const digest = await buildDocsDigest({ docsContextFiles, docsRoot });
|
|
317
|
+
if (digest == null) return null;
|
|
318
|
+
|
|
319
|
+
const relPath = path.join(paths.tempRoot, `epic-${epicId}`, 'docs-digest.md');
|
|
320
|
+
const absPath = path.resolve(root, relPath);
|
|
321
|
+
await fs.promises.mkdir(path.dirname(absPath), { recursive: true });
|
|
322
|
+
await fs.promises.writeFile(absPath, digest, 'utf-8');
|
|
323
|
+
return relPath;
|
|
324
|
+
}
|
|
325
|
+
|
|
289
326
|
export async function runEpicDeliverPrepare({
|
|
290
327
|
epicId,
|
|
291
328
|
cwd,
|
|
@@ -374,6 +411,8 @@ export async function runEpicDeliverPrepare({
|
|
|
374
411
|
});
|
|
375
412
|
}
|
|
376
413
|
|
|
414
|
+
const docsDigestPath = await writeDocsDigest({ epicId, cwd, config });
|
|
415
|
+
|
|
377
416
|
return {
|
|
378
417
|
epicId,
|
|
379
418
|
storyCount: openStories.length,
|
|
@@ -385,6 +424,7 @@ export async function runEpicDeliverPrepare({
|
|
|
385
424
|
new Date().toISOString(),
|
|
386
425
|
concurrencyHazardsBypassed: gate.bypassed,
|
|
387
426
|
preflightCache: cacheStatus,
|
|
427
|
+
docsDigestPath,
|
|
388
428
|
};
|
|
389
429
|
}
|
|
390
430
|
|
|
@@ -232,7 +232,10 @@ const PREFIX_MEANINGS = Object.freeze([
|
|
|
232
232
|
'planning.taskSizing',
|
|
233
233
|
'Story-sizing threshold for the decompose validator.',
|
|
234
234
|
],
|
|
235
|
-
[
|
|
235
|
+
[
|
|
236
|
+
'qa.environments',
|
|
237
|
+
'QA harness deployment target (baseUrl, per-environment sign-in seam, allowWrites gate).',
|
|
238
|
+
],
|
|
236
239
|
['qa.personas', 'QA harness persona / credential mapping.'],
|
|
237
240
|
]);
|
|
238
241
|
|
|
@@ -406,12 +406,36 @@ const QA_PERSONAS_SCHEMA = {
|
|
|
406
406
|
],
|
|
407
407
|
};
|
|
408
408
|
|
|
409
|
+
// `environments` is the environment-keyed contract (Epic #4326, Story #4327).
|
|
410
|
+
// It replaces the retired top-level single `signInSeam` shape: each named
|
|
411
|
+
// environment carries its own `baseUrl`, its own per-environment `signInSeam`
|
|
412
|
+
// (reusing the same url-template/skill union), and an optional `allowWrites`
|
|
413
|
+
// gate. Downstream, `resolveQaEnvironment` selects one environment per
|
|
414
|
+
// invocation by name or by raw-URL origin match against `baseUrl`. The map
|
|
415
|
+
// must carry at least one environment. This is a hard cutover — there is no
|
|
416
|
+
// top-level `signInSeam` acceptance branch (see
|
|
417
|
+
// `.agents/rules/git-conventions.md` § Contract Cutovers).
|
|
418
|
+
const QA_ENVIRONMENTS_SCHEMA = {
|
|
419
|
+
type: 'object',
|
|
420
|
+
minProperties: 1,
|
|
421
|
+
additionalProperties: {
|
|
422
|
+
type: 'object',
|
|
423
|
+
properties: {
|
|
424
|
+
baseUrl: { ...SAFE_STRING, minLength: 1 },
|
|
425
|
+
signInSeam: QA_SIGN_IN_SEAM_SCHEMA,
|
|
426
|
+
allowWrites: { type: 'boolean' },
|
|
427
|
+
},
|
|
428
|
+
required: ['baseUrl', 'signInSeam'],
|
|
429
|
+
additionalProperties: false,
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
|
|
409
433
|
export const QA_SCHEMA = {
|
|
410
434
|
type: 'object',
|
|
411
435
|
properties: {
|
|
412
436
|
featureRoot: { ...SAFE_STRING, minLength: 1 },
|
|
413
437
|
fixturesManifest: { ...SAFE_STRING, minLength: 1 },
|
|
414
|
-
|
|
438
|
+
environments: QA_ENVIRONMENTS_SCHEMA,
|
|
415
439
|
personas: QA_PERSONAS_SCHEMA,
|
|
416
440
|
consoleAllowlist: {
|
|
417
441
|
type: 'array',
|
|
@@ -198,6 +198,94 @@ export function hasTechSpecContent(body) {
|
|
|
198
198
|
return typeof body === 'string' && DELIVERY_SLICING_RE.test(body);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* `##` headings dropped from the delivery-hydrated Epic body. These are
|
|
203
|
+
* ideation / authoring / close-machinery sections a story agent never acts
|
|
204
|
+
* on: keeping them out of the per-Story prompt trims token cost without
|
|
205
|
+
* losing any binding context (the Story carries its own inline
|
|
206
|
+
* acceptance[] / verify[]).
|
|
207
|
+
*
|
|
208
|
+
* @type {ReadonlySet<string>}
|
|
209
|
+
*/
|
|
210
|
+
const DELIVERY_DROP_HEADINGS = new Set([
|
|
211
|
+
'context',
|
|
212
|
+
'scope',
|
|
213
|
+
'acceptance criteria',
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Slice an Epic body down to the sections a delivery story agent acts on.
|
|
218
|
+
*
|
|
219
|
+
* KEEP: the Epic title / preamble before the first `##`, `## Goal`,
|
|
220
|
+
* `## Non-Goals`, `## User Stories`, the `techSpec` managed region, and —
|
|
221
|
+
* fail-open — any unknown / operator-authored `##` section not in the drop
|
|
222
|
+
* list. DROP: `## Context`, `## Scope`, `## Acceptance Criteria`, and the
|
|
223
|
+
* `acceptanceTable` managed region.
|
|
224
|
+
*
|
|
225
|
+
* The `techSpec` region is located by its markers (not by heading
|
|
226
|
+
* boundaries) so its inner `## Delivery Slicing` heading is preserved
|
|
227
|
+
* verbatim and never mistaken for a plain droppable section. Plain `##`
|
|
228
|
+
* sections outside the managed regions are sliced by heading boundaries.
|
|
229
|
+
*
|
|
230
|
+
* Fail-open is load-bearing: any heading not explicitly in the drop set is
|
|
231
|
+
* kept, so operator-authored content is never silently lost.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} body
|
|
234
|
+
* @returns {string}
|
|
235
|
+
*/
|
|
236
|
+
export function sliceEpicBodyForDelivery(body) {
|
|
237
|
+
if (typeof body !== 'string' || body.length === 0) return '';
|
|
238
|
+
|
|
239
|
+
// 1. Drop the acceptance-table managed region outright (authoring/close
|
|
240
|
+
// machinery, never delivery context).
|
|
241
|
+
let working = stripEpicSection(body, 'acceptanceTable');
|
|
242
|
+
|
|
243
|
+
// 2. Protect the techSpec managed region from heading-boundary slicing by
|
|
244
|
+
// lifting it out behind an opaque placeholder, then restoring it after
|
|
245
|
+
// the plain-section pass. Its inner `## Delivery Slicing` heading must
|
|
246
|
+
// survive verbatim.
|
|
247
|
+
const techLoc = locate(working, 'techSpec');
|
|
248
|
+
let techRegion = null;
|
|
249
|
+
const PLACEHOLDER = 'MANDREL_TECH_SPEC_PLACEHOLDER';
|
|
250
|
+
if (techLoc) {
|
|
251
|
+
const { end } = descriptor('techSpec');
|
|
252
|
+
techRegion = working.slice(techLoc.startIdx, techLoc.endIdx + end.length);
|
|
253
|
+
working =
|
|
254
|
+
working.slice(0, techLoc.startIdx) +
|
|
255
|
+
PLACEHOLDER +
|
|
256
|
+
working.slice(techLoc.endIdx + end.length);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 3. Slice plain `##` sections by heading boundaries, dropping only the
|
|
260
|
+
// known ideation/authoring headings. The preamble before the first
|
|
261
|
+
// `##` (Epic title / lede) is always kept.
|
|
262
|
+
const kept = [];
|
|
263
|
+
let dropping = false;
|
|
264
|
+
for (const line of working.split('\n')) {
|
|
265
|
+
const headingMatch = line.match(/^##\s+(.*?)\s*$/);
|
|
266
|
+
if (headingMatch) {
|
|
267
|
+
dropping = DELIVERY_DROP_HEADINGS.has(
|
|
268
|
+
headingMatch[1].trim().toLowerCase(),
|
|
269
|
+
);
|
|
270
|
+
if (dropping) continue;
|
|
271
|
+
}
|
|
272
|
+
if (dropping) continue;
|
|
273
|
+
kept.push(line);
|
|
274
|
+
}
|
|
275
|
+
working = kept.join('\n');
|
|
276
|
+
|
|
277
|
+
// 4. Restore the techSpec region in place.
|
|
278
|
+
if (techRegion !== null) {
|
|
279
|
+
working = working.replace(PLACEHOLDER, () => techRegion);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 5. Normalise the blank-line runs left by the drops.
|
|
283
|
+
return working
|
|
284
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
285
|
+
.replace(/^\n+/, '')
|
|
286
|
+
.trimEnd();
|
|
287
|
+
}
|
|
288
|
+
|
|
201
289
|
/**
|
|
202
290
|
* Strip the retired machine-managed `## Planning Artifacts` checklist from
|
|
203
291
|
* an Epic body (the section that linked the now-retired context tickets).
|
|
@@ -196,8 +196,8 @@ function clusterToFinding(cluster) {
|
|
|
196
196
|
/**
|
|
197
197
|
* Build the `routedTo` link the schema stamps onto a promoted ledger item.
|
|
198
198
|
*
|
|
199
|
-
* The `routedTo.url` field is `minLength: 1` in
|
|
200
|
-
* and
|
|
199
|
+
* The `routedTo.url` field is `minLength: 1` in `qa-ledger.schema.json`,
|
|
200
|
+
* and the search/create port contract requires a
|
|
201
201
|
* routed issue to carry its canonical URL. So rather than silently stamp an
|
|
202
202
|
* empty string (which would persist a schema-invalid ledger item), this guards
|
|
203
203
|
* the url and throws when it is absent or blank (Story #3816, AC #4).
|
|
@@ -213,7 +213,7 @@ function routedToLink(issue, kind) {
|
|
|
213
213
|
throw new Error(
|
|
214
214
|
`promoteFindings: routed issue #${issue?.number ?? '?'} is missing a url; ` +
|
|
215
215
|
'the search/create port contract requires a non-empty url ' +
|
|
216
|
-
'(routedTo.url is minLength:1 in the qa-
|
|
216
|
+
'(routedTo.url is minLength:1 in the qa-ledger schema)',
|
|
217
217
|
);
|
|
218
218
|
}
|
|
219
219
|
return {
|