mandrel 1.77.0 → 1.79.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/workflows.md +19 -0
- package/.agents/schemas/lifecycle/loop.tick.schema.json +20 -0
- package/.agents/schemas/loop-unit.schema.json +70 -0
- package/.agents/scripts/check-doc-links.js +24 -1
- package/.agents/scripts/check-loop-units.js +204 -0
- package/.agents/scripts/generate-workflows-doc.js +37 -4
- package/.agents/scripts/lib/close-validation/process.js +61 -5
- package/.agents/scripts/lib/close-validation/runner.js +17 -1
- package/.agents/scripts/lib/loop-units/validate-loop-unit.js +197 -0
- package/.agents/scripts/lib/mandrel-catalog.js +36 -0
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +7 -2
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +15 -36
- package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +183 -0
- package/.agents/scripts/lib/story-lifecycle.js +12 -4
- package/.agents/scripts/lib/templates/decomposer-prompts.js +14 -5
- package/.agents/scripts/providers/github/branch-protection.js +1 -1
- package/.agents/scripts/providers/github/errors.js +53 -2
- package/.agents/scripts/providers/github/labels.js +1 -1
- package/.agents/scripts/providers/github/projects-v2-graphql.js +1 -1
- package/.agents/scripts/run-lint.js +11 -0
- package/.agents/scripts/sync-claude-commands.js +112 -29
- package/.agents/scripts/update-maintainability-baseline.js +19 -76
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +10 -7
- package/.agents/workflows/loops/README.md +65 -0
- package/.agents/workflows/loops/fix-failing-tests.md +74 -0
- package/.agents/workflows/loops/nightly-audit.md +71 -0
- package/.agents/workflows/loops/watch-ci.md +68 -0
- package/docs/CHANGELOG.md +26 -0
- package/package.json +1 -1
- package/.agents/scripts/providers/github/transient-retry.js +0 -62
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: >-
|
|
3
|
+
Cron maintenance loop that runs a nightly audit sweep over the repository and
|
|
4
|
+
files actionable findings. Each run executes the audit workflows and routes
|
|
5
|
+
the results; the host (`/schedule` or a cron-driven `/loop`) owns the cadence.
|
|
6
|
+
verify is optional for a cron loop — the scheduler owns iteration, so this
|
|
7
|
+
unit ships the action and goal, not a terminating oracle.
|
|
8
|
+
loop:
|
|
9
|
+
cadence: cron
|
|
10
|
+
goal: >-
|
|
11
|
+
Keep the repository's standing health surfaced by running the audit sweep on
|
|
12
|
+
a nightly schedule and turning each fresh finding into an actionable, deduped
|
|
13
|
+
record so regressions are caught within a day rather than at release time.
|
|
14
|
+
maxRounds: 30
|
|
15
|
+
onExhaust: report
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# /loops:nightly-audit — scheduled maintenance audit sweep
|
|
19
|
+
|
|
20
|
+
A **cron maintenance loop**. The host (`/schedule`, or a cron-driven `/loop`)
|
|
21
|
+
owns the cadence and fires this unit once per scheduled window — typically
|
|
22
|
+
overnight. Because the scheduler owns iteration, this unit carries **no
|
|
23
|
+
`verify` oracle**: per the loop-unit schema, `verify` is required only for
|
|
24
|
+
`self-paced` cadence and optional for `interval` / `cron`. Each run is a single
|
|
25
|
+
sweep that observes, records, and yields until the next scheduled tick.
|
|
26
|
+
|
|
27
|
+
## Action
|
|
28
|
+
|
|
29
|
+
Each scheduled run:
|
|
30
|
+
|
|
31
|
+
1. **Run the audit sweep.** Execute the relevant audit workflows for the repo
|
|
32
|
+
(`/audit-security`, `/audit-clean-code`, `/audit-dependencies`,
|
|
33
|
+
`/audit-quality`, and any others the project relies on). Each audit writes a
|
|
34
|
+
structured `temp/audits/audit-*-results.md` report — that is the canonical
|
|
35
|
+
artifact this loop consumes, not free-form prose.
|
|
36
|
+
2. **Diff against the prior night.** Compare the fresh findings against the last
|
|
37
|
+
sweep's reports and against already-open Issues. A finding seen before is
|
|
38
|
+
not new signal; only genuinely fresh or regressed findings warrant a record.
|
|
39
|
+
3. **Route fresh findings.** Hand the new findings to `/audit-to-stories`, which
|
|
40
|
+
deduplicates against existing Issues by fingerprint and either chains into
|
|
41
|
+
`/plan` or opens standalone Stories. Do not open raw duplicate Issues —
|
|
42
|
+
dedup is the loop's job, not the operator's.
|
|
43
|
+
4. **Report and yield.** Emit a short digest (sweeps run, new findings, Issues
|
|
44
|
+
opened or updated) and return control to the scheduler, which sleeps until
|
|
45
|
+
the next cron window.
|
|
46
|
+
|
|
47
|
+
## Goal & done-signal
|
|
48
|
+
|
|
49
|
+
- **Goal:** the repository's health regressions are caught and turned into
|
|
50
|
+
actionable, deduplicated records within a day, without a human remembering to
|
|
51
|
+
run the audits by hand.
|
|
52
|
+
- **Done-signal:** the nightly sweep completed and every fresh finding has been
|
|
53
|
+
routed to a record (or explicitly judged a non-finding). A cron loop has no
|
|
54
|
+
self-evaluated oracle — the scheduler owns whether the loop runs again; this
|
|
55
|
+
unit simply finishes the night's sweep and yields.
|
|
56
|
+
- **Backstop:** `maxRounds: 30`. Roughly a month of nightly runs;
|
|
57
|
+
`onExhaust: report` emits a final digest and stops so a long-lived schedule
|
|
58
|
+
is renewed deliberately rather than running unbounded.
|
|
59
|
+
|
|
60
|
+
## Stop & escalate
|
|
61
|
+
|
|
62
|
+
- **An audit cannot run** (a required tool is missing, the audit harness errors,
|
|
63
|
+
the working tree is dirty in a way that invalidates the sweep). Report the
|
|
64
|
+
failure for that audit and continue with the others — do not abort the whole
|
|
65
|
+
night because one audit broke.
|
|
66
|
+
- **A finding is high-severity and time-sensitive** (an exposed secret, a
|
|
67
|
+
critical CVE reachable in production). Surface it loudly in the digest rather
|
|
68
|
+
than letting it sit as one row among many — a nightly cadence is too slow for
|
|
69
|
+
an actively-exploitable finding.
|
|
70
|
+
- **`maxRounds` is reached.** Emit a final digest (`onExhaust: report`) so the
|
|
71
|
+
operator can renew or retire the schedule deliberately.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: >-
|
|
3
|
+
Interval watch loop that polls a pull request's CI checks until they settle.
|
|
4
|
+
Each round runs `gh pr checks` and reports the delta; the host (`/loop 5m`)
|
|
5
|
+
owns the cadence and re-invokes the unit on its schedule. verify is optional
|
|
6
|
+
for an interval loop — the externally-scheduled host owns iteration, so this
|
|
7
|
+
unit ships the action and goal, not a terminating oracle.
|
|
8
|
+
loop:
|
|
9
|
+
cadence: interval
|
|
10
|
+
goal: >-
|
|
11
|
+
Keep an eye on the current pull request's required CI checks each interval,
|
|
12
|
+
surfacing the first failed or stuck check the moment it appears so a human
|
|
13
|
+
can act before the checks finish.
|
|
14
|
+
maxRounds: 60
|
|
15
|
+
onExhaust: report
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# /loops:watch-ci — poll a PR's checks until they settle
|
|
19
|
+
|
|
20
|
+
An **interval watch loop**. The host (`/loop <interval> /loops:watch-ci`, e.g.
|
|
21
|
+
`/loop 5m …`) owns the cadence and re-runs this unit on each tick. Because the
|
|
22
|
+
host schedules iteration externally, this unit carries **no `verify` oracle** —
|
|
23
|
+
per the loop-unit schema, `verify` is required only for `self-paced` cadence and
|
|
24
|
+
optional for `interval` / `cron`. The unit's job each round is to observe and
|
|
25
|
+
report, not to converge.
|
|
26
|
+
|
|
27
|
+
## Action
|
|
28
|
+
|
|
29
|
+
Each interval:
|
|
30
|
+
|
|
31
|
+
1. **Read the current check state.** Run `gh pr checks` for the PR under watch
|
|
32
|
+
(the host supplies the PR number, or it is inferred from the current
|
|
33
|
+
branch's open PR). Capture the per-check status: pending, passed, or failed.
|
|
34
|
+
2. **Compute the delta since last round.** Compare against the prior round's
|
|
35
|
+
snapshot. A check that flipped `pending → failed` is the headline; a check
|
|
36
|
+
that flipped `pending → passed` is progress.
|
|
37
|
+
3. **Surface failures immediately.** On the first failed or cancelled required
|
|
38
|
+
check, report it — name the check, link the run, and quote the first error
|
|
39
|
+
line if cheaply available — so a human can act before the rest of the matrix
|
|
40
|
+
finishes. Do not wait for the whole suite to settle to raise a red check.
|
|
41
|
+
4. **Report and yield.** Emit a one-line status summary
|
|
42
|
+
(`N passed, M pending, K failed`) and return control to the host, which
|
|
43
|
+
sleeps until the next interval.
|
|
44
|
+
|
|
45
|
+
## Goal & done-signal
|
|
46
|
+
|
|
47
|
+
- **Goal:** the operator learns about a CI failure on the watched PR as early as
|
|
48
|
+
the polling interval allows, and knows when all required checks have gone
|
|
49
|
+
green.
|
|
50
|
+
- **Done-signal:** all required checks have a terminal status (every check
|
|
51
|
+
passed, or at least one has failed). An interval loop has no self-evaluated
|
|
52
|
+
oracle — the host stops the loop when the operator cancels it, when a failure
|
|
53
|
+
is surfaced and acted on, or when `maxRounds` is reached.
|
|
54
|
+
- **Backstop:** `maxRounds: 60`. At a 5-minute interval that is ~5 hours of
|
|
55
|
+
watching; `onExhaust: report` emits a final status and stops rather than
|
|
56
|
+
polling forever on a wedged check.
|
|
57
|
+
|
|
58
|
+
## Stop & escalate
|
|
59
|
+
|
|
60
|
+
- **A required check failed.** Surface it and let the operator decide whether to
|
|
61
|
+
keep watching the remaining checks or stop. A failed required check is the
|
|
62
|
+
signal the watch existed to catch.
|
|
63
|
+
- **The PR cannot be resolved** (no open PR for the branch, `gh` not
|
|
64
|
+
authenticated, the PR was merged or closed out from under the watch). Report
|
|
65
|
+
the condition and stop — there is nothing left to watch.
|
|
66
|
+
- **`maxRounds` is reached with checks still pending.** Emit a final summary of
|
|
67
|
+
the stuck checks (`onExhaust: report`) so the operator can investigate the
|
|
68
|
+
wedged run.
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.79.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.78.0...mandrel-v1.79.0) (2026-06-30)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
* **decompose-author:** require namespaced [@epic](https://github.com/epic)-<id>-ac-N tag on wave-0 BDD scaffold scenarios (refs [#4301](https://github.com/dsj1984/mandrel/issues/4301)) ([#4304](https://github.com/dsj1984/mandrel/issues/4304)) ([c97d45e](https://github.com/dsj1984/mandrel/commit/c97d45e806a72a8511995d8f606b5be2ef486315))
|
|
11
|
+
* **reconciler:** single-source the parent/Epic body trailer so force re-decompose retains Epic: #<id> (refs [#4300](https://github.com/dsj1984/mandrel/issues/4300)) ([#4303](https://github.com/dsj1984/mandrel/issues/4303)) ([a8e78a9](https://github.com/dsj1984/mandrel/commit/a8e78a9fd946e6f532a59df9cd37237e0d046b2d))
|
|
12
|
+
|
|
13
|
+
## [1.78.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.77.0...mandrel-v1.78.0) (2026-06-24)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
* Epic [#4284](https://github.com/dsj1984/mandrel/issues/4284) ([#4297](https://github.com/dsj1984/mandrel/issues/4297)) ([0f7c543](https://github.com/dsj1984/mandrel/commit/0f7c543e1ceb1c38ac425c388e338e3bfaaec49e))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
* **baselines:** apply ignoreGlobs in the maintainability baseline CLI diff-scope branch (refs [#4293](https://github.com/dsj1984/mandrel/issues/4293)) ([#4294](https://github.com/dsj1984/mandrel/issues/4294)) ([c0c7dd5](https://github.com/dsj1984/mandrel/commit/c0c7dd5948314800e5a9d5808fb5eb0b01b09833))
|
|
24
|
+
* **close-validation:** treat biome "No files were processed" as a clean format-gate skip (refs [#4292](https://github.com/dsj1984/mandrel/issues/4292)) ([#4295](https://github.com/dsj1984/mandrel/issues/4295)) ([bf6840d](https://github.com/dsj1984/mandrel/commit/bf6840dd69abbece4eb221797e6432519b6614a0))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
* **github-provider:** unify the two divergent withTransientRetry into one canonical primitive (refs [#4298](https://github.com/dsj1984/mandrel/issues/4298)) ([#4299](https://github.com/dsj1984/mandrel/issues/4299)) ([e5668dc](https://github.com/dsj1984/mandrel/commit/e5668dcca73b37c37d38e52e1eb8d2930a0e1790))
|
|
30
|
+
|
|
5
31
|
## [1.77.0](https://github.com/dsj1984/mandrel/compare/mandrel-v1.76.0...mandrel-v1.77.0) (2026-06-24)
|
|
6
32
|
|
|
7
33
|
|
package/package.json
CHANGED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* transient-retry — shared retry-with-backoff for GitHub provider calls.
|
|
3
|
-
*
|
|
4
|
-
* Resilience for flaky connections (e.g. cell hotspots): retry ONLY on
|
|
5
|
-
* connectivity blips, never on auth / scope / not-found / already-exists /
|
|
6
|
-
* validation errors. Works for both surfaces the provider uses:
|
|
7
|
-
* - the gh CLI path — errors carry the Go HTTP error on `err.stderr`
|
|
8
|
-
* (e.g. `dial tcp ...: i/o timeout`); and
|
|
9
|
-
* - the direct `fetch` path — errors are a `TypeError: fetch failed` with
|
|
10
|
-
* the real reason on `err.cause` (e.g. `ETIMEDOUT`, `ENOTFOUND`).
|
|
11
|
-
*
|
|
12
|
-
* Retrying a non-idempotent create is acceptable for the dominant hotspot
|
|
13
|
-
* failure (`dial tcp ... i/o timeout` means the connection never opened, so
|
|
14
|
-
* the request never reached GitHub). Callers still gate retry per call so
|
|
15
|
-
* the genuinely non-idempotent project-create can opt out.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
const TRANSIENT_RE =
|
|
19
|
-
/i\/o timeout|dial tcp|TLS handshake timeout|connection reset|connection refused|temporary failure|could not resolve host|no such host|network is unreachable|socket hang up|fetch failed|ConnectTimeoutError|UND_ERR_CONNECT_TIMEOUT|ETIMEDOUT|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|\b50[234]\b/i;
|
|
20
|
-
|
|
21
|
-
/** True when an error looks like a retryable network/connectivity blip. */
|
|
22
|
-
export function isTransientNetworkError(err) {
|
|
23
|
-
const hay = [
|
|
24
|
-
err?.stderr,
|
|
25
|
-
err?.message,
|
|
26
|
-
err?.code,
|
|
27
|
-
err?.cause?.message,
|
|
28
|
-
err?.cause?.code,
|
|
29
|
-
]
|
|
30
|
-
.filter(Boolean)
|
|
31
|
-
.join(' ');
|
|
32
|
-
return TRANSIENT_RE.test(hay);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Run `fn`, retrying with exponential backoff ONLY on transient network
|
|
37
|
-
* errors (1s, 2s, 4s by default). Non-transient errors throw immediately so
|
|
38
|
-
* real failures stay loud. `sleep` is injectable for tests.
|
|
39
|
-
*
|
|
40
|
-
* @template T
|
|
41
|
-
* @param {() => Promise<T>} fn
|
|
42
|
-
* @param {{ retries?: number, baseDelayMs?: number,
|
|
43
|
-
* sleep?: (ms: number) => Promise<void> }} [opts]
|
|
44
|
-
* @returns {Promise<T>}
|
|
45
|
-
*/
|
|
46
|
-
export async function withTransientRetry(
|
|
47
|
-
fn,
|
|
48
|
-
{
|
|
49
|
-
retries = 3,
|
|
50
|
-
baseDelayMs = 1000,
|
|
51
|
-
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
52
|
-
} = {},
|
|
53
|
-
) {
|
|
54
|
-
for (let attempt = 0; ; attempt++) {
|
|
55
|
-
try {
|
|
56
|
-
return await fn();
|
|
57
|
-
} catch (err) {
|
|
58
|
-
if (attempt >= retries || !isTransientNetworkError(err)) throw err;
|
|
59
|
-
await sleep(baseDelayMs * 2 ** attempt);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|