mandrel 1.83.0 → 1.85.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/agentrc-reference.json +8 -2
- package/.agents/docs/configuration.md +7 -2
- package/.agents/instructions.md +4 -0
- package/.agents/rules/ci-remediation.md +131 -0
- package/.agents/rules/testing-standards.md +14 -0
- package/.agents/schemas/agentrc.schema.json +29 -6
- package/.agents/schemas/lifecycle/epic.watch.end.schema.json +2 -1
- package/.agents/scripts/git-pr-quality-gate.js +7 -5
- package/.agents/scripts/lib/config/ci.js +24 -3
- package/.agents/scripts/lib/config/explain.js +11 -3
- package/.agents/scripts/lib/config/github.js +11 -7
- package/.agents/scripts/lib/config-settings-schema-delivery.js +21 -0
- package/.agents/scripts/lib/config-settings-schema.js +6 -6
- package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +65 -0
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +401 -84
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +48 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +6 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +172 -58
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +19 -0
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -0
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +17 -16
- package/.agents/scripts/lib/templates/decomposer-prompts.js +17 -3
- package/.agents/scripts/pr-watch-with-update.js +324 -37
- package/.agents/scripts/run-verify.js +18 -3
- package/.agents/scripts/single-story-confirm-merge.js +1 -1
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +32 -1
- package/.agents/skills/core/scope-triage/SKILL.md +5 -4
- package/.agents/workflows/helpers/code-review.md +70 -5
- package/.agents/workflows/helpers/deliver-epic-reference.md +22 -8
- package/.agents/workflows/helpers/deliver-epic.md +123 -28
- package/.agents/workflows/helpers/deliver-stories.md +2 -2
- package/.agents/workflows/helpers/single-story-deliver-reference.md +3 -3
- package/.agents/workflows/helpers/single-story-deliver.md +56 -19
- package/docs/CHANGELOG.md +16 -0
- package/package.json +1 -1
|
@@ -92,7 +92,6 @@
|
|
|
92
92
|
"taskSizing": {
|
|
93
93
|
"softFiles": 15,
|
|
94
94
|
"hardFiles": 30,
|
|
95
|
-
"maxAcceptance": 14,
|
|
96
95
|
"softAcceptanceCount": 10
|
|
97
96
|
}
|
|
98
97
|
},
|
|
@@ -101,7 +100,14 @@
|
|
|
101
100
|
"maxTokenBudget": 300000,
|
|
102
101
|
"lease": { "ttlMs": 900000 },
|
|
103
102
|
"ci": {
|
|
104
|
-
"skipForStoryPushes": true
|
|
103
|
+
"skipForStoryPushes": true,
|
|
104
|
+
"earlyPr": true,
|
|
105
|
+
"watch": {
|
|
106
|
+
"pollIntervalMs": 30000,
|
|
107
|
+
"maxPolls": 120,
|
|
108
|
+
"maxResumes": 3
|
|
109
|
+
},
|
|
110
|
+
"autoMerge": "trust-ci"
|
|
105
111
|
},
|
|
106
112
|
"preflight": {
|
|
107
113
|
"maxStories": 50,
|
|
@@ -108,8 +108,7 @@ top-level keys are validation errors.
|
|
|
108
108
|
| `taskSizing` | No | `object` | — | Story-sizing thresholds consumed by ticket-validator-sizing.js. Operator overrides shallow-merge with DEFAULT_TASK_SIZING defaults. Story #3760 collapsed the per-profile matrix and the parallel testSurface axis into a flat set of knobs; the sizingProfile enum was replaced by an optional body-level `wide` declaration that lifts the hardFiles rejection. Story #3874 cut over to one uniform relaxed profile sized for capability slices a frontier model delivers and self-verifies in one pass. |
|
|
109
109
|
| `taskSizing.softFiles` | No | `integer` | — | File-count soft-warn threshold above which a typical-Story width finding fires (default 15). |
|
|
110
110
|
| `taskSizing.hardFiles` | No | `integer` | — | File-count hard ceiling: a Story exceeding it is rejected unless it declares `wide` with a reason (default 30). |
|
|
111
|
-
| `taskSizing.
|
|
112
|
-
| `taskSizing.softAcceptanceCount` | No | `integer` | — | Soft-warn threshold on acceptance[] item count (default 10). |
|
|
111
|
+
| `taskSizing.softAcceptanceCount` | No | `integer` | — | Soft-warn threshold on acceptance[] item count (default 10). Acceptance mass is advisory-only — there is no hard acceptance ceiling. |
|
|
113
112
|
| `taskSizing.mergeCandidateMaxFiles` | No | `integer` | — | Under-size threshold (Story #4312): a Story with at most this many declared changes[] files, at most mergeCandidateMaxAcceptance acceptance items, and at least one depends_on edge to a sibling trips the advisory `merge-candidate` soft finding (default 3). |
|
|
114
113
|
| `taskSizing.mergeCandidateMaxAcceptance` | No | `integer` | — | Under-size threshold (Story #4312): the acceptance[] item ceiling of the `merge-candidate` soft finding heuristic (default 4). |
|
|
115
114
|
| `failOnSharedEditors` | No | `boolean` | — | — |
|
|
@@ -293,6 +292,12 @@ top-level keys are validation errors.
|
|
|
293
292
|
| `acceptanceEval.maxRounds` | No | `integer` | — | Maximum number of redraft rounds before escalation. Default 2; clamped into [1, hard ceiling] by lib/config/acceptance-eval.js so the cap can never be disabled (maxRounds: 0 clamps up to 1). |
|
|
294
293
|
| `ci` | No | `object` | — | Nested configuration block. |
|
|
295
294
|
| `ci.skipForStoryPushes` | No | `boolean` | — | Story #2899 (Epic #2880, F13). When true (default), pre-push tooling appends a '[skip ci]' trailer to Story-branch commit subjects so intermediate pushes do not stampede the CI fleet. The Epic-branch merge commit produced by story-close.js never carries the marker, regardless of this flag. |
|
|
295
|
+
| `ci.earlyPr` | No | `boolean` | — | Story #4356 (Epic #4355). When true (default), /deliver opens the Epic PR early — before every Story merges — so CI starts warming while later waves run. Set false to defer PR creation until the Epic branch is complete. |
|
|
296
|
+
| `ci.watch` | No | `object` | — | Story #4356 (Epic #4355). Poll-loop tuning for the merge/CI watch. pollIntervalMs is the cadence between check probes; maxPolls caps total probes before the watcher gives up; maxResumes caps how many times the watcher may resume after a transient stall. |
|
|
297
|
+
| `ci.watch.pollIntervalMs` | No | `integer` | — | — |
|
|
298
|
+
| `ci.watch.maxPolls` | No | `integer` | — | — |
|
|
299
|
+
| `ci.watch.maxResumes` | No | `integer` | — | — |
|
|
300
|
+
| `ci.autoMerge` | No | `"trust-ci"` \| `"strict"` | — | Story #4356 (Epic #4355). Merge posture. 'trust-ci' (default) merges once required checks pass; 'strict' additionally requires a clean review gate. |
|
|
296
301
|
| `preflight` | No | `object` | — | Story #2899 (Epic #2880, F13). Thresholds consumed by `.agents/scripts/epic-deliver-preflight.js`. When any value is exceeded the preflight envelope flags a breach and /deliver Phase 1 surfaces it via agent::blocked. |
|
|
297
302
|
| `preflight.maxStories` | No | `integer` | — | — |
|
|
298
303
|
| `preflight.maxWaves` | No | `integer` | — | — |
|
package/.agents/instructions.md
CHANGED
|
@@ -110,6 +110,10 @@ does not re-pay their bytes on every turn.
|
|
|
110
110
|
- [`rules/orchestration-error-handling.md`](rules/orchestration-error-handling.md)
|
|
111
111
|
— before writing or modifying orchestration scripts under
|
|
112
112
|
`.agents/scripts/**`.
|
|
113
|
+
- [`rules/ci-remediation.md`](rules/ci-remediation.md) — before remediating
|
|
114
|
+
a red (or repeatedly slow) CI check during delivery (the root-cause-only
|
|
115
|
+
triage decision tree, the never-rerun / never-quarantine prohibitions,
|
|
116
|
+
and the escalation criteria).
|
|
113
117
|
- [`rules/api-conventions.md`](rules/api-conventions.md),
|
|
114
118
|
[`rules/gherkin-standards.md`](rules/gherkin-standards.md),
|
|
115
119
|
[`rules/changelog-style.md`](rules/changelog-style.md),
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# CI Failure Triage & Remediation
|
|
2
|
+
|
|
3
|
+
This rule applies when a delivery path is watching a pull request's CI checks
|
|
4
|
+
and a required check is red (or repeatedly slow) — the Epic Phase 8
|
|
5
|
+
watch-and-iterate loop
|
|
6
|
+
([`deliver-epic.md`](../workflows/helpers/deliver-epic.md)) and the standalone
|
|
7
|
+
single-Story Step 4 CI watch + fix loop
|
|
8
|
+
([`single-story-deliver.md`](../workflows/helpers/single-story-deliver.md))
|
|
9
|
+
both hand off to it. It is the single triage brain those mechanisms defer to:
|
|
10
|
+
the watcher (`pr-watch-with-update.js`) surfaces the failing check, the run
|
|
11
|
+
link, and the failure signature; this rule decides what to do next.
|
|
12
|
+
|
|
13
|
+
The animating principle: **a red check is a defect until proven otherwise, and
|
|
14
|
+
the fix is always to remove the defect — never to hide it.** There is no
|
|
15
|
+
rerun-the-failed-job path and no quarantine path in this rule, by design.
|
|
16
|
+
Reruns and quarantines mask defects; a flaky test that passes on the second
|
|
17
|
+
attempt is still a bug that will fail a future run for a real user or a future
|
|
18
|
+
delivery. Root-cause it or file it — never re-roll the dice.
|
|
19
|
+
|
|
20
|
+
## The triage decision tree
|
|
21
|
+
|
|
22
|
+
When a required check goes red, walk this tree top to bottom. Do **not** skip
|
|
23
|
+
to "fix" before you have classified the failure — an unclassified fix is a
|
|
24
|
+
guess.
|
|
25
|
+
|
|
26
|
+
### 1. Pull the evidence
|
|
27
|
+
|
|
28
|
+
Fetch the failing job log and record the failure signature (the failing check
|
|
29
|
+
name, the run id / run link, and the first distinctive error line). The
|
|
30
|
+
watcher already writes this to `temp/epic-<epicId>-ci-digest.{json,md}` (Epic
|
|
31
|
+
path) or surfaces it inline (standalone path) — start from that digest.
|
|
32
|
+
|
|
33
|
+
### 2. Classify the failure
|
|
34
|
+
|
|
35
|
+
- **Deterministic (real) failure** — the check fails the same way every time,
|
|
36
|
+
and the failure is caused by the diff under review (a lint violation, a
|
|
37
|
+
broken test, a coverage regression, a baseline drift the diff genuinely
|
|
38
|
+
caused). → Go to [§ Real failures](#real-failures--route-to-the-per-check-fix-table).
|
|
39
|
+
- **Infra / transient failure** — a runner died, a network fetch timed out, a
|
|
40
|
+
dependency registry 5xx'd, a step hit a platform-conditional path. → Go to
|
|
41
|
+
[§ Infra, transient, and flaky failures](#infra-transient-and-flaky-failures-are-root-cause-defects).
|
|
42
|
+
- **Flaky failure** — the check fails intermittently: green on one run, red on
|
|
43
|
+
the next, with **no diff change** between them (order-dependent tests,
|
|
44
|
+
timing/race assertions, shared-state bleed, wall-clock or timezone
|
|
45
|
+
assumptions). → Go to
|
|
46
|
+
[§ Infra, transient, and flaky failures](#infra-transient-and-flaky-failures-are-root-cause-defects).
|
|
47
|
+
|
|
48
|
+
## Real failures — route to the per-check fix table
|
|
49
|
+
|
|
50
|
+
A deterministic failure caused by the diff is remediated at source. Use the
|
|
51
|
+
existing per-check fix table — do **not** duplicate it here:
|
|
52
|
+
|
|
53
|
+
- **Epic Phase 8**:
|
|
54
|
+
[`deliver-epic-reference.md` § Phase 8 — Watch-and-iterate remediation](../workflows/helpers/deliver-epic-reference.md#phase-8--watch-and-iterate-remediation)
|
|
55
|
+
(§ 8.1 Remediation).
|
|
56
|
+
- **Standalone Step 4**:
|
|
57
|
+
[`single-story-deliver-reference.md` § Step 4 — CI watch + fix recovery](../workflows/helpers/single-story-deliver-reference.md#step-4--ci-watch--fix-recovery).
|
|
58
|
+
|
|
59
|
+
In short: lint/format → `npm run lint` + biome apply; maintainability/crap
|
|
60
|
+
baseline drift → re-run the ratcheted script and fix at source (refresh a
|
|
61
|
+
baseline only when the diff demonstrably can't be covered); test failure →
|
|
62
|
+
reproduce with `npm test`, fix source or test; coverage threshold → add tests.
|
|
63
|
+
Commit the fix on the delivery branch (`epic/<epicId>` or `story-<storyId>`),
|
|
64
|
+
push, and re-run the watcher. Auto-merge stays armed across retries.
|
|
65
|
+
|
|
66
|
+
## Infra, transient, and flaky failures ARE root-cause defects
|
|
67
|
+
|
|
68
|
+
Treat every infra/transient failure **and** every flaky failure as a
|
|
69
|
+
root-cause defect. The remediation sequence is the same for both — you do not
|
|
70
|
+
get to wave it off because "CI was flaky."
|
|
71
|
+
|
|
72
|
+
1. **Reproduce.** Run the failing check locally (or in a clean environment as
|
|
73
|
+
close to CI as you can get). Re-run it enough times to observe the
|
|
74
|
+
intermittency for a flaky failure. If you cannot reproduce it at all after a
|
|
75
|
+
genuine attempt, that itself is a finding — record it in the issue you file
|
|
76
|
+
in step 4.
|
|
77
|
+
2. **Check whether it also fails on `main`.** Run the same check against an
|
|
78
|
+
unmodified `main` (or the Epic base branch) checkout. If it fails on `main`
|
|
79
|
+
too, the defect is **pre-existing** — it is not caused by the diff under
|
|
80
|
+
review, and the fix belongs in a separate change, not silently folded into
|
|
81
|
+
this delivery.
|
|
82
|
+
3. **Bisect environment vs. code.** Determine whether the failure is driven by
|
|
83
|
+
the environment (runner OS, Node version, concurrency, a platform-
|
|
84
|
+
conditional branch, an external service) or by the code (an
|
|
85
|
+
order-dependent test, a race, a shared-state assumption). This tells you
|
|
86
|
+
where the fix has to land.
|
|
87
|
+
4. **Then either fix in-scope OR file the defect.**
|
|
88
|
+
- **Fix in-scope** when the root cause is within this delivery's footprint
|
|
89
|
+
and the fix is a cohesive part of the change under review (e.g. a race in
|
|
90
|
+
a test the diff touches, a timezone assumption in code the Story owns).
|
|
91
|
+
Commit it on the delivery branch, push, and re-run the watcher.
|
|
92
|
+
- **File a `meta::framework-gap` issue** when the root cause is outside this
|
|
93
|
+
delivery's scope — a pre-existing flaky test, a runner/infra weakness, a
|
|
94
|
+
framework-level environment gap. Open the issue with the
|
|
95
|
+
`meta::framework-gap` label (see
|
|
96
|
+
[`git-conventions.md` § `meta::framework-gap`](git-conventions.md)),
|
|
97
|
+
and include **the run link and the failure signature** (failing check,
|
|
98
|
+
run id, first distinctive error line) captured in step 1 so a later
|
|
99
|
+
`/plan` Phase 0 sweep can act on it. Then remediate this delivery only if
|
|
100
|
+
the pre-existing defect is genuinely blocking it; otherwise proceed once
|
|
101
|
+
the defect is filed and the check is not caused by your diff.
|
|
102
|
+
|
|
103
|
+
**There is no shortcut.** You may **not** re-run the failed job to "see if it
|
|
104
|
+
goes green," and you may **not** quarantine, skip, or `.only`/`.skip` a flaky
|
|
105
|
+
test to get a green bar. Both mask the defect and are prohibited by this rule.
|
|
106
|
+
|
|
107
|
+
## Escalation criteria
|
|
108
|
+
|
|
109
|
+
Escalate — flip the ticket to `agent::blocked`, post a `friction` comment, and
|
|
110
|
+
hand back to the operator — under **any** of the following. These extend the
|
|
111
|
+
existing three-strikes halt rule; they do not replace it.
|
|
112
|
+
|
|
113
|
+
- **Three strikes (existing).** Three consecutive remediation iterations on the
|
|
114
|
+
same failure class without convergence. Stop; the diagnosis is likely wrong
|
|
115
|
+
(see [`instructions.md` § 1.I Anti-Thrashing](../instructions.md)).
|
|
116
|
+
- **Wall-clock timebox.** Regardless of iteration count, if you have spent more
|
|
117
|
+
than **30 minutes of active remediation** on a single CI failure without a
|
|
118
|
+
green bar in sight, stop and escalate. A long grind on one red check is
|
|
119
|
+
itself a signal that the failure exceeds a single delivery turn's judgment.
|
|
120
|
+
- **Clearly-environmental → escalate immediately (fast path).** When the
|
|
121
|
+
failure is unambiguously environmental and outside your control — a runner
|
|
122
|
+
provisioning failure, a persistent registry/network outage, a
|
|
123
|
+
branch-protection or CI-configuration misconfiguration, an expired
|
|
124
|
+
credential — do **not** burn iterations trying to code around it. File the
|
|
125
|
+
`meta::framework-gap` issue (with run link + signature) and escalate on the
|
|
126
|
+
first encounter. This fast path exists so an operator-side or infra-side
|
|
127
|
+
problem reaches the operator immediately instead of consuming the turn.
|
|
128
|
+
|
|
129
|
+
When you escalate, name the failing check, the run link, the failure
|
|
130
|
+
signature, the classification you reached, and what you tried — never fall
|
|
131
|
+
silent.
|
|
@@ -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
|
|
@@ -339,15 +339,10 @@
|
|
|
339
339
|
"minimum": 1,
|
|
340
340
|
"description": "File-count hard ceiling: a Story exceeding it is rejected unless it declares `wide` with a reason (default 30)."
|
|
341
341
|
},
|
|
342
|
-
"maxAcceptance": {
|
|
343
|
-
"type": "integer",
|
|
344
|
-
"minimum": 1,
|
|
345
|
-
"description": "Hard ceiling on acceptance[] item count (default 14)."
|
|
346
|
-
},
|
|
347
342
|
"softAcceptanceCount": {
|
|
348
343
|
"type": "integer",
|
|
349
344
|
"minimum": 1,
|
|
350
|
-
"description": "Soft-warn threshold on acceptance[] item count (default 10)."
|
|
345
|
+
"description": "Soft-warn threshold on acceptance[] item count (default 10). Acceptance mass is advisory-only — there is no hard acceptance ceiling."
|
|
351
346
|
},
|
|
352
347
|
"mergeCandidateMaxFiles": {
|
|
353
348
|
"type": "integer",
|
|
@@ -1421,6 +1416,34 @@
|
|
|
1421
1416
|
"skipForStoryPushes": {
|
|
1422
1417
|
"type": "boolean",
|
|
1423
1418
|
"description": "Story #2899 (Epic #2880, F13). When true (default), pre-push tooling appends a '[skip ci]' trailer to Story-branch commit subjects so intermediate pushes do not stampede the CI fleet. The Epic-branch merge commit produced by story-close.js never carries the marker, regardless of this flag."
|
|
1419
|
+
},
|
|
1420
|
+
"earlyPr": {
|
|
1421
|
+
"type": "boolean",
|
|
1422
|
+
"description": "Story #4356 (Epic #4355). When true (default), /deliver opens the Epic PR early — before every Story merges — so CI starts warming while later waves run. Set false to defer PR creation until the Epic branch is complete."
|
|
1423
|
+
},
|
|
1424
|
+
"watch": {
|
|
1425
|
+
"type": "object",
|
|
1426
|
+
"description": "Story #4356 (Epic #4355). Poll-loop tuning for the merge/CI watch. pollIntervalMs is the cadence between check probes; maxPolls caps total probes before the watcher gives up; maxResumes caps how many times the watcher may resume after a transient stall.",
|
|
1427
|
+
"properties": {
|
|
1428
|
+
"pollIntervalMs": {
|
|
1429
|
+
"type": "integer",
|
|
1430
|
+
"minimum": 1
|
|
1431
|
+
},
|
|
1432
|
+
"maxPolls": {
|
|
1433
|
+
"type": "integer",
|
|
1434
|
+
"minimum": 1
|
|
1435
|
+
},
|
|
1436
|
+
"maxResumes": {
|
|
1437
|
+
"type": "integer",
|
|
1438
|
+
"minimum": 0
|
|
1439
|
+
}
|
|
1440
|
+
},
|
|
1441
|
+
"additionalProperties": false
|
|
1442
|
+
},
|
|
1443
|
+
"autoMerge": {
|
|
1444
|
+
"type": "string",
|
|
1445
|
+
"enum": ["trust-ci", "strict"],
|
|
1446
|
+
"description": "Story #4356 (Epic #4355). Merge posture. 'trust-ci' (default) merges once required checks pass; 'strict' additionally requires a clean review gate."
|
|
1424
1447
|
}
|
|
1425
1448
|
},
|
|
1426
1449
|
"additionalProperties": false
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://github.com/dsj1984/mandrel/blob/main/.agents/schemas/lifecycle/epic.watch.end.schema.json",
|
|
4
4
|
"title": "epic.watch.end",
|
|
5
|
-
"description": "Emitted by Watcher when required checks settle. checkOutcomes maps check-name → terminal state (success | failure | timed_out | skipped). AutomergePredicate subscribes to this event (test-only Watcher path; the production Phase 8.5 boundary is epic.automerge.start).",
|
|
5
|
+
"description": "Emitted by Watcher when required checks settle. checkOutcomes maps check-name → terminal state (success | failure | timed_out | skipped) or the slow-but-not-failed sentinel `still-running` (Story #4358: the poll cap fired with the check still pending after the resume budget was exhausted — distinct from a genuine `failure`). AutomergePredicate subscribes to this event (test-only Watcher path; the production Phase 8.5 boundary is epic.automerge.start).",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["prUrl", "checkOutcomes"],
|
|
8
8
|
"properties": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"neutral",
|
|
18
18
|
"cancelled",
|
|
19
19
|
"timed_out",
|
|
20
|
+
"still-running",
|
|
20
21
|
"action_required",
|
|
21
22
|
"stale",
|
|
22
23
|
"skipped"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* node:coverage ignore file -- top-level CLI gate; spawns lint/format/test and asserts exit codes — heavy mocking would assert only mock structure */
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* git-pr-quality-gate.js — Lint /
|
|
5
|
+
* git-pr-quality-gate.js — Lint / test / baselines gate for `/git-merge-pr`.
|
|
6
6
|
*
|
|
7
7
|
* `/git-merge-pr` Steps 3–4 previously hardcoded the command sequence
|
|
8
8
|
* (`npm run lint`, `npm run format:check`, `npm test`) in the workflow
|
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
* skill every time.
|
|
12
12
|
*
|
|
13
13
|
* This script runs the gate and emits a structured result so the .md routes
|
|
14
|
-
* on outcome rather than re-implementing the command sequence. The
|
|
15
|
-
*
|
|
14
|
+
* on outcome rather than re-implementing the command sequence. The checks it
|
|
15
|
+
* runs are read from
|
|
16
16
|
* `.agentrc.json → github.branchProtection.requiredChecks` when present,
|
|
17
|
-
* falling back to the hardcoded default trio
|
|
17
|
+
* falling back to the hardcoded default trio, which mirrors the live required
|
|
18
|
+
* check set (`lint`, `test`, `baselines` — Story #4356, Epic #4355; the stale
|
|
19
|
+
* `lifecycle-doc-drift` check was pruned in the same slice).
|
|
18
20
|
*
|
|
19
21
|
* Usage:
|
|
20
22
|
* node .agents/scripts/git-pr-quality-gate.js [--json] [--skip <name>[,<name>]]
|
|
@@ -48,8 +50,8 @@ import { Logger } from './lib/Logger.js';
|
|
|
48
50
|
*/
|
|
49
51
|
export const DEFAULT_CHECKS = Object.freeze([
|
|
50
52
|
{ name: 'lint', cmd: ['npm', 'run', 'lint'] },
|
|
51
|
-
{ name: 'format:check', cmd: ['npm', 'run', 'format:check'] },
|
|
52
53
|
{ name: 'test', cmd: ['npm', 'test'] },
|
|
54
|
+
{ name: 'baselines', cmd: ['node', '.agents/scripts/check-baselines.js'] },
|
|
53
55
|
]);
|
|
54
56
|
|
|
55
57
|
function resolveChecks(config) {
|
|
@@ -1,23 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `delivery.ci` accessor + framework defaults — Story #2899 (Epic #2880, F13)
|
|
2
|
+
* `delivery.ci` accessor + framework defaults — Story #2899 (Epic #2880, F13)
|
|
3
|
+
* and Story #4356 (Epic #4355).
|
|
3
4
|
*
|
|
4
5
|
* `delivery.ci.skipForStoryPushes` defaults to `true` so per-Task Story-branch
|
|
5
6
|
* commits append a `[skip ci]` trailer out of the box. The Epic-branch merge
|
|
6
7
|
* commit produced by `story-close.js`'s merge runner never carries the
|
|
7
8
|
* marker — that path is the one consumers actually want CI to evaluate.
|
|
9
|
+
*
|
|
10
|
+
* Story #4356 adds the CI-aware delivery knobs: `earlyPr` (default `true`)
|
|
11
|
+
* gates whether /deliver opens the Epic PR early so CI warms while later
|
|
12
|
+
* waves run; `watch` tunes the merge/CI watch poll loop; and `autoMerge`
|
|
13
|
+
* (default `"trust-ci"`) selects the merge posture — `"trust-ci"` merges once
|
|
14
|
+
* required checks pass, `"strict"` additionally requires a clean review gate.
|
|
8
15
|
*/
|
|
9
16
|
|
|
10
17
|
export const CI_DELIVERY_DEFAULTS = Object.freeze({
|
|
11
18
|
skipForStoryPushes: true,
|
|
19
|
+
earlyPr: true,
|
|
20
|
+
autoMerge: 'trust-ci',
|
|
12
21
|
});
|
|
13
22
|
|
|
14
23
|
/**
|
|
15
24
|
* Read the merged `delivery.ci` block, applying framework defaults for any
|
|
16
25
|
* field the operator omitted. Accepts the full resolved config, the bare
|
|
17
|
-
* delivery bag, or the bare ci bag.
|
|
26
|
+
* delivery bag, or the bare ci bag. The `watch` sub-block is passed through
|
|
27
|
+
* as-is (undefined when unset) so consumers apply their own poll-loop
|
|
28
|
+
* defaults; only the scalar knobs carry framework defaults here.
|
|
18
29
|
*
|
|
19
30
|
* @param {object | null | undefined} config
|
|
20
|
-
* @returns {
|
|
31
|
+
* @returns {{ skipForStoryPushes: boolean, earlyPr: boolean, autoMerge: 'trust-ci' | 'strict', watch: object | undefined }}
|
|
21
32
|
*/
|
|
22
33
|
export function getCiDelivery(config) {
|
|
23
34
|
const ci = config?.delivery?.ci ?? config?.ci ?? config ?? {};
|
|
@@ -26,5 +37,15 @@ export function getCiDelivery(config) {
|
|
|
26
37
|
typeof ci.skipForStoryPushes === 'boolean'
|
|
27
38
|
? ci.skipForStoryPushes
|
|
28
39
|
: CI_DELIVERY_DEFAULTS.skipForStoryPushes,
|
|
40
|
+
earlyPr:
|
|
41
|
+
typeof ci.earlyPr === 'boolean'
|
|
42
|
+
? ci.earlyPr
|
|
43
|
+
: CI_DELIVERY_DEFAULTS.earlyPr,
|
|
44
|
+
autoMerge:
|
|
45
|
+
ci.autoMerge === 'trust-ci' || ci.autoMerge === 'strict'
|
|
46
|
+
? ci.autoMerge
|
|
47
|
+
: CI_DELIVERY_DEFAULTS.autoMerge,
|
|
48
|
+
watch:
|
|
49
|
+
ci.watch && typeof ci.watch === 'object' ? { ...ci.watch } : undefined,
|
|
29
50
|
};
|
|
30
51
|
}
|
|
@@ -126,10 +126,8 @@ const KEY_MEANINGS = Object.freeze({
|
|
|
126
126
|
'How many recent commits the snapshot summarizes.',
|
|
127
127
|
'planning.riskHeuristics':
|
|
128
128
|
'Phrases that flag a Story as high-risk for HITL escalation.',
|
|
129
|
-
'planning.taskSizing.maxAcceptance':
|
|
130
|
-
'Hard ceiling on acceptance criteria per Story.',
|
|
131
129
|
'planning.taskSizing.softAcceptanceCount':
|
|
132
|
-
'Acceptance-criteria count above which a Story is flagged as large.',
|
|
130
|
+
'Acceptance-criteria count above which a Story is flagged as large (advisory-only — there is no hard acceptance ceiling).',
|
|
133
131
|
'planning.taskSizing.softFiles':
|
|
134
132
|
'Touched-file count above which a Story is flagged as large.',
|
|
135
133
|
'planning.taskSizing.hardFiles':
|
|
@@ -148,6 +146,16 @@ const KEY_MEANINGS = Object.freeze({
|
|
|
148
146
|
'Time-to-live for the Epic lease before a stale claim is reclaimable.',
|
|
149
147
|
'delivery.ci.skipForStoryPushes':
|
|
150
148
|
'Whether Story-branch pushes carry a [skip ci] trailer.',
|
|
149
|
+
'delivery.ci.earlyPr':
|
|
150
|
+
'Whether /deliver opens the Epic PR early so CI warms during later waves.',
|
|
151
|
+
'delivery.ci.watch.pollIntervalMs':
|
|
152
|
+
'Poll cadence (ms) for the merge/CI watch loop.',
|
|
153
|
+
'delivery.ci.watch.maxPolls':
|
|
154
|
+
'Maximum number of poll probes before the CI watch gives up.',
|
|
155
|
+
'delivery.ci.watch.maxResumes':
|
|
156
|
+
'Maximum times the CI watch may resume after a transient stall.',
|
|
157
|
+
'delivery.ci.autoMerge':
|
|
158
|
+
'Merge posture: trust-ci merges on green checks; strict also requires a clean review gate.',
|
|
151
159
|
'delivery.preflight.maxStories':
|
|
152
160
|
'Pre-dispatch ceiling on estimated Story count (no cap when unset).',
|
|
153
161
|
'delivery.preflight.maxWaves':
|
|
@@ -13,23 +13,27 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Default required-check suite.
|
|
17
|
-
* test
|
|
18
|
-
* `github.branchProtection.requiredChecks` in `.agentrc.json`.
|
|
16
|
+
* Default required-check suite. Mirrors the live CI required-check set
|
|
17
|
+
* (`lint` + `test` + `baselines`); consumers override via
|
|
18
|
+
* `github.branchProtection.requiredChecks` in `.agentrc.json`. Kept in sync
|
|
19
|
+
* with `DEFAULT_CHECKS` in `git-pr-quality-gate.js` and the CI job names in
|
|
20
|
+
* `.github/workflows/ci.yml` — the retired `format:check` folded into `lint`
|
|
21
|
+
* (Story #1829) and `lifecycle-doc-drift` collapsed into `lint`/`docs:check`
|
|
22
|
+
* (Epic #1943), so neither belongs in the default set.
|
|
19
23
|
*/
|
|
20
24
|
export const DEFAULT_REQUIRED_CHECKS = Object.freeze([
|
|
21
25
|
Object.freeze({
|
|
22
26
|
name: 'lint',
|
|
23
27
|
cmd: Object.freeze(['npm', 'run', 'lint']),
|
|
24
28
|
}),
|
|
25
|
-
Object.freeze({
|
|
26
|
-
name: 'format:check',
|
|
27
|
-
cmd: Object.freeze(['npm', 'run', 'format:check']),
|
|
28
|
-
}),
|
|
29
29
|
Object.freeze({
|
|
30
30
|
name: 'test',
|
|
31
31
|
cmd: Object.freeze(['npm', 'test']),
|
|
32
32
|
}),
|
|
33
|
+
Object.freeze({
|
|
34
|
+
name: 'baselines',
|
|
35
|
+
cmd: Object.freeze(['node', '.agents/scripts/check-baselines.js']),
|
|
36
|
+
}),
|
|
33
37
|
]);
|
|
34
38
|
|
|
35
39
|
export const BRANCH_PROTECTION_DEFAULTS = Object.freeze({
|
|
@@ -226,10 +226,31 @@ const EPIC_AUDIT_SCHEMA = {
|
|
|
226
226
|
// commit subjects so intermediate pushes do not stampede the CI fleet.
|
|
227
227
|
// The Epic-branch merge commit produced by story-close.js's merge
|
|
228
228
|
// runner never carries the marker, regardless of this flag.
|
|
229
|
+
//
|
|
230
|
+
// Story #4356 (Epic #4355) — CI-aware delivery namespace. `earlyPr` gates
|
|
231
|
+
// whether /deliver opens the Epic PR early (before every Story merges) so CI
|
|
232
|
+
// starts warming while later waves run; defaults to `true` via getCiDelivery.
|
|
233
|
+
// `watch.*` tunes the merge/CI watch poll loop (poll cadence, poll cap, and
|
|
234
|
+
// how many times the watcher may resume after a transient stall).
|
|
235
|
+
// `autoMerge` selects the merge posture: `"trust-ci"` (default) merges once
|
|
236
|
+
// required checks pass, `"strict"` additionally requires a clean review gate.
|
|
237
|
+
const CI_WATCH_SCHEMA = {
|
|
238
|
+
type: 'object',
|
|
239
|
+
properties: {
|
|
240
|
+
pollIntervalMs: { type: 'integer', minimum: 1 },
|
|
241
|
+
maxPolls: { type: 'integer', minimum: 1 },
|
|
242
|
+
maxResumes: { type: 'integer', minimum: 0 },
|
|
243
|
+
},
|
|
244
|
+
additionalProperties: false,
|
|
245
|
+
};
|
|
246
|
+
|
|
229
247
|
const CI_DELIVERY_SCHEMA = {
|
|
230
248
|
type: 'object',
|
|
231
249
|
properties: {
|
|
232
250
|
skipForStoryPushes: { type: 'boolean' },
|
|
251
|
+
earlyPr: { type: 'boolean' },
|
|
252
|
+
watch: CI_WATCH_SCHEMA,
|
|
253
|
+
autoMerge: { type: 'string', enum: ['trust-ci', 'strict'] },
|
|
233
254
|
},
|
|
234
255
|
additionalProperties: false,
|
|
235
256
|
};
|
|
@@ -250,18 +250,18 @@ const CODEBASE_SNAPSHOT_SCHEMA = {
|
|
|
250
250
|
* `planning.taskSizing` — Story-sizing thresholds consumed by
|
|
251
251
|
* `ticket-validator-sizing.js`. Operator overrides shallow-merge with
|
|
252
252
|
* `DEFAULT_TASK_SIZING` defaults (softFiles 15, hardFiles 30,
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
253
|
+
* softAcceptanceCount 10 — the uniform relaxed profile from Story #3874).
|
|
254
|
+
* Story #3760 collapsed the per-profile matrix and the parallel
|
|
255
|
+
* `testSurface` axis into a flat set of knobs; the `sizingProfile` enum was
|
|
256
|
+
* replaced by an optional body-level `wide` declaration that lifts the
|
|
257
|
+
* `hardFiles` rejection. The hard `maxAcceptance` ceiling was removed after
|
|
258
|
+
* the Epic #4355 decomposition experiment — acceptance mass is advisory-only.
|
|
258
259
|
*/
|
|
259
260
|
const TASK_SIZING_SCHEMA = {
|
|
260
261
|
type: 'object',
|
|
261
262
|
properties: {
|
|
262
263
|
softFiles: { type: 'integer', minimum: 1 },
|
|
263
264
|
hardFiles: { type: 'integer', minimum: 1 },
|
|
264
|
-
maxAcceptance: { type: 'integer', minimum: 1 },
|
|
265
265
|
softAcceptanceCount: { type: 'integer', minimum: 1 },
|
|
266
266
|
// Under-size (merge-candidate) thresholds (Story #4312). A Story whose
|
|
267
267
|
// footprint is at or below BOTH ceilings and that carries a `depends_on`
|
|
@@ -27,6 +27,20 @@
|
|
|
27
27
|
* does not attempt to create a duplicate PR. This is the AC-10
|
|
28
28
|
* idempotency contract the Finalizer relies on for cross-process
|
|
29
29
|
* re-runs of `/deliver`.
|
|
30
|
+
*
|
|
31
|
+
* Early-PR draft mode — Story #4359 (Epic #4355).
|
|
32
|
+
*
|
|
33
|
+
* When `delivery.ci.earlyPr` is on (the default), `/deliver` opens the
|
|
34
|
+
* Epic PR as a **draft** at wave 1 (`openOrLocatePr({ draft: true })`)
|
|
35
|
+
* so every subsequent per-wave push runs CI attributed to its own wave.
|
|
36
|
+
* The `draft` flag only appends `--draft` to the `gh pr create` shell —
|
|
37
|
+
* the probe/locate path is unchanged, so a re-run on the same head
|
|
38
|
+
* branch still short-circuits without opening a duplicate. Phase 7 then
|
|
39
|
+
* flips that existing draft to ready-for-review via `markPrReady` rather
|
|
40
|
+
* than creating the PR. When `earlyPr` is off, no draft is opened at
|
|
41
|
+
* wave 1 and Phase 7 opens the PR at close time exactly as before
|
|
42
|
+
* (`openOrLocatePr` with no `draft`). The title/body contract
|
|
43
|
+
* (`feat: Epic #<id>` / `Closes #<id>`) is identical in both modes.
|
|
30
44
|
*/
|
|
31
45
|
|
|
32
46
|
import { spawnSync } from 'node:child_process';
|
|
@@ -119,6 +133,12 @@ export function parsePrViewResult(stdout) {
|
|
|
119
133
|
* or changelog entry).
|
|
120
134
|
* @param {string} [args.body] — explicit PR body override; defaults to
|
|
121
135
|
* `Closes #<epicId>`.
|
|
136
|
+
* @param {boolean} [args.draft] — when `true`, open the new PR as a draft
|
|
137
|
+
* (`gh pr create --draft`). Story #4359: the early-PR path opens the
|
|
138
|
+
* Epic PR as a draft at wave 1 so per-wave pushes warm CI; Phase 7 later
|
|
139
|
+
* flips it ready via `markPrReady`. Only affects the create path — the
|
|
140
|
+
* locate short-circuit never inspects draft state, so a re-run stays
|
|
141
|
+
* idempotent. Default `false`.
|
|
122
142
|
* @param {string} [args.cwd] — working directory for the gh shells.
|
|
123
143
|
* Default `process.cwd()`.
|
|
124
144
|
* @param {Function} [args.ghSpawn] — override the gh invocation for
|
|
@@ -131,6 +151,7 @@ export async function openOrLocatePr({
|
|
|
131
151
|
baseBranch = 'main',
|
|
132
152
|
title,
|
|
133
153
|
body,
|
|
154
|
+
draft = false,
|
|
134
155
|
cwd = process.cwd(),
|
|
135
156
|
ghSpawn = defaultGhSpawn,
|
|
136
157
|
} = {}) {
|
|
@@ -206,6 +227,11 @@ export async function openOrLocatePr({
|
|
|
206
227
|
finalTitle,
|
|
207
228
|
'--body',
|
|
208
229
|
finalBody,
|
|
230
|
+
// Story #4359: open as a draft when the early-PR path requests it.
|
|
231
|
+
// The flag is elided entirely when `draft` is false so the
|
|
232
|
+
// close-time (earlyPr=false) create path is byte-identical to the
|
|
233
|
+
// pre-Story behaviour.
|
|
234
|
+
...(draft ? ['--draft'] : []),
|
|
209
235
|
],
|
|
210
236
|
cwd,
|
|
211
237
|
});
|
|
@@ -239,3 +265,42 @@ export async function openOrLocatePr({
|
|
|
239
265
|
}
|
|
240
266
|
return { prNumber: parsed.number, url: parsed.url, created: true };
|
|
241
267
|
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Mark an existing draft PR as ready-for-review — Story #4359 (Epic
|
|
271
|
+
* #4355). This is the Phase-7 counterpart to the wave-1 draft open: when
|
|
272
|
+
* `delivery.ci.earlyPr` is on, the Epic PR already exists as a draft, so
|
|
273
|
+
* finalize flips it ready rather than creating a PR.
|
|
274
|
+
*
|
|
275
|
+
* Idempotent: `gh pr ready` on an already-ready PR is a no-op that exits
|
|
276
|
+
* 0, so re-running finalize (or replaying after a crash) is safe. When the
|
|
277
|
+
* PR reference does not resolve, `gh` exits non-zero and this helper
|
|
278
|
+
* throws with the stderr detail.
|
|
279
|
+
*
|
|
280
|
+
* @param {object} args
|
|
281
|
+
* @param {string} args.pr — a PR reference `gh pr ready` accepts: the
|
|
282
|
+
* numeric id, the branch name, or the html URL. `openOrLocatePr`
|
|
283
|
+
* returns both `prNumber` and `url`; either is a valid input.
|
|
284
|
+
* @param {string} [args.cwd] — working directory for the gh shell.
|
|
285
|
+
* Default `process.cwd()`.
|
|
286
|
+
* @param {Function} [args.ghSpawn] — override the gh invocation for
|
|
287
|
+
* tests. Same shape as `defaultGhSpawn`.
|
|
288
|
+
* @returns {Promise<{ pr: string, ready: true }>}
|
|
289
|
+
*/
|
|
290
|
+
export async function markPrReady({
|
|
291
|
+
pr,
|
|
292
|
+
cwd = process.cwd(),
|
|
293
|
+
ghSpawn = defaultGhSpawn,
|
|
294
|
+
} = {}) {
|
|
295
|
+
const ref = pr == null ? '' : String(pr).trim();
|
|
296
|
+
if (ref.length === 0) {
|
|
297
|
+
throw new TypeError('markPrReady: pr must be a non-empty reference');
|
|
298
|
+
}
|
|
299
|
+
const ready = ghSpawn({ args: ['pr', 'ready', ref], cwd });
|
|
300
|
+
if (ready.status !== 0) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`markPrReady: gh pr ready failed (status=${ready.status}): ${ready.stderr.trim()}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return { pr: ref, ready: true };
|
|
306
|
+
}
|