mandrel 2.22.0 → 2.24.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 +1 -0
- package/.agents/schemas/agentrc.schema.json +6 -0
- package/.agents/schemas/story-deliver-terminal.schema.json +6 -1
- package/.agents/scripts/deliver-light.js +23 -45
- package/.agents/scripts/diagnose-friction.js +95 -4
- package/.agents/scripts/lib/audit-suite/lens-diff-floor.js +10 -25
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +20 -32
- package/.agents/scripts/lib/config-settings-schema-delivery.js +8 -0
- package/.agents/scripts/lib/escomplex-ast-compat.js +360 -0
- package/.agents/scripts/lib/maintainability-engine.js +83 -11
- package/.agents/scripts/lib/maintainability-unscorable.js +60 -0
- package/.agents/scripts/lib/maintainability-utils.js +14 -5
- package/.agents/scripts/lib/observability/runtime-friction.js +37 -1
- package/.agents/scripts/lib/orchestration/diff-magnitude.js +283 -0
- package/.agents/scripts/lib/orchestration/light-backstop.js +107 -0
- package/.agents/scripts/lib/orchestration/light-escalation.js +169 -0
- package/.agents/scripts/lib/orchestration/light-suitability.js +151 -46
- package/.agents/scripts/lib/orchestration/plan-context.js +12 -13
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +18 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +70 -2
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +23 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +76 -4
- package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
- package/.agents/scripts/lib/workers/maintainability-worker.js +14 -9
- package/.agents/workflows/helpers/deliver-light.md +21 -4
- package/.agents/workflows/helpers/plan-reference.md +40 -0
- package/.agents/workflows/plan.md +21 -16
- package/docs/CHANGELOG.md +23 -0
- package/package.json +1 -1
|
@@ -26,6 +26,33 @@ import { upsertStructuredComment } from './ticketing.js';
|
|
|
26
26
|
|
|
27
27
|
export const FOLLOW_UPS_COMMENT_TYPE = 'follow-ups';
|
|
28
28
|
|
|
29
|
+
/** Milliseconds in one day — the unit `frictionWindowDays` is expressed in. */
|
|
30
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
/** Window bound applied when `frictionWindowDays` is unset. */
|
|
33
|
+
const DEFAULT_FRICTION_WINDOW_DAYS = 30;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* How many days back the run-scope recurrence window reaches (Story #4850).
|
|
37
|
+
*
|
|
38
|
+
* Defaults to 30 rather than to "unbounded": the widened cross-run window
|
|
39
|
+
* exists to let a once-per-Story defect reach the ≥ 2 threshold, and 30 days is
|
|
40
|
+
* long enough for that while short enough that a defect fixed last month stops
|
|
41
|
+
* re-routing. An absent, non-integer, or sub-1 value takes the default — the
|
|
42
|
+
* runtime AJV in `config-settings-schema-delivery.js` rejects those at load, so
|
|
43
|
+
* reaching this fallback means the config never went through the validator.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} [config]
|
|
46
|
+
* @returns {number}
|
|
47
|
+
*/
|
|
48
|
+
function resolveFrictionWindowDays(config) {
|
|
49
|
+
const raw = config?.delivery?.feedbackLoop?.frictionWindowDays;
|
|
50
|
+
const days = Number(raw);
|
|
51
|
+
return Number.isInteger(days) && days >= 1
|
|
52
|
+
? days
|
|
53
|
+
: DEFAULT_FRICTION_WINDOW_DAYS;
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
/**
|
|
30
57
|
* @param {object} [config]
|
|
31
58
|
* @returns {{ frameworkRepo: string, consumerRepo: string, currentRepo: { owner: string, repo: string } }}
|
|
@@ -145,18 +172,55 @@ function signalIdentity(parsed, file, lineNumber) {
|
|
|
145
172
|
* Unusable ids are skipped rather than throwing — a roll-up must not fail the
|
|
146
173
|
* epilogue over one malformed entry.
|
|
147
174
|
*
|
|
175
|
+
* **Bounded by age, not by run (Story #4850).** Widening the window to the
|
|
176
|
+
* whole surviving temp tree also made it unbounded in *time*: a defect fixed
|
|
177
|
+
* weeks ago kept its occurrences on disk and kept re-routing forever, burying
|
|
178
|
+
* a genuine new regression underneath a historical ledger. Rows older than
|
|
179
|
+
* `delivery.feedbackLoop.frictionWindowDays` (default 30) are excluded, as are
|
|
180
|
+
* rows carrying no `ts` a `Date` can read — excluding an undateable row is the
|
|
181
|
+
* direction that fails toward under-counting, and under-counting fails toward
|
|
182
|
+
* not filing. Both exclusions are **counted and reported**, so a caller can
|
|
183
|
+
* tell a bounded window from an unbounded one without reading prose.
|
|
184
|
+
*
|
|
185
|
+
* A recovery marker is written after the incident it cancels, so a marker can
|
|
186
|
+
* never fall outside a window its incident is inside — the netting cannot be
|
|
187
|
+
* broken by the age floor.
|
|
188
|
+
*
|
|
148
189
|
* @param {Array<number|string>} storyIds The run's own Stories.
|
|
149
190
|
* @param {object} [config]
|
|
150
|
-
* @
|
|
191
|
+
* @param {{ now?: number }} [clock] Injected epoch-ms seam so a test can pin
|
|
192
|
+
* the window without touching the system clock.
|
|
193
|
+
* @returns {Promise<{
|
|
194
|
+
* signals: Array<{ category: string, source: 'framework'|'consumer', storyId: number, ts: string|null, details: object }>,
|
|
195
|
+
* window: { days: number, cutoff: string, excludedStale: number, excludedUnparseable: number },
|
|
196
|
+
* }>}
|
|
151
197
|
*/
|
|
152
|
-
export async function gatherRunFrictionSignals(
|
|
198
|
+
export async function gatherRunFrictionSignals(
|
|
199
|
+
storyIds,
|
|
200
|
+
config,
|
|
201
|
+
{ now = Date.now() } = {},
|
|
202
|
+
) {
|
|
203
|
+
const days = resolveFrictionWindowDays(config);
|
|
204
|
+
const cutoffMs = now - days * MS_PER_DAY;
|
|
153
205
|
const signals = [];
|
|
154
206
|
const seen = new Set();
|
|
207
|
+
let excludedStale = 0;
|
|
208
|
+
let excludedUnparseable = 0;
|
|
155
209
|
const take = (parsed, fallbackStoryId, identity) => {
|
|
156
210
|
if (seen.has(identity)) return;
|
|
157
211
|
seen.add(identity);
|
|
158
212
|
const signal = normalizeGatheredSignal(parsed, fallbackStoryId);
|
|
159
|
-
if (signal)
|
|
213
|
+
if (!signal) return;
|
|
214
|
+
const ms = signal.ts === null ? Number.NaN : Date.parse(signal.ts);
|
|
215
|
+
if (!Number.isFinite(ms)) {
|
|
216
|
+
excludedUnparseable += 1;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (ms < cutoffMs) {
|
|
220
|
+
excludedStale += 1;
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
signals.push(signal);
|
|
160
224
|
};
|
|
161
225
|
|
|
162
226
|
for (const raw of Array.isArray(storyIds) ? storyIds : []) {
|
|
@@ -178,7 +242,15 @@ export async function gatherRunFrictionSignals(storyIds, config) {
|
|
|
178
242
|
config,
|
|
179
243
|
);
|
|
180
244
|
|
|
181
|
-
return
|
|
245
|
+
return {
|
|
246
|
+
signals,
|
|
247
|
+
window: {
|
|
248
|
+
days,
|
|
249
|
+
cutoff: new Date(cutoffMs).toISOString(),
|
|
250
|
+
excludedStale,
|
|
251
|
+
excludedUnparseable,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
182
254
|
}
|
|
183
255
|
|
|
184
256
|
/**
|
|
@@ -155,7 +155,7 @@ The serialized \`body\` string renders these markdown sections (in order):
|
|
|
155
155
|
- **Observed-behavior claims open with \`Current state (verified <date>)\`.** Any Spec claim about how the codebase behaves today MUST open with that preamble (e.g. \`Current state (verified 2026-07-17): …\`) so a reader can tell a verified observation from an assumption, and can tell when the observation went stale.
|
|
156
156
|
- **Intent-then-proxy acceptance shape.** When an acceptance item verifies through a proxy check (a grep, a file-exists probe, an exit-code test), state the intent clause before the proxy check — what outcome the check stands in for — so the proxy never becomes the goal (e.g. "the workflow names hygiene findings as re-author input: \`grep -n "textHygiene" …\` exits 0").
|
|
157
157
|
- **Slicing checkpoints are one line each.** Each \`## Slicing\` checkpoint is a single line naming the checkpoint; implementation detail lives in \`## Spec\`, never duplicated into Slicing. A Slicing section outweighing its Spec is a defect the text-hygiene lint flags.
|
|
158
|
-
- **Bodies record decisions, never questions to the operator.** Never persist an open question ("Flag if…", "TBD", "confirm with the operator") into a Story body — the executing sub-agent is non-interactive and cannot answer it.
|
|
158
|
+
- **Bodies record decisions, never questions to the operator.** Never persist an open question ("Flag if…", "TBD", "confirm with the operator") into a Story body — the executing sub-agent is non-interactive and cannot answer it. Triage each unknown by who can resolve it: an AFK-shaped unknown (a fact in docs, a third-party API surface, observable repo behavior) MUST be resolved by your own research before authoring — never restated as an assumption; only a HITL-shaped unknown (a genuine product or architecture call the operator owns) may be restated as a declarative Key Assumption the agent can act on, stating the default chosen (a decision-made-by-default).
|
|
159
159
|
- **non_goals** (OPTIONAL, in body string as the \`## Non-Goals\` section): A short list of capabilities or changes this Story explicitly does NOT deliver — an advisory negative-scope bound that fences the executing agent away from adjacent work. It is **advisory and NON-GATING**: the validator does not require, count, or reject on it, and an absent or empty section renders nothing. Use the EXACT single-word hyphenated heading spelling \`## Non-Goals\` (a space-separated heading like \`## Out of Scope\` is NOT recognized by the parser and will be dropped). Reach for it when a Story's negative boundary is non-obvious from its \`acceptance[]\` alone; omit it otherwise.
|
|
160
160
|
|
|
161
161
|
#### SPEC PROSE CONTRACT — state the contract, not the implementation:
|
|
@@ -7,16 +7,20 @@
|
|
|
7
7
|
* Message contract — see lib/cpu-pool.js:
|
|
8
8
|
* IN : { item: string } — absolute file path to score
|
|
9
9
|
* { exit: true } — drain & terminate
|
|
10
|
-
* OUT : { ok: true, result: { filePath, score
|
|
10
|
+
* OUT : { ok: true, result: { filePath, score, unscorable?, reason? } }
|
|
11
11
|
*
|
|
12
12
|
* `score` is `null` only when the file genuinely cannot be read (ENOENT
|
|
13
|
-
* or other I/O error).
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* or other I/O error).
|
|
14
|
+
*
|
|
15
|
+
* A file the kernel cannot analyse comes back as `unscorable: true` with the
|
|
16
|
+
* kernel's own `reason`, rather than as a bare `0`. The `0` is still carried in
|
|
17
|
+
* `score` for wire compatibility, but it is no longer the only signal — the
|
|
18
|
+
* point of the flag is that the caller can *report* the file instead of
|
|
19
|
+
* silently dropping it (see `maintainability-engine.js`'s `UNSCORABLE`).
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
import { parentPort } from 'node:worker_threads';
|
|
19
|
-
import {
|
|
23
|
+
import { scoreFile } from '../maintainability-engine.js';
|
|
20
24
|
|
|
21
25
|
/**
|
|
22
26
|
* Pure handler for a single inbound worker message. Exported so unit
|
|
@@ -24,7 +28,7 @@ import { calculateForFile } from '../maintainability-engine.js';
|
|
|
24
28
|
* without spawning a real `Worker` thread.
|
|
25
29
|
*
|
|
26
30
|
* @param {unknown} msg
|
|
27
|
-
* @param {{ score?: (filePath: string) => number |
|
|
31
|
+
* @param {{ score?: (filePath: string) => { score: number, unscorable: boolean, reason: string|null } }} [deps]
|
|
28
32
|
* @returns {{kind: 'exit'} | {kind: 'reply', message: object}}
|
|
29
33
|
*/
|
|
30
34
|
export function handleMaintainabilityWorkerMessage(msg, deps = {}) {
|
|
@@ -40,12 +44,13 @@ export function handleMaintainabilityWorkerMessage(msg, deps = {}) {
|
|
|
40
44
|
};
|
|
41
45
|
}
|
|
42
46
|
const filePath = msg.item;
|
|
43
|
-
const scoreFn = deps.score ??
|
|
47
|
+
const scoreFn = deps.score ?? scoreFile;
|
|
44
48
|
try {
|
|
45
|
-
|
|
49
|
+
// `scoreFn` returns `{ score, unscorable, reason }` — spread so the flag
|
|
50
|
+
// and its reason reach the pool caller intact.
|
|
46
51
|
return {
|
|
47
52
|
kind: 'reply',
|
|
48
|
-
message: { ok: true, result: { filePath,
|
|
53
|
+
message: { ok: true, result: { filePath, ...scoreFn(filePath) } },
|
|
49
54
|
};
|
|
50
55
|
} catch (err) {
|
|
51
56
|
// I/O or other unexpected error — surface as a per-item null score
|
|
@@ -48,6 +48,11 @@ Because the predicted footprint is a *declaration* — a guess, and a gameable o
|
|
|
48
48
|
multi-capability enumeration). Size is enforced where ground truth is available:
|
|
49
49
|
the diff backstop in step 4. Do not talk yourself past that one.
|
|
50
50
|
|
|
51
|
+
**The backstop counts by the same principle.** It reads magnitude — changed
|
|
52
|
+
lines over implementation files — not artifacts, and exempts the test and doc
|
|
53
|
+
companions the framework itself mandates. A ceiling that punishes a repo for
|
|
54
|
+
obeying its own test-first rule is a ceiling that over-fires.
|
|
55
|
+
|
|
51
56
|
Sensitivity is the exception and stays absolute: a footprint touching an auth,
|
|
52
57
|
crypto, billing, or migration class routes `full` however small or mechanical —
|
|
53
58
|
and unlike a ceiling, it is **not overridable** (§ Recording a proceed-light
|
|
@@ -136,10 +141,22 @@ answer).
|
|
|
136
141
|
node .agents/scripts/deliver-light.js --backstop --story <storyId>
|
|
137
142
|
```
|
|
138
143
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
This is the pass that actually bounds size, which is why the prediction gate
|
|
145
|
+
above can afford to be coarse. It measures **magnitude on the change's
|
|
146
|
+
implementation half** — changed lines (additions + deletions) plus a file
|
|
147
|
+
sprawl tripwire — never raw artifact count. Tests, `docs/**`, `**/*.md`,
|
|
148
|
+
`baselines/**`, and lockfiles are exempt from the counts, because the
|
|
149
|
+
framework mandates those companions and obeying it must not inflate the
|
|
150
|
+
number that then rejects the change. They are **not** exempt from
|
|
151
|
+
sensitive-path matching, which runs over the full change set.
|
|
152
|
+
|
|
153
|
+
Exit `3` (`blocked: true`) means the diff exceeds a light ceiling or touches a
|
|
154
|
+
sensitive-path class. STOP, flip `agent::blocked`, and **recycle the receipt**
|
|
155
|
+
through the envelope's `nextCommand` (`/plan <storyId>`) — tickets mode
|
|
156
|
+
rewrites it into properly-planned Stories and closes it as superseded. Do not
|
|
157
|
+
land, and do not leave the receipt open with no successor: it already carries
|
|
158
|
+
the branch, the worktree, and the implementation, all of which are evidence
|
|
159
|
+
the plan should read.
|
|
143
160
|
|
|
144
161
|
5. **Close and land (same engine).** Exactly [`/deliver`](../deliver.md)'s close:
|
|
145
162
|
|
|
@@ -36,6 +36,46 @@ whole surface exists to remove.
|
|
|
36
36
|
Mixed ids and prose in one invocation is a **hard error**: refuse and ask which
|
|
37
37
|
was meant, rather than guessing a mode and doing the wrong work.
|
|
38
38
|
|
|
39
|
+
## Unknown triage — AFK vs HITL
|
|
40
|
+
|
|
41
|
+
Every open question interrogation surfaces is triaged by **who can resolve
|
|
42
|
+
it**, not parked in one bucket (a shape borrowed from the Wayfinder skill's
|
|
43
|
+
HITL/AFK ticket typing):
|
|
44
|
+
|
|
45
|
+
- **AFK** (away from keyboard — the agent resolves it alone): the answer is a
|
|
46
|
+
fact something already records — third-party docs, a dependency's API
|
|
47
|
+
surface, observable behavior of this repo. Research it during interrogation
|
|
48
|
+
(per `.agents/instructions.md` § 1.C) and fold the answer into the plan as a
|
|
49
|
+
verified claim. An AFK unknown never becomes a Key Assumption — an
|
|
50
|
+
assumption standing in for a checkable fact is just an unchecked fact.
|
|
51
|
+
- **HITL** (human in the loop — only the operator can resolve it): a genuine
|
|
52
|
+
product or architecture call — what to support, what to drop, which
|
|
53
|
+
trade-off to prefer. Nothing the agent reads can answer it; presenting a
|
|
54
|
+
researched recommendation is fine, deciding is not.
|
|
55
|
+
|
|
56
|
+
Boundary examples: *"does library X support streaming?"* is AFK (read its
|
|
57
|
+
docs); *"should we drop Node 18 support?"* is HITL (a support-policy call);
|
|
58
|
+
*"does our CLI already validate this flag?"* is AFK (read the code);
|
|
59
|
+
*"which of two valid schema shapes should the new field use?"* is HITL when
|
|
60
|
+
both fit — but first verify it is not settled by an existing convention,
|
|
61
|
+
which would make it AFK.
|
|
62
|
+
|
|
63
|
+
**Attended runs** present the HITL list at Gate #1 as "needs your decision",
|
|
64
|
+
one line each, alongside the sharpened intent. **Under `--yes`** nobody is at
|
|
65
|
+
the keyboard: AFK unknowns are researched exactly as in an attended run, and
|
|
66
|
+
each HITL unknown degrades to a declarative Key Assumption that names the
|
|
67
|
+
default chosen and marks it a decision-made-by-default, e.g.:
|
|
68
|
+
|
|
69
|
+
> **Key Assumption (decision-made-by-default):** new-style envelopes only;
|
|
70
|
+
> re-emitting legacy envelopes was ruled out by default, not by the operator.
|
|
71
|
+
|
|
72
|
+
(Keep the assumption itself declarative — "flag if wrong" phrasing trips the
|
|
73
|
+
open-question hygiene lint, and the deliverer cannot answer it anyway.)
|
|
74
|
+
|
|
75
|
+
The marker keeps the operator's undelegated decisions findable after the
|
|
76
|
+
fact: reviewing a `--yes` plan means scanning its decisions-made-by-default,
|
|
77
|
+
not re-deriving which assumptions were really the agent's to make.
|
|
78
|
+
|
|
39
79
|
## Gate #1 → the light path (in-session handoff)
|
|
40
80
|
|
|
41
81
|
On a confirmed `deliverLightSuggestion`, `/plan` routes into
|
|
@@ -14,7 +14,7 @@ description:
|
|
|
14
14
|
|
|
15
15
|
Single planning path — there is no Epic/Story router, no scope-triage
|
|
16
16
|
`epic|story` verdict. **Derive the mode from what the operator typed, announce
|
|
17
|
-
it, then act
|
|
17
|
+
it, then act**:
|
|
18
18
|
|
|
19
19
|
| Invocation | Mode | Behavior |
|
|
20
20
|
| --- | --- | --- |
|
|
@@ -24,11 +24,10 @@ it, then act**; there is nothing for them to remember:
|
|
|
24
24
|
| `/plan 4712[,4713…]` | tickets | Fetch issue(s), analyze into proper Stories (prefer N=1 rewrite). |
|
|
25
25
|
| `/plan 4712`, already delivered | amends | Amend a shipped Story from a **delta envelope**, not a re-interrogation. |
|
|
26
26
|
|
|
27
|
-
**Resolving a bare id.**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
wasted run. Ask **only** for an open Story already at `agent::ready`.
|
|
27
|
+
**Resolving a bare id.** Read live state rather than asking: `agent::done` can
|
|
28
|
+
only be amended, an open unplanned issue can only be planned. **Announce the
|
|
29
|
+
derivation** — "4712 is `agent::done` → amending" — so a wrong read costs one
|
|
30
|
+
correction. Ask **only** for an open Story already at `agent::ready`.
|
|
32
31
|
|
|
33
32
|
`--body` is **not** a `/plan` entry; persist goes through `plan-persist.js`.
|
|
34
33
|
|
|
@@ -62,19 +61,26 @@ node .agents/scripts/plan-context.js --seed "<seed>" \
|
|
|
62
61
|
|
|
63
62
|
**Always pass `--out`.** Persist auto-discovers the envelope from `--plan-dir`
|
|
64
63
|
and derives source ids from its `sourceTickets[]`; the CLI also writes
|
|
65
|
-
**`stories.template.json`**,
|
|
64
|
+
**`stories.template.json`**, step 2's skeleton.
|
|
66
65
|
|
|
67
66
|
The envelope carries docs context, the story-author
|
|
68
67
|
prompt, `sourceTickets[]`, `duplicates[]` (open **Stories**, never Epics), and
|
|
69
68
|
advisory `complexitySignals` (**no routing authority**). A trivial scope earns
|
|
70
69
|
`--route-downgrade-reason "<why>"` at persist — shape-validated, failing closed
|
|
71
|
-
to `full` ([detail](helpers/plan-reference.md)).
|
|
72
|
-
|
|
70
|
+
to `full` ([detail](helpers/plan-reference.md)).
|
|
71
|
+
|
|
72
|
+
**Triage each unknown by resolver**
|
|
73
|
+
([detail](helpers/plan-reference.md)): an **AFK** unknown (research settles
|
|
74
|
+
it) is resolved before authoring, never assumed; a **HITL** unknown (an
|
|
75
|
+
operator call) goes to Gate #1 as "needs your decision". Under `--yes`, do
|
|
76
|
+
not ask free-form operator questions — AFK unknowns are still researched;
|
|
77
|
+
only HITL unknowns land in Key Assumptions, each marked a
|
|
78
|
+
decision-made-by-default.
|
|
73
79
|
|
|
74
80
|
**Gate #1** — STOP to confirm the sharpened plan intent and any
|
|
75
81
|
duplicate-candidate review. Under `--yes`, auto-proceed.
|
|
76
82
|
|
|
77
|
-
|
|
83
|
+
On a truthy `deliverLightSuggestion.suggested`, offer —
|
|
78
84
|
**advisory, never an automatic reroute** — to deliver the seed instead of
|
|
79
85
|
planning it. On confirm, route **in this session** into
|
|
80
86
|
[`helpers/deliver-light.md`](helpers/deliver-light.md), filling its gate from
|
|
@@ -88,9 +94,9 @@ A truthy `complexitySignals.uiSurface` marks a UI-touching plan: name
|
|
|
88
94
|
### 2. Author
|
|
89
95
|
|
|
90
96
|
**One-shot authoring.** Start from `stories.template.json`; author
|
|
91
|
-
`stories.json` in one pass.
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
`stories.json` in one pass. `body` is a markdown string **or** a structured
|
|
98
|
+
object; persist parses either, serializes the canonical markdown, and syncs
|
|
99
|
+
top-level `acceptance[]` /
|
|
94
100
|
`verify[]` into it — never dual-author those lists.
|
|
95
101
|
|
|
96
102
|
**Grounding = your reads + Phase 8.** Nothing inventories the repo for you:
|
|
@@ -149,9 +155,8 @@ node .agents/scripts/plan-persist.js \
|
|
|
149
155
|
[--source-tickets 123,456]
|
|
150
156
|
```
|
|
151
157
|
|
|
152
|
-
At lite shape, `--chain-on-clean` chains
|
|
153
|
-
one round-trip
|
|
154
|
-
review round-trip.
|
|
158
|
+
At lite shape, `--chain-on-clean` chains a clean dry-run into the real persist
|
|
159
|
+
in one round-trip; a full plan keeps its review round-trip.
|
|
155
160
|
|
|
156
161
|
Persist creates `type::story` issue(s) plus a `plan-run::<id>` grouping label
|
|
157
162
|
(**metadata only**); N>1 `depends_on` edges become `blocked by #<id>` footers.
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [2.24.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.23.0...mandrel-v2.24.0) (2026-07-31)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
* **deliver:** hold the Story assignee-lease until the PR is confirmed merged (refs [#4860](https://github.com/dsj1984/mandrel/issues/4860)) ([#4861](https://github.com/dsj1984/mandrel/issues/4861)) ([d722a65](https://github.com/dsj1984/mandrel/commit/d722a6558f2fa1af6aa428d2f8549101c4bedc23))
|
|
11
|
+
* **maintainability:** score the Babel AST the escomplex kernel actually parses ([#4859](https://github.com/dsj1984/mandrel/issues/4859)) ([279b86f](https://github.com/dsj1984/mandrel/commit/279b86fa49c48454b5e8421ca1e0bb01f54a3bba))
|
|
12
|
+
* scope the light diff backstop by change magnitude, and recycle the receipt instead of orphaning it ([#4856](https://github.com/dsj1984/mandrel/issues/4856)) ([#4857](https://github.com/dsj1984/mandrel/issues/4857)) ([114b479](https://github.com/dsj1984/mandrel/commit/114b4797f248bf00064206f32f490413e28276fb))
|
|
13
|
+
|
|
14
|
+
## [2.23.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.22.0...mandrel-v2.23.0) (2026-07-30)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
* **plan:** triage interrogation unknowns by resolver — AFK research vs HITL operator decision (refs [#4845](https://github.com/dsj1984/mandrel/issues/4845)) ([#4846](https://github.com/dsj1984/mandrel/issues/4846)) ([3cfcd00](https://github.com/dsj1984/mandrel/commit/3cfcd00f3885fc588bdf2bb4597118a790f9b270))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
* **diagnose-friction:** name the signal that killed a child instead of reporting "Unknown exit code null" and exiting 0 ([#4851](https://github.com/dsj1984/mandrel/issues/4851)) ([#4853](https://github.com/dsj1984/mandrel/issues/4853)) ([1985be1](https://github.com/dsj1984/mandrel/commit/1985be1b97d751d094acf7dd371b97124039cc6d))
|
|
25
|
+
* **rollup:** describe the friction corpus by its own window, not the triggering run (refs [#4850](https://github.com/dsj1984/mandrel/issues/4850)) ([#4852](https://github.com/dsj1984/mandrel/issues/4852)) ([d866697](https://github.com/dsj1984/mandrel/commit/d8666976be30d218115b9db32602359d55379f88))
|
|
26
|
+
* **rollup:** track anchorKind in the triggering-anchor label ([#4854](https://github.com/dsj1984/mandrel/issues/4854)) ([b7e4ddf](https://github.com/dsj1984/mandrel/commit/b7e4ddf8c362f7c31c2abd73ee181a94f78f6dd2))
|
|
27
|
+
|
|
5
28
|
## [2.22.0](https://github.com/dsj1984/mandrel/compare/mandrel-v2.21.0...mandrel-v2.22.0) (2026-07-30)
|
|
6
29
|
|
|
7
30
|
|
package/package.json
CHANGED